Windows API — набор функций операционной
системы
Win16 API и Win32 API
Зачем нужен Win API для VB-программистов
Как изучать Win API
Win API и Dynamic Link Library (DLL)
Совет 1. Следите за правильным
оформлением объявления DLL-процедур
Совет 2. Будьте особенно внимательны
при работе с DLL-функциями
Совет 3. Десять рекомендаций
Дэна Эпплмана по надежному API-программированию в среде VB
Совет 4. Не нужно бояться применять
Win API
Пример обращения к API-функции
Совет 5. Используйте Alias для
передачи параметров As Any
Совет 6. Внимание при работе
со строковыми переменными
Совет 7. Как обращаться к DLL-функциям
Windows API — наиболее важная и мощная дополнительная библиотека функций,
доступная каждому VB-программисту. Многие из них, в том числе и опытные разработчики,
работают с функциями Windows API, используя простые готовые решения, почерпнутые
из различных книг и журналов (возможно, и в нашей постоянной рубрике «Советы
для тех, кто программирует на VB»), и не очень задумываясь о сути этой технологии.
Такой подход является достаточным при решении простых задач, но для серьезной
работы предпочтительнее более детально разобраться с основными принципами использования
функций Windows API. Этим мы сейчас и займемся.
Windows API — набор функций операционной системы
Аббревиатура API многим начинающим
программистам кажется весьма таинственной и
даже пугающей. На самом же деле Application Programming Interface
(API) — это просто некоторый готовый набор
функций, который могут использовать
разработчики приложений. В общем случае данное
понятие эквивалентно тому, что раньше чаще
называли библиотекой подпрограмм. Однако обычно
под API подразумевается особая категория таких
библиотек.
В ходе разработки практически любого
достаточно сложного приложения (MyAppication) для
конечного пользователя формируется набор
специфических внутренних функций, используемых
для реализации данной конкретной программы,
который называется MyApplication API. Однако часто
оказывается, что эти функции могут эффективно
использоваться и для создания других приложений,
в том числе другими программистами. В этом случае
авторы, исходя из стратегии продвижения своего
продукта, должны решить вопрос: открывают они
доступ к этому набору для внешних пользователей
или нет? При утвердительном ответе в описании
программного пакета в качестве положительной
характеристики появляется фраза: «Комплект
включает открытый набор API-функций» (но иногда за
дополнительные деньги).
Таким образом, чаще всего под API
подразумевается набор функций, являющихся
частью одного приложения, но при этом доступных
для использования в других программах. Например,
Excel, кроме интерфейса для конечного пользователя,
имеет набор функций Excel API, который может
использоваться, в частности, при создании
приложений с помощью VB.
Соответственно Windows API — это набор
функций, являющийся частью самой операционной
системы и в то же время — доступный для любого
другого приложения, в том числе написанного с
помощью VB. В этом плане вполне оправданна
аналогия с набором системных прерываний BIOS/DOS,
который фактически представляет собой DOS API.
Отличие заключается в том, что состав функций Windows API, с одной стороны,
значительно шире по сравнению с DOS, с другой — не включает многие средства
прямого управления ресурсами компьютера, которые были доступны программистам
в предыдущей ОС. Кроме того, обращение к Windows API выполняется с помощью обыкновенных
процедурных обращений, а вызов функций DOS — через специальную машинную
команду процессора, которая называется Interrupt («прерывание»).
Win16 API и Win32 API
Как известно, смена Windows 3.x на Windows 95
ознаменовала собой переход от 16-разрядной
архитектуры операционной системы к 32-разрядной.
Одновременно произошла замена 16-разрядного Windows
API (Win16 API) на новый 32-разрядный вариант (Win32 API) —
о некоторых аспектах этого перехода будет
упомянуто в этой главе. В данном случае нужно
просто иметь в виду, что, за небольшим
исключением, набор Win32 API является единым для
семейств Windows 9x и Windows NT.
Далее в этой статье под термином API будет подразумеваться Win API, более того,
по умолчанию — Win32 API.
Зачем нужен Win API для VB-программистов
Несмотря на то что VB обладает огромным
множеством разнообразных функций, в процессе
более-менее серьезной разработки
обнаруживается, что их возможностей зачастую не
достаточно для решения необходимых задач. При
этом программисты-новички часто начинают
жаловаться на недостатки VB и подумывать о смене
инструмента, не подозревая, что на их компьютере
имеется огромный набор средств и нужно только
уметь ими воспользоваться.
При знакомстве с Win API обнаруживается,
что многие встроенные VB-функции — не что иное,
как обращение к соответствующим системным
процедурам, но только реализованное в виде
синтаксиса данного языка. С учетом этого
необходимость использования API определяется
следующим вариантами:
- API-функции, которые полностью реализованы в виде встроенных VB-функций.
Тем не менее иногда и в этом случае бывает полезным перейти к применению API,
так как это позволяет порой существенно повысить производительность (в частности,
за счет отсутствия ненужных преобразований передаваемых параметров). - Встроенные VB-функции реализуют лишь частный случай соответствующей API-функции.
Это довольно обычный вариант. Например, API-функция CreateDirectory обладает
более широкими возможностями по сравнению со встроенным VB-оператором MkDir. - Огромное число API-функций вообще не имеет аналогов в существующем сегодня
варианте языка VB. Например, удалить каталог средствами VB нельзя — для
этого нужно использовать функцию DeleteDirectory.
Следует также подчеркнуть, что
некоторые API-функции (их доля в Win API весьма
незначительна) не могут вызываться из VB-программ
из-за ряда ограничений языка, например из-за
отсутствия возможности работы с адресами памяти.
Но в ряде случаев могут помочь нетривиальные
приемы программирования (в частности, в случае с
теми же адресами).
Личная точка зрения автора такова — вместо расширения от версии к версии
встроенных функций VВ следовало бы давать хорошее описание наиболее ходовых
API-функций. В то же время хочется посоветовать разработчикам не ждать появления
новой версии средства с расширенными функциями, а внимательнее изучить состав
существующего Win API — вполне вероятно, что нужные вам возможности можно
было реализовать уже в версии VB 1.0 выпуска 1991 года.
Как изучать Win API
Это не такой простой вопрос, если
учесть, что число функций Win32 API оценивается
величиной порядка 10 тысяч (точной цифры не знает
никто, даже Microsoft).
В состав VB (версий 4-6) входит файл с
описанием объявлений Win API — WIN32API.TXT (подробнее
о его применении мы расскажем позднее). Но,
во-первых, с его помощью можно получить сведения
о назначении той или иной функции и ее параметрах
только по используемым мнемоническим именам, а
во-вторых — перечень функций в этом файле
далеко не полный. В свое время (семь лет назад) в VB
3.0 имелись специальные справочные файлы с
описанием функций Win16 API. Однако уже в v.4.0 эта
полезная информация с удобным интерфейсом
исчезла.
Исчерпывающую информацию о Win32 API можно
найти в справочной системе Platform Software Development Kit,
которая, в частности, находится на компакт-дисках
MSDN Library, включенных в состав VB 5.0 и 6.0 Enterprise Edition и
Office 2000 Developer Edition. Однако разыскать там нужную
информацию и разобраться в ней совсем не просто.
Не говоря уж о том, что все описания там
приводятся применительно к языку C.
Общепризнанным в мире пособием для
изучения API-программирования в среде VB являются
книги известного американского эксперта Даниэля
Эпплмана (Daniel Appleman). Его серия Dan Appleman’s Visual Basic
Programmer’s Guide to the Windows API (для Win16, Win32, применительно
к разным версиям VB) с 1993 года неизменно входит в
число бестселлеров для VB-программистов. Книгу Dan
Appleman’s VB 5.0 Programmer’s Guide to the Win32 API, выпущенную в 1997
году, автору привез из США приятель, который
нашел ее в первом же книжном магазине небольшого
провинциального городка.
Эта книга объемом свыше 1500 страниц
включает описание общей методики
API-программирования в среде VB, а также более 900
функций. Прилагаемый компакт-диск содержит
полный текст книги и всех программных примеров, а
кроме того, несколько дополнительных глав, не
вошедших в печатный вариант. В 1999 году Дэн
Эпплман выпустил новую книгу Dan Appleman’s Win32 API Puzzle
Book and Tutorial for Visual Basic Programmers, которая включает
сведения о еще 7600 функциях (хотя и не столь
обстоятельные).
Подробнее о книге можно прочитать по адресу www.visual.2000.ru\develop\vb\books-vb\80531api.htm.
Win API и Dynamic Link Library (DLL)
Набор Win API реализован в виде
динамических DLL-библиотек. Далее речь фактически
пойдет о технологии использования DLL в среде VB на
примере библиотек, входящих в состав Win API. Однако,
говоря о DLL, необходимо сделать несколько важных
замечаний.
В данном случае под DLL мы подразумеваем
традиционный вариант двоичных динамических
библиотек, которые обеспечивают прямое
обращение приложений к нужным процедурам —
подпрограммам или функциям (примерно так же, как
это происходит при вызове процедур внутри
VB-проекта). Такие библиотеки могут создаваться с
помощью разных инструментов: VC++, Delphi, Fortran, кроме VB
(посмотрим, что появится в версии 7.0) —
последний может делать только ActiveX DLL, доступ к
которым выполняется через интерфейс OLE Automation.
Обычно файлы динамических библиотек
имеют расширение .DLL, но это совсем не обязательно
(для Win16 часто применялось расширение .EXE);
драйверы внешних устройств обозначаются с
помощью .DRV.
Как мы уже отмечали, определить точное
число API-функций Windows и содержащих их файлов
достаточно сложно, однако все они находятся в
системном каталоге. В этом плане лучше выделить
состав библиотек, входящих в ядро операционной
системы, и основных библиотек с ключевыми
дополнительными функциями.
А теперь несколько советов.
Совет 1. Следите за правильным оформлением объявления
DLL-процедур
Само обращение к DLL-процедурам в программе выглядит точно так же, как к «обычным»
процедурам Visual Basic, например:
Call DllName ([список аргументов])
Однако для использования внешних DLL-функций (в том числе и Win API) их нужно
обязательно объявить в программе с помощью оператора Declare, который имеет
следующий вид:
[Public | Private] Declare Sub ИмяПроцедуры Lib _ “ИмяБиблиотеки” [Alias “Псевдоним”] _ [([СписокАргументов])]
или
[Public | Private] Declare Function ИмяФункции _ Lib “ИмяБиблиотеки” [Alias “Псевдоним”] _ [([СписокАргументов])] [As type]
Здесь в квадратных скобках приведены
необязательные элементы оператора, курсивом
выделены переменные выражения, остальные
слова — ключевые. В справочной системе
приведено достаточно хорошее описание
синтаксиса оператора, поэтому сейчас мы только
отметим некоторые моменты.
Объявления внешних функций должны
размещаться в секции General Declarations модуля. Если вы
размещаете его в модуле формы, то обязательно
нужно указать ключевое слово Private (это объявление
будет доступно только внутри данного
модуля) — таково ограничение для всех
процедур модуля формы.
Набор Win32 API реализован только в виде
функций (в Win16 API было много подпрограмм Sub). В
большинстве своем — это функции типа Long,
которые чаще всего возвращают код завершения
операции.
Оператор Declare появился в MS Basic еще во
времена DOS, причем он использовался и для
объявления внутренних процедур проекта. В Visual Basic
этого не требуется, так как объявлением
внутренних процедур автоматически является их
описание Sub или Function. По сравнению с Basic/DOS в новом
описании обязательно указывать имя
файла-библиотеки, где находится искомая
процедура. Библиотеки Wip API размещаются в
системном каталоге Windows, поэтому достаточно
привести только название файла. Если же вы
обращаетесь к DLL, которая находится в
произвольном месте, нужно записать полный путь к
данному файлу.
Описание оператора Declare обычно занимает довольно много места и не помещается
в одну строку в окне кода. Поэтому мы рекомендуем придерживаться при написании
приложений какой-либо определенной схемы переноса строк, например:
Declare Function GetTempPath _ Lib “kernel32” Alias “GetTempPathA” _ (ByVal nBufferLength As Long, _ ByVal lpBuffer As String) As Long
В этом случае все основные элементы описания разнесены на разные строчки и
поэтому хорошо читаются.
Совет 2. Будьте особенно внимательны при работе с DLL-функциями
Использование Win API и разнообразных
DLL-функций существенно расширяет функциональные
возможности VB и зачастую позволяет повысить
производительность программ. Однако расплата за
это — риск снижения надежности работы
приложения, особенно в процессе его отладки.
Одним из самых важных достоинств среды
VB является надежность процесса разработки
программ: функционируя под управлением
интерпретатора, программный код теоретически не
может нарушить работу Windows и самого VB.
Программист может не очень внимательно следить
за правильностью передачи параметров в
вызываемые функции — подобные ошибки будут
легко обнаружены самим интерпретатором либо в
процессе трансляции кода, либо во время его
выполнения. В самом неприятном случае просто
произойдет прерывание режима обработки, причем с
указанием, где и почему произошла ошибка.
Использование напрямую функций Windows API
или других DLL-библиотек снимает такой контроль за
передачей данных и процессом выполнения кода вне
среды VB. Поэтому ошибка в обращении к внешним
функциям может привести к неработоспособности и
VB и операционной системы. Это особенно актуально
на этапе разработки программы, когда наличие
ошибок — дело вполне естественное. Таким
образом, применяя более широкие возможности
функций базового слоя системы, программист берет
на себя ответственность за правильность их
применения.
Проблема усугубляется еще и тем, что
разные языки программирования используют
различные способы передачи параметров между
процедурами. (Точнее, разные способы передачи
используются по умолчанию, так как многие языки
могут поддерживать несколько способов.) Win API
реализованы на C/C++ и применяют соглашения о
передаче параметров, принятые в этой системе,
которые отличаются от привычного для VB варианта.
В связи с этим следует отметить, что появление встроенных в VB аналогов API-функций
оправданно именно адаптацией последних к синтаксису VB и реализацией соответствующего
механизма контроля обмена данными. Обратим также внимание, что на этапе опытной
отладки приложения при создании исполняемого модуля лучше использовать вариант
компиляции P-code вместо Native Code (машинный код). В первом случае программа
будет работать под управлением интерпретатора — медленнее по сравнению
с машинным кодом, но более надежно с точки зрения возможного ошибочного воздействия
на операционную систему и обеспечивая более удобный режим выявления возможных
ошибок.
Совет 3. Десять рекомендаций Дэна Эпплмана по надежному
API-программированию в среде VB
Использование функции API требует более
внимательного программирования с
использованием некоторых не очень привычных
методов обращения к процедурам (по сравнению с VB).
Далее мы будем постоянно обращаться к этим
вопросам. А сейчас приведем изложение
сформулированных Дэном Эпплманом советов на эту
тему (их первый вариант появился еще в 1993 году) с
некоторыми нашими дополнениями и комментариями.
1. Помните о ByVal. Наиболее частая ошибка, совершаемая при обращении
к функциям API и DLL, заключается в некорректном использовании ключевого слова
ByVal: его или забывают ставить, или, наоборот, ставят, когда в нем нет необходимости.
На этих примерах показано влияние оператора
ByVal на передачу параметров
Тип параметра | С ByVal | Без ByVal |
---|---|---|
Integer | В стек помещается 16-разрядное целое | В стек помещается 32-разрядный адрес 16-разрядного целого |
Long | В стек помещается 32-разрядное целое | В стек помещается 32-разрядный адрес 32-разрядного целого |
String | Строка преобразуется в формат, используемый в С (данные и завершающий нулевой байт). 32-разрядный адрес новой строки помещается в стек |
В стек помещается VB-дескриптор строки. (Такие дескрипторы никогда не используются самим Windows API и распознаются только в DLL, реализованных специально для VB.) |
Здесь следует напомнить, что передача параметров в любой системе программирования,
в том числе и VB, выполняется двумя основными путями: по ссылке (ByRef) или
по значению (ByVal). В первом случае передается адрес переменной (этот вариант
используется в VB по умолчанию), во втором — ее величина. Принципиальное
отличие заключается в том, что с помощью ссылки обеспечивается возврат в вызывающую
программу измененного значения передаваемого параметра.
Чтобы разобраться в этом, проведите эксперимент с помощью таких программ:
Dim v As Integer v = 2 Call MyProc(v) MsgBox “v = “ & v Sub MyProc (v As Integer) v = v + 1 End Sub
Запустив на выполнение этот пример, вы получите сообщение со значением переменной,
равным 3. Дело в том, что в данном случае в подпрограмму MyProc передается адрес
переменной v, физически созданной в вызывающей программе. Теперь измените описание
процедуры на
Sub MyProc (ByVal v As Integer)
В результате при выполнении теста вы получите v = 2, потому что в процедуру
передается лишь исходное значение переменной — результат выполненных с
ним операций не возвращается в вызывающую программу. Режим передачи по значению
можно поменять также с помощью оператора Call следующим образом:
Sub MyProc (v As Integer) ... Call MyProc((v)) ‘ (v) — скобки указывают режим _ передачи по значению.
Однако при обращении к внутренним
VB-процедурам использование в операторе Call
ключевого слова ByVal запрещено — вместо него
применяются круглые скобки. Этому есть свое
объяснение.
В классическом случае (С, Fortran, Pascal)
различие режимов ByRef и ByVal зависит от того, что
именно помещается в стек обмена данными —
адрес переменной или ее значение. В Basic
исторически используется вариант программной
эмуляции ByVal — в стеке всегда находится адрес,
но только при передаче по значению для этого
создается временная переменная. Чтобы отличить
два этих варианта (классический и Basic),
используются разные способы описания режима ByVal.
Отметим, что эмуляция режима ByVal в VB обеспечивает
более высокую надежность программы: перепутав
форму обращения, программист рискует лишь тем,
что в вызывающую программу вернется (или не
вернется) исправленное значение переменной. В
«классическом» же варианте такая путаница может
привести к фатальной ошибке при выполнении
процедуры (например, когда вместо адреса памяти
будет использоваться значение переменной,
равное, скажем, нулю).
DLL-функции реализованы по
«классическим» принципам и поэтому требуют
обязательного описания того, каким образом
происходит обмен данными с каждым из аргументов.
Именно этой цели служат объявления функций через
описание Declare (точнее, списка передаваемых
аргументов). Чаще всего передача параметров в
функцию Windows API или DLL выполняется с помощью
ключевого слова ByVal. Причем оно может быть задано
как в операторе Declare, так и непосредственно при
вызове функции.
Последствия неправильной передачи параметров легко предугадать. В случае получения
явно недопустимого адреса вам будет выдано сообщение GPF (General Protection
Fault — ошибка защиты памяти). Если же функция получит значение, совпадающее
с допустимым адресом, то функция API залезет в чужую область (например, в ядро
Windows) со всеми вытекающими отсюда катастрофическими последствиями.
2. Проверяйте тип передаваемых параметров. Не менее важны верное число
и тип передаваемых параметров. Необходимо, чтобы объявленные в Declare аргументы
соответствовали ожидаемым параметрам в функции API. Наиболее часто встречающийся
случай ошибки в передаче параметров связан с различием между NULL и строкой
нулевой длины — следует помнить, что это не одно и то же.
3. Проверяйте тип возвращаемого значения.
VB довольно терпимо относится к несовпадению типов возвращаемых функцией значений,
поскольку числовые значения обычно возвращаются через регистры, а не через стек.
Следующие правила помогут определить корректное значение, возвращаемое функцией
API:
- DLL-функция, не возвращающая значения (аналог void в ‘C’), должна быть
объявлена как VB Sub. - функция API, возвращающая целое значение (Integer или Long), может быть
определена или как Sub, или как Function, возвращающая значение соответствующего
типа. - ни одна из функций API не возвращает числа с плавающей точкой, но некоторые
DLL вполне могут возвращать такой тип данных.
4. С большой осторожностью используйте конструкцию «As Any». Множество
функций Windows API имеют возможность принимать параметры различных типов и
используют при этом обращение с применением конструкции As Any (интерпретация
типа выполняется в зависимости от значения других передаваемых параметров).
Хорошим решением в этом случае может быть использование нескольких псевдонимов
(Alias) функции с созданием двух и более объявлений для одной и той же функции,
причем в каждом из описаний указываются параметры определенного типа.
5. Не забывайте инициализировать строки. В Win API существует множество
функций, возвращающих информацию путем загрузки данных в передаваемые как параметр
строковые буферы. В своей программе вы можете вроде бы все сделать правильно:
не забыть о ByVal, верно передать параметры в функцию. Но Windows не может проверить,
насколько велик размер выделенного под строку участка памяти. Размер строки
должен быть достаточным для размещения всех данных, которые могут быть в него
помещены. Ответственность за резервирование буфера нужного размера лежит на
VB-программисте.
Следует отметить, что в 32-разрядных
Windows при использовании строк производится
преобразование из Unicode (двухбайтовая кодировка) в
ANSI (однобайтовая) и обратно, причем с учетом
национальных установок системы. Поэтому для
резервирования буферов порой удобнее
использовать байтовые массивы вместо строковых
переменных. (Подробнее об этом будет рассказано
ниже.)
Чаще всего функции Win API позволяют вам самим определить максимальный размер
блока. В частности, иногда для этого нужно вызвать другую функцию API, которая
«подскажет» размер блока. Например, GetWindowTextLength позволяет определить
размер строки, необходимый для размещения заголовка окна, получаемого функцией
GetWindowText. В этом случае Windows гарантирует, что вы не выйдете за границу.
6. Обязательно используйте Option Explicit.
7. Внимательно проверяйте значения параметров и возвращаемых величин. VB
обладает хорошими возможностями по проверке типов. Это означает, что, когда
вы пытаетесь передать неверный параметр в функцию VB, самое плохое, что может
случиться, — вы получите сообщение об ошибке от VB. Но данный механизм, к сожалению,
не работает при обращении к функциям Windows API.
Windows 9x обладает усовершенствованной
системой проверки параметров для большинства
функций API. Поэтому наличие ошибки в данных
обычно не вызывает фатальной ошибки, однако
определить, что же явилось ее причиной — не
так-то просто.
Здесь можно посоветовать использовать
несколько способов отладки ошибки данного типа:
- используйте пошаговый режим отладки или команду Debug.Print для проверки
каждого подозрительного вызова функции API. Проверьте результаты этих вызовов,
чтобы удостовериться, что все в пределах нормы и функция корректно завершилась; - используйте Windows-отладчик типа CodeView и отладочную версию Windows
(имеется в Windows SDK). Эти средства могут обнаружить ошибку параметров и
по меньшей мере определить, какая функция API приводит к ошибке; - используйте дополнительные средства третьих фирм для проверки типов параметров
и допустимости их значений. Такие средства могут не только находить ошибки
параметров, но даже указать на строку кода VB, где произошла ошибка.
Кроме того, нужно обязательно проверять результат выполнения API-функции.
8. Помните, что целые числа в VB и в Windows — не одно и то же. В
первую очередь следует иметь в виду, что под термином «Integer» в VB понимается
16-разрядное число, в документации Win 32 — 32-разрядное. Во-вторых, целые
числа (Integer и Long) в VB — это величины со знаком (то есть один разряд
используется как знак, остальные — как мантисса числа), в Windows —
используются только неотрицательные числа. Это обстоятельство нужно иметь в
виду, когда вы формируете передаваемый параметр с помощью арифметических операций
(например, вычисляете адрес с помощью суммирования некоторой базы и смещения).
Для этого стандартные арифметические функции VB не годятся. Как быть в этом
случае, мы поговорим отдельно.
9. Внимательно следите за именами функций. В отличие от Win16 имена
всех функций Win32 API являются чувствительными к точному использованию строчных
и прописных букв (в Win16 такого не было). Если вы где-то применяете строчную
букву вместо прописной или наоборот, то нужная функция не будет найдена. Следите
также за правильным использованием суффикса A или W в функциях, применяющих
строковые параметры. (Подробнее об этом – см. ниже.)
10. Чаще сохраняйте результаты работы. Ошибки, связанные с неверным
использованием DLL и Win API, могут приводить к аварийному завершению работы
VB-среды, а возможно — и всей операционной системы. Вы должны позаботиться о
том, чтобы написанный вами код перед тестовым запуском был сохранен. Самое простое —
это установить режим автоматической записи модулей проекта перед запуском проекта
в среде VB.
Совет 4. Не нужно бояться применять Win API
После прочтения предыдущего совета
может возникнуть мысль, что использование
функций Win API — дело рискованное. В какой-то
степени это так, но только в сравнении с
безопасным программированием, предоставляемым
самим VB. Но при умелом их применении и знании
возможных подводных камней этот риск минимален.
К тому же полностью отказаться от применения Win API
зачастую просто невозможно — они все равно
потребуются при сколь-нибудь серьезной
разработке.
К тому же ранее мы упоминали о
«подводных» камнях для широкого класса DLL. В
случае с Win API все обстоит гораздо проще, так как
здесь четко унифицирована форма обращения к этим
функциям. При этом следует иметь в виду следующие
основные моменты:
- Функции Win32 API являются именно функциями, то есть процедурами типа Function
(в Win16 API было много подпрограмм Sub). Все это функции типа Long, поэтому
их описания записываются в следующем виде:Declare Function name ... As Long ‘ тип функции _ определяется в явном виде
или
Declare Function name& ‘ тип функции _ определяется с помощью суффикса
Обращение к API-функции выглядит так:
Result& = ApiName& ([СписокАргументов]
- Чаще всего возвращаемое значение функции является кодом завершения операции.
Причем ненулевое значение означает в данном случае нормальное завершение,
нулевое — ошибку. Обычно (но не всегда) уточнить характер ошибки можно
с помощью обращения к функции GetLastError. Описание этой функции имеет такой
вид:Declare Function GetLastError& Lib “kernel32” ()
ВНИМАНИЕ! При работе в среде VB для получения значения уточненного
кода ошибки лучше использовать свойство LastDLLError объекта Err, так как
иногда VB обнуляет функцию GetLastError в промежутке между обращением к
API и продолжением выполнения программы.Интерпретировать код, возвращаемый GelLastError, можно с помощью констант,
записанных в файле API32.TXT, с именами, начинающимися с суффикса ERROR_.Наиболее типичные ошибки имеют следующие коды:
- ERROR_INVALID_HANDLE = 6& — неверный описатель
- ERROR_CALL_NOT_IMPLEMENTED = 120& — вызов в Windows 9x функции,
доступной только для Windows NT - ERROR_INVALID_PARAMETER = 87& — неверное значение параметра
Однако многие функции возвращают значение некоторого запрашиваемого параметра
(например, OpenFile возвращает значение описателя файла). В таких случаях
ошибка определяется каким-либо другим специальным значением Return&,
чаще всего 0 или –1. - Win32 API используют строго фиксированные способы передачи самых простых
типов данных.а) ByVal ... As Long
С помощью переменных типа Long выполняется не менее 80% передачи аргументов.
Обратите внимание, что аргумент всегда сопровождается ключевым словом
ByVal, а это, кроме всего прочего, означает, что выполняется односторонняя
передача данных — от VB-программы к API-функции.б) ByVal ... As String
Этот тип передачи данных также встречается достаточно часто, причем с аргументом
также всегда применяется ByVal. При вызове API-функции в стек записывается
адрес строки, поэтому в данном случае возможен двусторонний обмен данными.
При работе со строками нужно учитывать несколько опасностей.Первая — резервирование памяти под строку производится в вызывающей
программе, поэтому если API-функция будет заполнять строки, то нужно перед
ее вызовом создать строку необходимого размера. Например, функция GetWindowsDirectory
возвращает путь к каталогу Windows, который по определению не должен занимать
более 144 символов. Соответственно обращение к этой функции должно выглядеть
примерно так:WinPath$ = Space$(144) ‘ резервируем строку в _ 144 символа Result& = GetWindowsDirectory& (WinTath$, 144) _ ‘заполнение буфера ‘ Result& — фактическое число символов в имени _ каталога WinPath$ = Left$(WinPath, Result&)
Вторая проблема заключается в том, что при обращении к API-функции производится
преобразование исходной строки в ее некоторое внутреннее представление,
а при выходе из функции — наоборот. Если во времена Win16 эта операция
заключалась лишь в добавлении нулевого байта в конце строки, то с появлением
Win32 к этому добавилась трансформация двухбайтной кодировки Unicode в ANSI
и наоборот. (Об этом подробно говорилось в статье «Особенности работы со
строковыми переменными в VB», КомпьютерПресс 10’99 и 01’2000). Сейчас же
только отметим, что с помощью конструкции ByVal … As String можно обмениваться
строками только с символьными данными.в) ... As Any
Это означает, что в стек будет помещен некоторый адрес буфера памяти, интерпретация
содержимого которого будет выполняться API-функцией, например, в зависимости
от значения других аргументов. Однако As Any может использоваться только
в операторе Declare — при конкретном обращении к функции в качестве
аргумента должна быть определена конкретная переменная.г) ... As UserDefinedType
Такая конструкция также часто применяется, когда необходимо обменяться
данными (в общем случае в обе стороны) с помощью некоторой структуры. На
самом деле эта конструкция — некий вид конкретной реализации формы
передачи As Any, просто в данном случае функция настроена на фиксированную
структуру.Форма структуры данных определяется конкретной API-функцией, и на программисте
лежит ответственность правильным образом описать и зарезервировать ее в
вызывающей программе. Такая конструкция всегда используется без
слова ByVal, то есть в данном случае выполняется передача по ссылке —
в стек записывается адрес переменной.
Пример обращения к API-функции
Проиллюстрируем сказанное выше на примере использования двух полезных функций
работы с файлами — lopen и lread, которые описываются следующим образом:
Declare Function lopen Lib “kernel32” _ Alias “_lopen” (_ ByVal lpFileName As String, _ ByVal wReadWrite As Long) As Long Declare Function lread Lib “kernel32” _ Alias “_lread” (_ ByVal hFile As Long, lpBuffer As Any, _ ByVal wBytes As Long) As Long
В VB их аналогами — в данном случае
точными — являются операторы Open и Get (для
режима Binary). Обратим сразу внимание на
использование ключевого слова Alias в объявлении
функции — это как раз тот случай, когда без
него не обойтись. Настоящие названия функции в
библиотеке начинаются с символа подчеркивания
(типичный стиль для языка C), что не разрешается в
VB.
Операция открытия файла может выглядеть следующим образом:
Const INVALID_HANDLE_VALUE = -1 ‘ неверное _ значение описателя lpFileName$ = “D:\calc.bas” ‘ имя файла wReadWrite& = 2 ‘ режим “чтения-записи” hFile& = lopen(lpFileName$, wReadWrite&) _ ‘ определяем описатель файла If hFile& = INVALID_HANDLE_VALUE Then _ ‘ ошибка открытия файла ‘ уточняем код ошибки CodeError& = Err.LastDllError ‘CodeError& = GetLastError _ ‘ эта конструкция не работает End If
Здесь нужно обратить внимание на два
момента:
- в качестве значения функции мы получаем значение описателя файла. Ошибке
соответствует значение –1; - как раз в данном случае не срабатывает обращение к функции GetLastError —
для получения уточненного значения ошибки мы обратились к объекту Err (о возможности
такой ситуации мы говорили выше).
Далее можно читать содержимое файла, но это предполагает, что программист должен
иметь некоторое представление о его структуре (так же как это происходит при
работе с произвольными двоичными файлами). В этом случае обращение к функции
lread может выглядеть следующим образом:
Dim MyVar As Single wBytes = lread (hFile&, MyVar, Len(MyVar) ‘ чтение вещественного числа, 4 байта ‘ wBytes — число фактически прочитанных данных, ‘ -1 — ошибка ... Type MyStruct x As Single i As Integer End Type Dim MyVar As MyStruct wBytes = lread (hFile&, MyVar, Len(MyVar)) ‘ чтение структуры данных, 6 байтов
Еще раз обратите внимание: второй
аргумент функции передается по ссылке,
остальные — по значению.
Однако если вы хотите прочитать символьные данные в строке переменной длины,
то вам нужно использовать иной вид обращения:
Dim MyVar As String MyVar = Space$(10) ‘резервируем переменную для 10 символов wBytes = lread (hFile&, ByVal MyVar, Len(MyVar)) ‘ чтение символьной строки, 10 символов
Здесь видно важное отличие от
приведенного ранее примера — строковая
переменная обязательно сопровождается ключевым
словом ByVal.
Чтение содержимого файла в массиве (для простоты будем использовать одномерный
байтовый массив) выполняется следующим образом:
Dim MyArray(1 To 10) As Byte wBytes = lread (hFile&, MyArray(1), _ Len(MyArray(1))* 10) ‘ чтение 10 элементов массива
Указывая первый элемент массива в качестве аргумента, мы передаем адрес начала
области памяти, зарезервированной под массив. Очевидно, что таким образом можно
заполнить любой фрагмент массива:
wBytes = lread (hFile&, MyArray(4), _ Len(MyArray(1))* 5) ‘ чтение элементов массива с 4-го по 8-й
Аналогичным образом можно прочитать фрагмент многомерного массива, но при
этом нужно знать алгоритм преобразования многомерной структуры в одномерную.
Совет 5. Используйте Alias для передачи параметров
As Any
Здесь на основе предыдущего примера мы
раскроем суть четвертого совета Дэна Эпплмана.
Работая с функцией lread, следует помнить, что при обращении к ней с использованием
строковой переменной необходимо использовать ключевое слово ByVal (иначе сообщения
о нелегальной операции не избежать). Чтобы обезопасить себя, можно сделать дополнительное
специальное описание этой же функции для работы только со строковыми переменными:
Declare Function lreadString Lib “kernel32” _ Alias “_lread” (_ ByVal hFile As Long, ByVal lpBuffer As String, _ ByVal wBytes As Long) As Long
При работе с этим описанием указывать ByVal при обращении уже не нужно:
wBytes = lreadString (hFile&, MyVarString, _ Len(MyVarString)) ‘
Казалось бы, синтаксис оператора Declare позволяет сделать подобное специальное
описание для массива:
Declare Function lreadString Lib “kernel32” Alias “_lread” (_ ByVal hFile As Long, lpBuffer() As Byte, _ ByVal wBytes As Long) As Long
Однако обращение
wBytes = lreadArray (hFile&, MyArray(), 10)
неизбежно приводит к фатальной ошибке программы.
Совет 6. Внимание при работе со строковыми переменными
Это продолжение разговора об
особенностях обработки строковых переменных в
Visual Basic: VB использует двухбайтную кодировку Unicode,
Win API — однобайтную ANSI (причем с форматом,
принятым в С, — с нулевым байтом в конце).
Соответственно при использовании строковых
переменных в качестве аргумента всегда
автоматически производится преобразование из
Unicode в ANSI при вызове API-функции (точнее, DLL-функции)
и обратное преобразование при возврате.
Вывод из этого простой: с помощью
переменных String можно обмениваться символьными
данными, но нельзя использовать их для обмена
произвольной двоичной информацией (как это было
при работе с 16-разрядными версиями VB). В последнем
случае лучше использовать одномерный байтовый
массив.
Как известно, тип String можно
использовать для описания пользовательской
структуры. В связи с этим нужно помнить
следующее:
- Категорически нельзя использовать для обращения к Win API конструкцию следующего
вида:Type MyStruct x As Single s As String ‘ строка переменной длины End Type
В случае строки переменной длины в составе структуры передается дескриптор
строки со всеми вытекающими отсюда последствиями в виде ошибки выполнения
программы. - Можно использовать в качестве элемента структуры строку фиксированной длины:
Type MyStruct x As Single s As String*8 ‘ строка фиксированной длины End Type
При этом производится соответствующее
преобразование кодировок.
И последнее замечание: применять массив строковых переменных (как фиксированной,
так и переменной длины) при обращении к API-функции нельзя ни в коем случае.
Иначе появление «нелегальной операции» будет гарантировано.
Совет 7. Как обращаться к DLL-функциям
Вполне вероятно, что у вас возникнет
ситуация, когда вам потребуется написать
собственную библиотеку DLL-функций. Потребность в
этом неизбежно появится, если вы будете
использовать технологию смешанного
программирования — использования двух или
более языков программирования для реализации
одного приложения.
Отметим в связи с этим, что смешанное
программирование — это вполне обычное
явление для реализации достаточно сложного
приложения. Действительно, каждый язык (точнее,
система программирования на базе языка) имеет
свои сильные и слабые стороны, поэтому вполне
логично использовать преимущества различных
инструментов для решения разных задач. Например,
VB — для создания пользовательского
интерфейса, С — для эффективного доступа к
системным ресурсам, Fortran — для реализации
численных алгоритмов.
Мнение автора таково: сколь-нибудь
серьезное занятие программированием требует от
разработчика владения по крайней мере двумя
инструментами. Разумеется, в современных
условиях четкого разделения труда очень сложно
быть отличным экспертом даже по двум системам,
поэтому более логичной является схема «основной
и вспомогательный языки». Идея здесь заключается
в том, что даже поверхностное знание
«вспомогательного» языка (написание довольно
простых процедур) может очень заметно повысить
эффективность применения «основного». Отметим,
что знание VB хотя бы в качестве вспомогательного
является сегодня практически обязательным
требованием для профессионального программиста.
Кстати, во времена DOS для любого программиста, в
том числе Basic, было крайне желательным знание
основ Ассемблера.
Так или иначе, но даже в условиях
групповой работы, когда каждый программист
занимается своим конкретным делом,
представление об особенностях процедурного
интерфейса в разных языках должны иметь все
участники проекта. И знать, что многие системы
программирования (в том числе и VB), кроме
интерфейса, используемого по умолчанию,
позволяют применять иные, расширенные методы
обращения к процедурам, которые дают возможность
адаптировать интерфейс к другому языку.
При изучении межпроцедурного
интерфейса следует обратить внимание на
следующие возможные «подводные камни»:
- Разные языки могут использовать различные соглашения о правилах написания
идентификаторов. Например, часто используется знак подчеркивания в начале
имени процедуры, что запрещено в VB. Эта проблема легко решается с помощью
ключевого слова Alias в операторе Declare (см. пример совета 2.3). - Может быть использована разная последовательность записи передаваемых аргументов
в стек. Например, во времена DOS (честно признаюсь — не знаю, как это
выглядит сейчас в среде Windows), C записывал аргументы с конца списка, другие
языки (Fortran, Pascal, Basic) — с начала. - По умолчанию используются разные принципы передачи параметров — по
ссылке или по значению. - Различные принципы хранения строковых переменных. Например, в C (так же
как в Fortran и Pascal) длина строки определяется нулевым байтом в ее конце,
а в Basic длина записывается в явном виде в дескрипторе строки. Разумеется,
нужно иметь в виду возможность использования разных кодировок символов. - При передаче многомерных массивов следует помнить, что возможны различные
варианты преобразования многомерных структур в одномерные (начиная с первого
индекса или с последнего, применительно к двухмерным массивам — «по строчкам»
или «по столбцам»).
С учетом всего этого можно сформулировать следующие рекомендации:
- Используйте самые простые, проверенные способы передачи аргументов в DLL-функции.
Стандарты, принятые для Win API, вполне годятся в качестве образца. - Ни в коем случае не передавайте массивы строковых переменных.
- Очень внимательно используйте передачу простых строковых переменных и многомерных
массивов. - Обязательно специальным образом проверяйте работоспособность механизма
передачи аргументов в вызываемую процедуру и обратно. Напишите специальный
тест для проверки передачи данных. Отдельно проверьте правильность передачи
каждого аргумента. Например, если у вас есть процедура с несколькими аргументами,
проверьте сначала корректность передачи каждого параметра для варианта с одним
аргументом, а уж потом — для всего списка.
А что делать, если DLL-функция уже
написана, например, на Фортране, но ее входной
интерфейс не очень хорошо вписывается в
приведенные выше стандарты VB? Здесь можно дать
два совета. Первый: напишите тестовую DLL-функцию и
с ее помощью постарайтесь методом проб и ошибок
подобрать нужное обращение из VB-программы.
Второй: напишите процедуру-переходник на том же
Фортране, который бы обеспечивал простой
интерфейс между VB и DLL-функцией с преобразованием
простых структур данных в сложные (например,
преобразовывал многомерный байтовый массив в
строковый массив).
Итак: используйте DLL-функции. Но сохраняйте бдительность…
КомпьютерПресс 9’2000
Из книги C#. Советы программистам (в сокращении)
Программный код, который выполняется под управлением CLR (Common Language Runtime, т. е. общая среда выполнения языков), называется управляемым (managed) кодом. Программный код, выполняющийся вне среды выполнения CLR, называется неуправляемым (unmanaged) кодом. Примером неуправляемого программного кода служат функции Win32 API, компоненты COM, интерфейсы ActiveX. Несмотря на большое количество классов .NET Framework, содержащих множество методов, программисту все равно приходится иногда прибегать к неуправляемому коду. Надо сказать, что число вызовов неуправляемого кода уменьшается с выходом каждой новой версии .NET Framework. Microsoft надеется, что наступит такое время, когда весь код можно будет сделать управляемым и безопасным. Но пока реальность такова, что без вызовов функций Windows API нам пока не обойтись. Но сначала немного теории.
Управляемый код .NET Framework может вызывать неуправляемую функцию из DLL (функцию Windows API) при помощи специального механизма Platform Invoke (сокр. P/Invoke). Для того чтобы обратиться к какой-нибудь неуправлямойнеуправляемой библиотеке DLL, вы должны преобразовать .NET-объекты в наборы struct, char* и указателей на функции, как того требует язык C. Как сказали бы программисты на своем жаргоне — вам нужно маршалировать параметры. Более подробно о маршалинге (Marshalling) вам следует почитать в документации. Чтобы вызвать DLL-функцию из C#, сначала ее необходимо объявить (программисты, имеющие опыт работы с Visual Basic 6.0, уже знакомы с этим способом). Для этого используется атрибут DllImport:
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("User32.Dll")]
public static extern void SetWindowText(IntPtr hwnd,
String lpString);
}
Иногда в примерах вы можете также встретить такой способ (длинный и неудобный): [System.Runtime.InteropServices.DllImport(«User32.Dll»)]…, но это на любителя.
Атрибут DllImport сообщает компилятору, где находится точка входа, что позволяет далее вызывать функцию из нужного места. Вы должны всегда использовать тип IntPtr для HWND, HMENU и любых других описателей. Для LPCTSTR используйте String, а сервисы взаимодействия (interop services) выполнят автоматический маршаллинг System.String в LPCTSTR до передачи в Windows. Компилятор ищет указанную выше функцию SetWindowText в файле User32.dll и перед ее вызовом автоматически преобразует вашу строку в LPTSTR (TCHAR*). Почему это происходит? Для каждого типа в C# определен свой тип, используемый при маршалинге по умолчанию (default marshaling type) . Для строк это LPTSTR.
Вызов функций Windows API, имеющих выходной строковый параметр char*
Предположим, нам необходимо вызвать функцию GetWindowText, у которой имеется строковый выходной параметр char*. По умолчанию, для строк используется LPTSTR, но если мы будем использовать System.String, как было сказано выше, то ничего не произойдет, так как класс System.String не позволяет модифицировать строку. Вам необходимо использовать класс StringBuilder, который позволяет изменять строки.
using System.Text; // для StringBuilder
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hwnd,
StringBuilder buf, int nMaxCount);
Тип, используемый для маршашлинга StringBuilder по умолчанию, — тоже LPTSTR, зато теперь GetWindowText может модифицировать саму вашу строку:
StringBuilder sTitleBar = new StringBuilder(255);
GetWindowText(this.Handle, sTitleBar, sTitleBar.Capacity);
MessageBox.Show(sTitleBar.ToString());
Таким образом, ответом на вопрос, как вызывать функцию, у которой есть выходной строковый параметр, будет — используйте класс StringBuilder.
Изменение типа, применяемого для маршалинга по умолчанию
Например, мы хотим вызвать функцию GetClassName, который принимает параметр LPSTR (char*) даже в Unicode-версиях. Если вы передадите строку, общеязыковая исполняющая среда (CLR) преобразует ее в серию TCHAR. Но с помощью атрибута MarshalAs можно переопределить то, что предлагается по умолчанию:
[DllImport("user32.dll")]
public static extern int GetClassName(IntPtr hwnd,
[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,
int nMaxCount);
Теперь, когда вы вызовете GetClassName, .NET передаст вашу строку в виде символов ANSI, а не «широких символов».
Вызов функций, требующих struct
Возьмем для примера функцию GetWindowRect, которая записывает в структуру RECT экранные координаты окна. Чтобы вызвать функцию GetWindowRect и передать ей структуру RECT нужно использовать тип struct в сочетании с атрибутом StructLayout:
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left ;
public int top;
public int right;
public int bottom;
}
[DllImport("user32.dll")]
public static extern int GetWindowRect(IntPtr hwnd, ref RECT rc);
Важно использовать ref, чтобы CLR передала параметр типа RECT как ссылку. В этом случае функция сможет модифицировать ваш объект, а не его безымянную копию в стеке. После такого объявления функции можно ее вызвать в коде:
int w, h;
RECT rc = new RECT();
GetWindowRect(this.Handle, ref rc);
w = rc.right - rc.left;
h = rc.bottom - rc.top;
MessageBox.Show("Ширина формы: " + w + "\n\rВысота формы: " + h);
Обратите внимание, что ref используется и в объявлении, и при вызове функции. Тип, по умолчанию применяемый для маршалинга типов struct — по умолчанию LPStruct, поэтому необходимости в атрибуте MarshalAs нет. Но если вы хотите использовать RECT в виде класса, а не struct, вам необходимо реализовать оболочку:
// Если RECT - класс, а не структура (struct)
[DllImport("user32.dll")]
public static extern int GetWindowRect(IntPtr hwnd,
[MarshalAs(UnmanagedType.LPStruct)] RECT rc);
Работа с функциями обратного вызова в C#
Для использования функций, написанных на C#, в качестве функций обратного вызова Windows, нужно использовать делегаты (delegate).
delegate bool EnumWindowsCB(int hwnd, int lparam);
Объявив свой тип делегата, можно написать оболочку для функции Windows API:
[DllImport("user32")]
public static extern int EnumWindows(EnumWindowsCB cb, int lparam);
Так как в строке с delegate просто объявляется тип делегата (delegate type), сам делегат нужно предоставить в классе:
// В вашем классе
public static bool MyEWP(int hwnd, int lparam)
{
// Делаем тут чего-то
return true;
}
а затем передать оболочке:
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, 0);
Проницательные читатели заметят, что я умолчал о проблеме с lparam.
В языке C, если в EnumWindows дается LPARAM, Windows будет уведомлять вашу функцию обратного вызова этим
LPARAM. Обычно lparam — указатель на некую структуру или класс, который содержит контекстную информацию, нужную вам для выполнения своих операций. Но запомните: в .NET слово «указатель» произносить нельзя! Так что же делать? Можно объявить ваш lparam как IntPtr и использовать GCHandle в качестве его оболочки:
// lparam — теперь IntPtr
delegate bool EnumWindowsCB(int hwnd, IntPtr lparam);
// Помещаем объект в оболочку GCHandle
MyClass obj = new MyClass();
GCHandle gch = GCHandle.Alloc(obj);
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, (IntPtr)gch);
gch.Free();
Не забудьте вызвать Free, когда закончите свои дела! Иногда в C# приходится самому освобождать память. Чтобы получить доступ к «указателю» lparam внутри перечислителя, используйте GCHandle.Target.
public static bool MyEWP(int hwnd, IntPtr param)
{
GCHandle gch = (GCHandle)param;
MyClass c = (MyClass)gch.Target;
// ...пользуемся
return true;
}
Ниже показан написанный мной класс, который инкапсулирует EnumWindows в массив. Вместо того чтобы возиться со
всеми этими делегатами и обратными вызовами, вы пишете:
WindowArray wins = new WindowArray();
foreach (int hwnd in wins)
{
// Делаем тут чегото
}
// WinArray: генерирует ArrayList окон верхнего уровня,
// используя EnumWindows
//
using System;
using System.Collections;
using System.Runtime.InteropServices;
namespace WinArray
{
public class WindowArray : ArrayList {
private delegate bool EnumWindowsCB(int hwnd, IntPtr param);
// Объявляются как private, потому что нужны лишь мне
[DllImport("user32")]
private static extern int EnumWindows(EnumWindowsCB cb,
IntPtr param);
private static bool MyEnumWindowsCB(int hwnd, IntPtr param)
{
GCHandle gch = (GCHandle)param;
WindowArray itw = (WindowArray)gch.Target;
itw.Add(hwnd);
return true;
}
// Это единственный открытый (public) метод.
// Только его вам и надо вызывать.
public WindowArray()
{
GCHandle gch = GCHandle.Alloc(this);
EnumWindowsCB ewcb = new EnumWindowsCB(MyEnumWindowsCB);
EnumWindows(ewcb, (IntPtr)gch);
gch.Free();
}
}
}
Небольшая программа ListWin (Приложение ListWin.cs), которую я написал для перечисления окон верхнего уровня, позволяет просматривать списки HWND, имен классов, заголовков и/или прямоугольников окон, используя RECT или Rectangle. Исходный код ListWin показан не полностью; весь исходный код можно скачать по ссылке, приведенной в конце статьи.
Создание собственной управляемой библиотеки
Можно создать собственную управляемую библиотеку, из которой можно будет вызывать функции Windows API. Для этого в Visual Studio предусмотрены специальные опции. Новый проект создается как библиотека классов (Class Library). Сборка при этом автоматически получает расширение dll. Использовать управляемую библиотеку в управляемом коде просто. Для этого надо добавить ссылку (используя меню Project | Add Reference…) на библиотечную сборку, указав месторасположение сборки в соответствующем диалоговом окне. После этого Visual Studio копирует сборку в директорию, в которой располагается разрабатываемый код. Далее в коде программы используется либо оператор using, либо полное имя библиотечного модуля с точечной нотацией. Все библиотечные классы и методы готовы к использованию в коде программы.
Также можно воспользоваться командной строкой. Чтобы скомпилировать класс Win32API.cs, введите:
csc /t:library /out:Win32API.dll Win32API.cs
В результате у вас будет создан файл Win32API.dll, доступный для любого проекта на C#.
using Win32API // подключаем класс из библиотеки DLL
int hwnd = // ваш код
string s = "Моя строка";
Win32.SetWindowText(hwnd, s); // вызываем функцию из нашей библиотеки
Пример создания библиотеки и использования функций Windows API находится в папке Win32 на прилагаемом к книге диске.
Примеры использования функций API
Вкратце ознакомившись с теорией, перейдем к конкретным примерам. В предыдущих главах я уже неоднократно приводил пример использования функций Windows API для решения различных проблем. Рассмотрим еще несколько полезных советов, которые не вошли в другие главы.
- Блокировка компьютера — Если вам необходимо блокировать компьютер, то вызовите функцию LockWorkStation. Результат работы примера будет аналогичен нажатию комбинации клавиш Win+L или клавиш Ctrl+Alt+Del с последующим выбором кнопки (или команды меню) Блокировка.
- Является ли текущий пользователь администратором? — Если необходимо удостовериться, что текущий пользователь имеет права администратора, то можно вызвать функцию IsUserAnAdmin.
- Мигание заголовка формы — Наверное, вам приходилось видеть, что заголовок окна вдруг начинал мигать, привлекая ваше внимание. Подобный эффект реализуется вызовом функций FlashWindow или FlashWindowsEx.
- Форматирование дисков — Чтобы вызвать стандартное диалоговое окно форматирования дисков нужно воспользоваться функцией SHFormatDrive
- Открытие и закрытие лотка привода компакт-дисков — Наверное, при работе с утилитами, прожигающими компакт-диски CD-R и CD-RW, вы замечали, что у них имеется возможность извлекать компакт-диск из привода программным путем. Неплохо бы научиться делать то же самое при помощи C#. Для этого используем функцию mciSendString в связке с специальными командами, которые и позволят нам открывать и закрывать лоток привода компакт-дисков
- Создание собственного пункта в системном меню — У большинства окон в Windows имеется так называемое системное меню. Внешний вид, как правило, у всех меню одинаков, но иногда попадаются программы, у которых в системном меню имеются свои собственные пункты. Естественно, любого программиста разбирает любопытство — а как реализовать эту функциональность в своей программе. На данный момент .NET Framework не предоставляет такого полезного свойства как Form.SystemMenu или что-то в этом роде. Поэтому придется прибегать к помощи механизма P/Invoke для вызовов функций Windows API. Опытные программисты (особенно имеющие опыт работы с языком С++) знают, что для модификации системного меню используется функция GetSystemMenu, а также вспомогательная функция AppendMenu, которая позволяет добавлять в меню разделители, картинки, галочки и сам текст.
- Извлечение значков из файлов — Функция ExtractIcon позволяет извлекать значки из ресурсов, которые зашиты в файлах EXE, DLL, CPL и др. Кроме того, функция позволяет подсчитать количество значков, находящихся в файле. В качестве испытуемого файла возьмем динамическую библиотеку shell32.dll, которая имеется в любой версии Windows.
- Вызов диалогового окна Смена значка — Существует такая функция Windows API как PickIconDlg. Долгое время она была официально не документирована, но, начиная, с Windows 2000 компания Microsoft все-таки выложила описание этой функции на сайте MSDN. Функция PickIconDlg вызывает стандартное диалоговое окно «Смена значка», позволяющее выбрать значок из модуля. Тем самым, мы можем предоставить пользователю возможность самому выбирать нужный значок, после чего и вывести его на форму (или произвести другие операции).
- Панель задач, кнопка Пуск и часы в области уведомлений — Очень часто программисты хотят получить доступ к стандартным элементам интерфейса Рабочего стола Windows. Например, разработчики хотят получить координаты панели задач, программно нажать на кнопку Пуск, спрятать и показать эту кнопку Пуск и многое другое.
- Смена обоев Рабочего стола — Если вы хотите периодически менять картинку на своем Рабочем столе, то можете это сделать программным способом прямо из своего приложения. Для смены обоев Рабочего стола вызывается одна функция Windows API SystemParametersInfo.
Заключение
Несмотря на огромное число имеющихся классов .NET Framework, программисту по-прежнему приходится прибегать к вызовам системных функций Windows API. В папке Win32Help на прилагаемом к книге компакт-диске вы найдете демо-версию справочника по функциям Windows API для .NET Framework. Если вам понравится этот справочник, то вы можете приобрести его полную версию на моем сайте.
Приложение
Win32API.cs
// Win32API: оболочка для избранных функций Win32 API
// Для компиляции наберите команду:
// csc /t:library /out:Win32API.dll Win32API.cs
using System;
using System.Drawing;
using System.Text;
using System.Runtime.InteropServices;
// Пространство имен для ваших Win32функций.
// Добавляйте их сюда по мере необходимости...
//
namespace Win32API
{
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public POINT(int xx, int yy) { x=xx; y=yy; }
public int x;
public int y;
public override string ToString()
{
String s = String.Format("({0},{1})", x, y);
return s;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
public SIZE(int cxx, int cyy) { cx=cxx; cy=cyy; }
public int cx;
public int cy;
public override string ToString()
{
String s = String.Format("({0},{1})", cx, cy);
return s;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
public int Width() { return right left; }
public int Height() { return bottom top; }
public POINT TopLeft() { return new POINT(left, top); }
public SIZE Size() { return new SIZE(Width(), Height()); }
public override string ToString()
{
String s = String.Format("{0}x{1}", TopLeft(), Size());
return s;
}
}
public class Win32
{
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(int hwnd);
[DllImport("user32.dll")]
public static extern int GetWindowText(int hwnd,
StringBuilder buf, int nMaxCount);
[DllImport("user32.dll")]
public static extern int GetClassName(int hwnd,
[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,
int nMaxCount);
[DllImport("user32.dll")]
public static extern int GetWindowRect(int hwnd, ref RECT rc);
[DllImport("user32.dll")]
// Заметьте, что исполняющая среда знает,
// как выполнить маршалинг Rectangle
public static extern int GetWindowRect(int hwnd, ref Rectangle rc);
// ...продолжайте добавлять нужные функции
}
}
ListWin.cs
using System;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Win32API; // самодельная оболочка для Win32 API
using WinArray; // самодельный перечислитель окон
class MyApp
{
// Глобальные ключи командной строки
static bool bRectangle = false; // показывает прямоугольник окна,
// используя Rectangle
static bool bRect = false; // показывает прямоугольник окна,
// используя RECT
static bool bClassName = false; // показывает имя класса
static bool bTitle = false; // показывает заголовок
static bool bHwnd = false; // показывает HWND
[STAThread]
// Main — главная точка входа
static int Main(string[] args) {
// Разбираем командную строку.
// Ключи могут быть указаны в любом порядке.
if (args.GetLength(0)<=0)
return help();
for (int i=0, len=args.GetLength(0); i<len; i++)
{
if (args[i].StartsWith("/") || args[i].StartsWith("") )
{
for (int j=1; j<args[i].Length; j++)
{
switch (args[i][j])
{
case 'c': bClassName = true; break;
case 'h': bHwnd = true; break;
case 'r': bRect = true; break;
case 'R': bRectangle = true; break;
case 't': bTitle = true; break;
case '?': default: return help();
}
}
}
}
WindowArray itw = new WindowArray();
foreach (int hwnd in itw)
{
if (Win32.IsWindowVisible(hwnd))
{
if (bHwnd)
{
Console.Write("{0:x8}", hwnd);
}
if (bClassName)
{
StringBuilder cname = new StringBuilder(256);
Win32.GetClassName(hwnd, cname, cname.Capacity);
Console.Write(" {0}",cname);
}
if (bRectangle)
{
Rectangle rc = new Rectangle();
Win32.GetWindowRect(hwnd, ref rc);
Console.Write(" {0}",rc);
}
else if (bRect)
{
RECT rc = new RECT();
Win32.GetWindowRect(hwnd, ref rc);
Console.Write(" {0}",rc);
}
if (bTitle)
{
StringBuilder title = new StringBuilder(256);
Win32.GetWindowText(hwnd, title, title.Capacity);
Console.Write(" {0}",title);
}
Console.WriteLine();
}
}
return 0;
}
static int help()
{
Console.WriteLine("ListWin: List toplevel windows.");
Console.WriteLine(" Copyright 2002 Paul DiLascia.");
Console.WriteLine("Format: ListWin [/chrRt]");
Console.WriteLine("Options:");
Console.WriteLine(" /c(lassname) show window class name");
Console.WriteLine(" /h(wnd) show HWNDs");
Console.WriteLine(" /t(itle) show title (caption)");
Console.WriteLine(" /r(ect) show window rect using RECT");
Console.WriteLine(" /R(ectangle) show window rect using Rectangle");
Console.WriteLine("");
return 0;
}
}
Дополнительная информация
Реклама
Работа с портами
в API
Windows
строится так же, как работа с файлами.
Порт открывается как файл с помощью
функции CreateFile.
Эта функция объявлена в файле winbase.h
следующим образом:
HANDLE
CreateFile(
LPCTSTR
lpFileName,
DWORD
dwDesiredAccess,
DWORD
dwShareMode,
LPSECURITY_ATTRIBUTES
lpSecurityAttributes,
DWORD
dwCreationDestribution,
DWORD
dwFlagsAndAttributes,
HANDLE
hTemplateFile
).
Параметр lpFileName
содержит имя открываемого порта. Для
последовательных портов используются
имена COM1,
COM2
и т.д. Однако невозможно открыть порт,
к которому подключены мышь, принтер или
другие устройства. Впрочем, порт,
используемый модемом, можно открыть,
если он в данный момент неактивен.
Параметр
dwDesiredAccess
определяет флаги доступа к порту. Он
может содержать комбинацию следующих
флагов:
0 – разрешает
опрашивать атрибуты порта;
GENERIC_READ
– разрешает чтение;
GENERIC_WRITE
– разрешает запись.
Комбинация флагов
с помощью операции ИЛИ позволяет открыть
порт одновременно и для записи, и для
чтения.
Параметр dwShareMode
определяет режим совместного доступа.
К порту совместный доступ невозможен,
так что этот параметр всегда должен
быть равен 0. Точно также при работе с
портами должны быть равны NULL
параметры lpSecurityAttributes
(определяет наследование порождаемыми
процессами) и hTemplateFile
(шаблон и атрибуты создаваемого файла).
Параметр dwCreationDistribution,
определяющий действия с создаваемым
файлом, должен для портов всегда иметь
значение OPEN_EXISTING
– открытие существующего порта.
Параметр
dwFlagsAndAttributes
в случае портов определяет режим работы:
0 – синхронный, FILE_FLAG_OVERLAPPED
– асинхронный, то есть фоновая обработка
ввода и вывода.
В случае успешного
выполнения функция CreateFile
возвращает дескриптор порта. Успешность
выполнения можно проверить с помощью
функции GetLastError.
Перед запуском функции CreateFile
имеет смысл применить функцию
SetLastError(0)
для того, чтобы исключить возможность
чтения ложных (предыдущих) ошибок.
После завершения
работы с портом он должен быть закрыт
функцией CloseHandle:
BOOL
CloseHandle(IN
OUT
HANDLE
hObject);
Таким образом,
работа с портом организуется по следующей
схеме:
HANDLE
port;
.
. . .
port =
CreateFile(“COM1”,GENERIC_READ|GENERIC_WRITE,
0,0,OPEN_EXISTING,0,0);
< настройка порта
>
< чтение/запись >
. . . .
CloseHandle(port).
Первый из приведенных
выполняемых операторов открывает порт
COM1
для чтения и записи в синхронном режиме.
Последний оператор закрывает порт.
Если вы работаете
с последовательным портом, то, после
того как он открыт, можно получить
информацию о его параметрах с помощью
функции GetCommState,
объявленной в файле Winbase.h
следующим образом:
BOOL
GetCommState (IN HANDLE hFile, OUT LPDCB lpDCB).
Параметр
hFile
– дескриптор порта, а параметр lpDCB
определяет структуру типа TDCB,
в которую функция GetCommState
заносит информацию. Такой же тип структуры
используется и при настройке порта.
Ограничимся знаниями о тех полях, которые
являются ключевыми для работы.
Поле BaudRate
определяет скорость передачи данных в
битах в секунду. Это целое число, которое
можно указывать или непосредственно,
или с помощью констант CBR_110,
CBR_300,
CBR_600,
CBR_1200,
. . ., CBR_256
000. Смысл констант ясен из наименований.
Так что, если требуется установить
значение BaudRate,
его можно задать, например, равным 14 400
или равным CBR_14
400. Результат будет одинаковым.
Поле ByteSize
определяет число битов данных в
передаваемых и принимаемых байтах. Это
поле может иметь значения 4, 5, 6, 7, 8.
Впрочем, не всегда все перечисленные
значения допустимы.
Поле Parity
задает схему контроля четности. Оно
может принимать следующие значения
(константы и их значения описаны в файле
Windows.h):
NOPARITY 0 Отсутствие
бита четности
ODDPARITY 1 Дополнение
до нечетности
EVENPARITY 2 Дополнение
до четности
MARKPARITY 3 Бит
четности всегда равен 1
SPACEPARITY 4 Бит
четности всегда равен 0.
Поле StopBits
задает число стоповых бит. Оно может
принимать следующие значения (константы
и их значения описаны в файле Windows.h):
ONESTOPBIT 0 Один
бит
ONE5STOPBITS 1 Полтора
бита
TWOSTOPBITS 2 Два
бита.
Поле EvtChar
задает символ, приход которого генерирует
некоторое событие. Этот символ может
использоваться в асинхронном режиме
работы.
При ошибке
выполнения функция GetCommState
возвращает false.
А если ошибки не произошло, функция
заполняет структуру lpDCB
типа TDCB
текущими параметрами настройки порта.
Далее можно изменить значения элементов
этой структуры и передать новые значения
параметров в порт с помощью функции
SetCommState:
BOOL
SetCommState (IN HANDLE hFile, IN LPDCB lpDCB).
Параметры
этой функции тождественны параметрам
функции GetCommState.
В случае неудачи функция возвращает
false.
Неудача может быть связана с заданием
недопустимых значений для данного
порта.
Помимо основных
параметров порта, существует еще ряд,
не менее важных, определяющих максимальное
время, отводимое на операции чтения и
записи. Эти параметры определяют тайм-аут
(timeout)
– интервал времени, в течение которого
функции записи и чтения ожидают появления
новой информации. Если это время истекает,
чтение или запись прерываются.
Получить информацию
о временных параметрах порта можно
функцией GetCommTimeouts:
BOOL
GetCommTimeouts (IN HANDLE hFile,
OUT
LPCOMMTIMEOUTS lpCommTimeouts).
Параметр
hFile является
дескриптором
порта,
а
lpCommTimeouts – структура
типа
TCommTimeouts, содержащая
следующие
поля:
DWORD
ReadIntervalTimeout
– максимальное время, допустимое между
двумя последовательными символами,
считываемыми с коммуника-ционной линии.
DWORD
ReadTotalTimeoutMultiplier
– множитель,
используемый для вычисления общего
тайм-аута операции чтения.
DWORD
ReadTotalTimeoutConstant
– константа, используемая для вычисления
общего тайм-аута операции чтения.
DWORD
WriteTotalTimeoutMultiplier
– множитель, используемый для вычисления
общего тайм-аута операции записи.
DWORD
WriteTotalTimeoutConstant
– константа, используемая для вычисления
общего тайм-аута операции записи.
Значения всех
полей указывается в миллисекундах. Поле
ReadIntervalTimeout
определяет максимальный интервал между
двумя последовательными символами,
точнее, между началами передачи двух
символов, включая время передачи самого
символа. Если интервал между символами
превысит заданное значение, операция
чтения завершится и все данные, накопленные
в буфере, передадутся в программу.
Нулевое значение данного поля означает,
что данный тайм-аут не используется.
Если же в поле подать значение,
соответствующее MAXDWORD
(4294967295), и одновременно задать нулевые
значения полей ReadTotalTimeoutConstant
и ReadTotalTimeoutMultiplier,
то операция чтения немедленно завершится
и вернет уже принятый символ, даже если
ни одного символа не было получено из
линии.
Во время операции
чтения временной период начинает
отсчитываться с момента приема первого
символа. Общий интервал времени на
операцию чтения рассчитывается как
произведение значения ReadTotalTimeoutMultiplier
на количество запрошенных символов N,
и к этому произведению добавляется
значение ReadTotalTimeoutConstant.
Пусть, например,
требуется прочитать 100 символов при
скорости порта 1200. И пусть на каждый
символ используется 8 бит данных, бит
дополнения четности, стоповый бит и
стартовый бит – итого 11 бит на символ.
Значит, на всю операцию чтения требуется
100*11/12 000 с или 93 мс. К этому времени надо
прибавить 99 интервалов между импульсами.
Например, если средний ожидаемый интервал
между окончанием одного и началом
следующего символа равен 1 мс, то на всю
операцию чтения потребуется 93 + 99 =
192 мс. Для решения подобной задачи можно
задать ReadIntervalTimeout
= 4 мс (в 2 раза больше среднего значения,
складывающегося из времени передачи
символа и промежутка между ними). Значение
ReadTotalTimeoutMultiplier
можно установить равным
2 мс и задать
некоторое значение ReadTotalTimeoutConstant
в качестве страховки от возможных
колебаний скорости чтения. В результате
в нашем примере общее время может
составить 300 мс. Если операция чтения
выполняется дольше, разумно предположить,
что это вызвано какими-то ошибками
внешнего устройства. В этом и заключается
смысл задания тайм-аута: через заданный
интервал времени считывание прекратится,
и тем самым можно будет избежать
возможного «зависания» программы. При
этом будут возвращены символы, прием
которых завершился до истечения
тайм-аута. Остальные символы можно
получить следующей операцией чтения.
Если между началами двух последовательных
символов пройдет более 4 мс, то операция
чтения также будет завершена.
Параметры
WriteTotalTimeoutMultiplier
и WriteTotalTimeoutConstant
аналогичны параметрам ReadTotalTimeoutMultiplier
и ReadTotal-
TimeoutConstant,
но относятся к операции записи.
Рассмотренная
функция GetCommTimeouts
позволяет определить текущие временные
параметры порта, а функция SetCommTimeouts
устанавливает эти параметры:
BOOL
SetCommTimeouts (IN HANDLE hFile,
IN
LPCOMMTIMEOUTS lpCommTimeouts).
Синхронный
режим
работы
Рассмотрим
операции чтения и записи. Обычно перед
началом этих операций необходимо
очистить буфер порта от мусора, а иногда
и отменить выполняющуюся в данный момент
предыдущую операцию записи или чтения.
Это можно сделать при помощи функции
PurgeComm:
BOOL
PurgeComm
(IN
HANDLE
hFile,
IN
DWORD
dwFlags).
Параметр hFile
является дескриптором порта, а параметр
dwFlags
указывает выполняемые операции. Он
может комбинироваться операцией ИЛИ
из следующих флагов:
PURGE_TXABORT
– немедленно завершить все операции
записи;
PURGE_RXABORT
– немедленно завершить все операции
чтения;
PURGE_TXCLEAR
– очистить в драйвере очередь передачи;
PURGE_RXCLEAR
– очистить в драйвере очередь приема.
Функция PurgeComm
позволяет очистить буфер от мусора,
который может быть следствием работы
какой-то предыдущей программы, и позволяет
прервать операции чтения и записи в
случае ошибки. Полезно также вызвать
эту функцию перед завершением работы
вашего приложения, чтобы не оставлять
мусор другим программам. Но надо
учитывать, что очистка буфера не означает
передачу находящихся в нем данных. Эти
данные просто стираются. Если же надо
завершить передачу данных, содержащихся
в буфере, то вместо PurgeComm
надо вызвать функцию FlushFileBuffers:
BOOL
FlushFileBuffers
(IN
HANDLE
hFile).
Эта функция
обеспечивает передачу данных из выходного
буфера и только после этого очищает
его.
Синхронный прием
и передача осуществляется функциями:
BOOL ReadFile
(IN HANDLE hFile, OUT LPVOID lpBuffer,
IN
DWORD nNumberOfBytesToRead,
OUT
LPDWORD lpNumberofBytesRead,
IN
LPOVERLAPPED lpOverlapped);
BOOL
WriteFile (IN HANDLE hFile, IN LPCVOID lpBuffer,
IN
DWORD nNumberOfBytesToWrite,
OUT
LPDWORD lpNumberOfBytesWritten,
IN
LPOVERLAPPED
lpOverlapped).
Параметр hFile
– дескриптор порта, параметр Buffer
– буфер, данные которого передаются
или в который заносятся принимаемые
данные. Параметры nNumberOfBytesToRead
и nNumberOfBytesToWrite
– число байт, которые должны быть
переданы или приняты. Параметры
lpNumberOfBytesRead
и lpNumberOfBytes-Written
– число реально переданных или принятых
байт. Это число может быть меньше
ожидаемого из-за ошибок или прерываний
по тайм-ауту. Параметр lpOverlapped
используется для асинхронного чтения
и записи. Для синхронных операций этот
параметр должен быть равен NULL.
Пример передачи
данных:
FlushFileBuffers
(port);
WriteFile
(port, buf.c_str(), buf.Lenght(), &DWORD(n), NULL);
ShowMessage
(“Передано
”+IntToStr(n)+” байт”);
Пример
чтения
данных:
AnsiString
buf; int n;
.
. . .
buf
= ””;
FlushFileBuffers
(port);
ReadFile
(port, buf.c_str(), 128, &DWORD(n), NULL);
ShowMessage
(“Принято
”+IntToStr(n)+” байт:
”+buf).
Надо
учитывать, что если для операций чтения
не заданы временные параметры, то
операция ReadFile
может бесконечно ожидать прихода входных
сигналов. В этом заключается один из
недостатков синхронного чтения.
Порт можно перевести
в состояние разрыва связи с помощью
функции
BOOL
SetCommBreak (IN HANDLE hFile).
При
вызове этой функции передача данных
прекращается, выходная линия переводится
в состояние «0», после чего и приемник
фиксирует состояние разрыва. Возобновить
прерванную передачу данных можно
функцией
BOOL
ClearCommBreak
(IN
HANDLE
hFile).
Асинхронный
режим работы
В большинстве
случаев синхронный режим работы с
последовательным портом – не лучшее
решение, так как приложение останавливается,
ожидая завершения команды ввода или
вывода. Более разумным является
асинхронный режим работы с портами. В
этом режиме вы можете указать системе,
какие события порта ей надо отслеживать.
Это
делается
с
помощью
функции
BOOL
SetCommMask (IN HANDLE hFile, IN DWORD dwEvtMask).
Параметр
hFile
является дескриптором порта, а параметр
dwEvtMask
– маской, в которой можно указывать
комбинацию следующих
событий:
EV_BREAK
– разрыв приемной линии;
EV_CTS
– изменение состояния линии CTS;
EV_DSR
– изменение состояния линии DSR;
EV_ERR
– ошибка;
EV_RING
– входящий звонок на модем;
EV_RLSD
– изменение состояния линии RLSD;
EV_RXCHAR
– принят символ и помещен в приемный
буфер;
EV_RXFLAG
– принят символ, заданный полем EvtChar
структуры типа TDCB,
использованной при настройке порта;
EV_TXEMPTY
– передан последний символ буфера
передачи.
Если параметр
dwEvtMask
в функции SetCommMask
равен нулю, никакие события не
отслеживаются.
Имеется функция
GetCommMask,
позволяющая получить текущую маску
событий:
BOOL
GetCommMask (IN HANDLE hFile, OUT LPDWORD lpEvtMask).
Она
заносит маску в переменную, заданную
параметром lpEvtMask.
Если вы задали
маску событий, то можете приостановить
выполнение приложения до наступления
события. Это
делает
функция
BOOL
WaitCommEvent (IN HANDLE hFile, OUT LPDWORD lpEvtMask,
IN
LPOVERLAPPED
lpOverlapped).
В параметр lpEvtMask
будут заноситься флаги, соответствующие
произошедшим событиям. Параметр
lpOverlapped
является указателем на структуру типа
TOverlapped.
Если вы просто хотите приостановить
выполнение до появления соответствующего
события, параметр lpOverlapped
можно задать равным NULL.
Например, чтение
можно организовать следующим образом:
AnsiString
buf; int n; DWORD lpEvtMask;
.
. . .
SetCommMask
(port, EV_RXCHAR);
WaitCommEvent
(port, &lpEvtMask, NULL);
ReadFile
(port, buf.c_str(), 128, &DWORD(n), NULL);
ShowMessage
(“Принято
”+IntToStr(n)+” байт:
”+buf).
В
этом примере вызов функции ReadFile
произойдет только после того, как на
входную линию придет первый символ. До
этого момента не будет возврата из
функции WaitCommEvent.
Подобная организация
чтения не очень улучшает ваше приложение.
Правда, на время ожидания ресурсы
компьютера освобождаются, но приложение
все равно «зависает», пока не придет
входной символ. Полноценный асинхронный
режим, позволяющий вашему приложению
работать, пока осуществляются чтение
и запись, организуется следующим образом.
В
вызове
функции
CreateFile задается
dwFlagsAndAttributes = = FILE_FLAG_O-VERLAPPED.
Вводится
глобальная
переменная
типа
TOverlapped. Это
тип
структуры,
содержащей
поля
hEvent, Internal, InternalHigh, Offset, OffsetHigh. Для
организации простой работы достаточно
указать все эти поля равными 0.
В вызовах функций
ReadFile
и WriteFile
параметр lpOverlapped
должен быть указателем на введенную
нами структуру типа TOverlapped.
В этом случае функции немедленно
возвращают управление, приложение
может продолжать выполняться. Успешность
запуска асинхронного чтения или записи
можно проверить функцией GetLastError.
Если эта функция вернет значение
ERROR_IO_PENDING,
значит, асинхронная операция стартовала
успешно. Любое другое значение,
возвращаемое функцией GetLastError,
свидетельствует об ошибке. Также следует
учесть, что, хотя параметры lpNumberOfBytesRead
и nNumberOfBytesToWrite
функций ReadFile
и WriteFile
по-прежнему надо задавать, они не будут
содержать никакой полезной информации.
Поскольку функции возвращаются сразу,
соответствующие числа всегда окажутся
равными нулю и не будут отражать число
переданных или полученных байтов.
В процессе
выполнения программы следует периодически
опрашивать систему, чтобы получить
информацию о завершении записи или
чтения. Опрос
производится
функцией
BOOL
GetOverlappedResult (IN HANDLE hFile,
IN
LPOVERLAPPED lpOverlapped,
OUT
LPDWORD lpNumberOfBytesTransferred,
IN BOOL
bWait).
Параметр
hFile
является дескриптором порта, параметр
lpOverlapped
– адресом структуры типа TOverlapped.
Параметр lpNumberOfBytesTransferred
указывает переменную, в которой
отображается число записанных или
прочитанных байтов. Параметр bWait
указывает, должна ли функция ждать
окончания операции записи или чтения.
Если задать значение этого параметра
равным true,
то функция будет ждать окончания
соответствующей операции и, следовательно,
будет реализован практически синхронный
режим. Если задать bWait
= false,
функция немедленно вернет управление.
Если возвращенное функцией значение
равно true,
значит соответствующая операция записи
или чтения завершилась. В этом случае
lpNumberOfBytesTransferred
укажет число переданных байтов. Если
возвращенное значение равно false,
значит, операция не завершена или
получилась ошибка в вызове функции
GetOverlappedResult.
Различить эти два варианта можно функцией
GetLastError.
Если она вернет ERROR_IO_INCOMPLETE,
значит, операция не завершилась. В
противном случае – ошибка в вызове
функции GetOverlappedResult.
Рассмотренная
выше организация асинхронной записи и
чтения работает в Windows
NT/2000/XP.
Если требуется создать приложение,
которое работает также в Windows
95/98, то организовать запись и чтение
надо несколько иначе. В поле hEvent
структуры TOverlapped
надо заносить результат, возвращаемый
функцией:
HANDLE
CreateEvent (IN LPSECURITY_ATTRIBUTES pEventAttributes, IN BOOL
bManualReset, IN BOOL InitialState,
IN
LPCSTR
lpName).
Эта функция создает
объект события и возвращает его
дескриптор. В простом случае параметры
lpEventAttributes
и lpName
надо задать равными NULL,
параметры bManualReset
и bInitialState
– равными false.
Так что в приведенных примерах в
процедурах записи и чтения необходимо
добавить перед вызовами функций WriteFile
и ReadFile
операторы
Overlapp.hEvent
= CreateEvent (NULL, false, false, NULL).
Опрос
производится
при
помощи
функции
DWORD
WaitForSingleObject (IN HANDLE hHandle, IN DWORD dwMilliseconds).
Параметр
hHandle
является дескриптором ожидаемого
события, а параметр dwMilliseconds
указывает в миллисекундах максимальное
время ожидания события. Если задать
dwMilliseconds
= 0, функция вернется немедленно. Если
задать dwMilliseconds
= INFINITE,
ожидание будет бесконечным (аналог
синхронной операции). Функция возвращает
значение WAIT_OBJECT_0,
если событие произошло, WAIT_TIMEOUT
– если истекло заданное время ожидания,
WAIT_FAILED
– в случае ошибки.
Настоящая
асинхронность опять же достигается при
dwMilliseconds
= 0. Пример функции обработчика по
наступлению события от таймера будет
следующий:
Void
__fastcall TForm1::Timer1Timer (TObject *Sender)
{ int
n;
if(WaitForSingleObject(Overlapp.hEvent,0)==
WAIT_OBJECT_0)
{
if(GetOverlappedResult(port,&Overlapp,&DWORD(n),
false))
{
Label1->Caption
= “Получены/переданы новые данные = “
+IntToStr(n)+”
байт”;
Timer1->Enabled
= false;
CloseHandle(Overlapp.hEvent);
}
}
else
Label1->Caption = “Новых
данных
нет”.
}
Следует обратить
внимание на необходимость закрыть
дескриптор объекта события функцией
CloseHandle
после того, как событие произошло.
Приведенный пример годится для
использования со всеми версиями Windows.
-
Модем
Одна из интересных возможностей VBA — поддержка функций, которые хранятся в динамически подключаемых библиотеках (Dynamic Link Libraries – DLL). В заметке демонстрируются популярные функции Windows API.[1] Для простоты изложения представленные здесь объявления API-функций могут корректно выполняться только в среде Excel 2010 и более поздних версиях. В то же время, примеры файлов совместимы с предыдущими версиями Excel.
Рис. 1. Выбор файла для поиска приложения$ чтобы увеличить изображение кликните на нем правой кнопкой мыши и выберите Открыть картинку в новой вкладке
Скачать заметку в формате Word или pdf, примеры в архиве (политика безопасности провайдера не позволяет загружать файлы Excel с поддержкой макросов)
Определение связей с файлами
В Windows многие типы файлов ассоциируются с конкретным приложением. Эта связь позволяет загрузить файл в соответствующее приложение (для этого дважды щелкните мышью на файле). Функция GetExecutable вызывает функцию Windows API с целью получить полный путь к приложению, связанному с указанным файлом. Например, в системе находится ряд файлов с расширением .txt; вероятно, один такой файл с названием Readme.txt в данный момент расположен в папке Windows. Функцию GetExecutable можно применять для определения полного пути приложения (которое запускается после двойного щелчка на выбранном файле).
Private Declare Function FindExecutableA Lib «shell32.dll» _ (ByVal lpFile As String, ByVal lpDirectory As String, _ ByVal lpResult As String) As Long Function GetExecutable(strFile As String) As String Dim strPath As String Dim intLen As Integer strPath = Space(255) intLen = FindExecutableA(strFile, «\«, strPath) GetExecutable = Trim(strPath) End Function |
Откройте файл file association.xlsm, кликните на кнопке «Определение связей между файлами», в открывшемся окне выберите файл (см. рис. 1), кликните Открыть. Функция GetExecutable вернет полный путь к приложению, которое связано с выбранным файлом (рис. 2).
Рис. 2. Определение пути и имени для приложения, связанного с заданным файлом
Определение буквы диска
В VBA нет способа получения информации о дисковых накопителях. Но эта проблема легко решается с помощью трех API-функций, обеспечивающих получение всех необходимых сведений. Откройте файл drive information.xlsm, кликните на копку. Процедуры VBA идентифицирует все подключенные дисковые накопители, определяет их тип, а также указывает размер свободного и занятого пространства на диске (рис. 3). Код используемых в примере функций Windows API можно найти в модуле Excel-файла.
Рис. 3. С помощью функций Windows API можно получить всю информацию о дисках, установленных в системе
Определение параметров принтера по умолчанию
Функция Windows API может быть использована для получения информации об активном принтере. Данная информация содержится в одной текстовой строке. Программа анализирует эту строку и отображает информацию в более удобном для чтения формате. Откройте файл printer info.xlsm, нажмите на кнопку. Свойство ActivePrinter объекта Application возвращает название активного принтера (и позволяет его изменить). Но не существует способа определить используемый драйвер принтера или порт. Поэтому функция DefaultPrinterInfo() столь полезна. После выполнения процедуры на экран выводится окно сообщения (рис. 4).
Рис. 4. Получение информации об активном принтере с помощью функции Windows API
Определение текущего видеорежима
Если в приложении необходимо отобразить определенный объем информации на одном экране, то, зная размер экрана, можно правильно задать масштаб текста. Кроме того, в коде определяется количество мониторов в системе. Если установлено более одного монитора, процедура определяет размер виртуального экрана. Откройте файл video mode.xlsm, кликните на кнопке, и процедура вернет видеорежим (рис. 5).
Рис. 5. Использование функций Windows API для определения видеорежима монитора
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
Option Explicit #If VBA7 And Win64 Then Declare PtrSafe Function GetSystemMetrics Lib «user32» _ (ByVal nIndex As Long) As Long #Else Declare Function GetSystemMetrics Lib «user32» _ (ByVal nIndex As Long) As Long #End If Public Const SM_CMONITORS = 80 Public Const SM_CXSCREEN = 0 Public Const SM_CYSCREEN = 1 Public Const SM_CXVIRTUALSCREEN = 78 Public Const SM_CYVIRTUALSCREEN = 79 Sub DisplayVideoInfo() Dim numMonitors As Long Dim vidWidth As Long, vidHeight As Long Dim virtWidth As Long, virtHeight As Long Dim Msg As String numMonitors = GetSystemMetrics(SM_CMONITORS) vidWidth = GetSystemMetrics(SM_CXSCREEN) vidHeight = GetSystemMetrics(SM_CYSCREEN) virtWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN) virtHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN) If numMonitors > 1 Then Msg = numMonitors & » мониторов» & vbCrLf Msg = Msg & «Виртуальный экран: « & virtWidth & » X « Msg = Msg & virtHeight & vbCrLf & vbCrLf Msg = Msg & «Видеорежим основного монитора: « Msg = Msg & vidWidth & » X « & vidHeight Else Msg = Msg & «Видеорежим монитора: « Msg = Msg & vidWidth & » X « & vidHeight End If MsgBox Msg End Sub |
Добавление звука в приложение
Можно расширить возможности Excel по воспроизведению звука в форматах WAV и MIDI. Откройте файл sound.xlsm, находящийся в папке sound, и измените значение в одной из ячеек диапазона В4:В12. Как только сумма в ячейке В13 достигнет значения 1000, прозвучит сигнал (рис. 6). Функция Alarm предназначена для применения в формуле рабочего листа. Она использует функцию Windows API для проигрывания звука, если ячейка соответствует определенному условию.
Рис. 6. Если сумма в ячейке В13 достигнет 1000, прозвучит сигнал
Function ALARM(Cell, Condition) Dim WAVFile As String Const SND_ASYNC = &H1 Const SND_FILENAME = &H20000 If Evaluate(Cell.Value & Condition) Then WAVFile = ThisWorkbook.Path & «\sound.wav» Call PlaySound(WAVFile, 0&, SND_ASYNC Or SND_FILENAME) ALARM = True Else ALARM = False End If End Function |
Функция Alarm имеет два аргумента: ссылку на ячейку и условие (выраженное в виде строки). Формула =ALARM (В13, "
>=1000"
) использует функцию Alarm для проигрывания WAV-файла, если значение в ячейке В13 больше или равно 1000. Функция использует функцию VBA Evaluate для определения, соответствует ли значение ячейки заданному критерию. Если условие выполнено (и звук воспроизведен), функция возвращает ИСТИНА, в противном случае она возвращает значение ЛОЖЬ.
Чтение и запись параметров системного реестра
Многие приложения Windows используют системный реестр для хранения параметров. Процедуры VBA могут считывать значения из реестра и записывать в него новые значения. Функции VBA GetRegistry и WriteRegistry – две функции-«оболочки», упрощающие управление реестром. Откройте файл windows registry.xlsm и изучите код VBA.
Функция GetRegistry возвращает раздел из указанного места регистра. У нее три аргумента:
- Root Key. Строка, представляющая ветвь реестра, к которой обращается функция. Данная строка может принимать одно из следующих значений: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_CONFIG, HKEY_DYN_DATA.
- Path. Полный путь к разделу реестра, к которому обращается функция.
- RegEntry. Название параметра, который должна получить функция.
Например, если необходимо найти графический файл, используемый в качестве обоев рабочего стола, используйте функцию GetRegistry в следующей процедуре (обратите внимание, что аргументы не чувствительны к регистру).
Sub Wallpaper() Dim RootKey As String Dim Path As String Dim RegEntry As String RootKey = «hkey_current_user» Path = «Control Panel\Desktop» RegEntry = «Wallpaper» MsgBox GetRegistry(RootKey, Path, RegEntry), vbInformation, _ Path & «\RegEntry» End Sub |
Напоминаю, чтобы вызвать процедуру пройдите по меню Вид –> Макросы –> Макросы, выделите процедуру Wallpaper, и кликните Выполнить. После выполнения этой процедуры в окне сообщения отображаются путь и имя графического файла (либо пустая строка, если обои не используются).
Функция WriteRegistry записывает значение в указанный раздел реестра. Если операция завершается успешно, функция возвращает ИСТИНА; в противном случае функция возвращает ЛОЖЬ. Первые три аргумента функция WriteRegistry такие же, как и аргументы GetRegistry, но также есть и четвертый аргумент – RegVal – значение, которое записывается в реестр. Например, процедура Workbook_Open() записывает текущую дату и время в разделе настроек Excel.
Sub Workbook_Open() RootKey = «hkey_current_user» Path = «software\microsoft\office\14.0\excel\LastStarted» RegEntry = «DateTime» RegVal = Now() If WriteRegistry(RootKey, Path, RegEntry, RegVal) Then Msg = RegVal & » сохранено в реестре.» Else Msg = «произошла ошибка» End If MsgBox Msg End Sub |
Если вы решили воспользоваться системным реестром для хранения и выборки настроек приложений Excel, проще обратиться к функциям VBA GetSetting и SaveSetting. Важно понимать, что они работают только со следующим разделом реестра:
HKEY_CURRENT_USER\Software\VB and VBA Program Settings
Другими словами, с помощью этих функций можно управлять данными только одной ветви реестра, в которой сохраняются базовые настройки Excel.
[1] По материалам книги Джон Уокенбах. Excel 2010. Профессиональное программирование на VBA. – М: Диалектика, 2013. – С. 376–383.
Данный текст немного сгенерирован chat-gpt 3.5
Но большая часть текста — моя
Если вы читаете эту статью — значит вы уже подучили с++ и интересуетесь разработкой под Windows
Следующая часть будет уже сложнее.
WinAPI (Windows Application Programming Interface) является набором функций и процедур, предоставляемых операционной системой Windows для разработки приложений. В этой статье мы рассмотрим основные принципы работы с WinAPI.
WinAPI обеспечивает доступ к функциям операционной системы Windows, таких как создание окон, управление ресурсами, работа с файлами, реагирование на события и еще многое другое. Чтобы начать разработку приложений с использованием WinAPI, необходимо знать несколько основных понятий и функций.
Основным элементом приложения, создаваемого с помощью WinAPI, является окно(не всегда). Для создания окна необходимо определить его характеристики, такие как размер, положение на экране и обработчик событий. Обработчик событий позволяет приложению реагировать на действия пользователя, например, нажатие кнопки мыши или нажатие клавиши.
После создания окна приложение переходит в цикл обработки сообщений, который прослушивает и обрабатывает события которые происходящие в окне. В цикле обработки сообщений мы можем реализовать логику нашего приложения.
WinAPI также предоставляет множество других функций и возможностей, таких как работа с ресурсами (иконки, курсоры, меню), рисование на экране, использование диалоговых окон и многое другое.
В этой статье мы рассмотрели лишь базовые принципы работы с WinAPI. Для изучения более сложных возможностей WinAPI рекомендуется обратиться к официальной документации и руководствам на других сайтах по программированию на WinAPI.
В заключение, WinAPI является мощным инструментом для разработки Windows-приложений. Он предоставляет разработчикам доступ к функциям операционной системы Windows и позволяет создавать приложения для операционных систем Windows в то время как UWP приложения может только на Windows 10+ запускаться.
И кстати: в рамках WinAPI вы также можете создавать приложения с использованием Direct3D , этот графический API разработанным для работы с трехмерной графикой. Direct3D позволяет разработчикам создавать и управлять 3D-графикой в WinApi-приложениях.
Автор: makwd