- Вызов функций Windows API
- Вызов функций Windows API, имеющих выходной строковый параметр char*
- Изменение типа, применяемого для маршалинга по умолчанию
- Вызов функций, требующих struct
- Работа с функциями обратного вызова в C#
- Создание собственной управляемой библиотеки
- Примеры использования функций API
- Заключение
- Первая программа на WinAPI
- Типы данных в WinAPI
- Соглашения по вызову (Calling Conventions) — __stdcall
- Описатели/дескрипторы (Handles) в WinAPI
- WinAPI окна
- Функция WinMain
- Классы окон (Window Classes)
- Первая WinAPI программа — Пустое окно
- Главный цикл WinAPI
- Очередь сообщений (Message Queue) в WinAPI
- Оконная процедура (Window Procedure) WindowProc
- Заключение
Вызов функций Windows API
Из книги 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:
Иногда в примерах вы можете также встретить такой способ (длинный и неудобный): [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, который позволяет изменять строки.
Тип, используемый для маршашлинга StringBuilder по умолчанию, — тоже LPTSTR, зато теперь GetWindowText может модифицировать саму вашу строку:
Таким образом, ответом на вопрос, как вызывать функцию, у которой есть выходной строковый параметр, будет — используйте класс StringBuilder.
Изменение типа, применяемого для маршалинга по умолчанию
Например, мы хотим вызвать функцию GetClassName, который принимает параметр LPSTR (char*) даже в Unicode-версиях. Если вы передадите строку, общеязыковая исполняющая среда (CLR) преобразует ее в серию TCHAR. Но с помощью атрибута MarshalAs можно переопределить то, что предлагается по умолчанию:
Теперь, когда вы вызовете GetClassName, .NET передаст вашу строку в виде символов ANSI, а не «широких символов».
Вызов функций, требующих struct
Возьмем для примера функцию GetWindowRect, которая записывает в структуру RECT экранные координаты окна. Чтобы вызвать функцию GetWindowRect и передать ей структуру RECT нужно использовать тип struct в сочетании с атрибутом StructLayout:
Важно использовать ref, чтобы CLR передала параметр типа RECT как ссылку. В этом случае функция сможет модифицировать ваш объект, а не его безымянную копию в стеке. После такого объявления функции можно ее вызвать в коде:
Обратите внимание, что ref используется и в объявлении, и при вызове функции. Тип, по умолчанию применяемый для маршалинга типов struct — по умолчанию LPStruct, поэтому необходимости в атрибуте MarshalAs нет. Но если вы хотите использовать RECT в виде класса, а не struct, вам необходимо реализовать оболочку:
Работа с функциями обратного вызова в C#
Для использования функций, написанных на C#, в качестве функций обратного вызова Windows, нужно использовать делегаты (delegate).
Объявив свой тип делегата, можно написать оболочку для функции Windows API:
Так как в строке с delegate просто объявляется тип делегата (delegate type), сам делегат нужно предоставить в классе:
а затем передать оболочке:
Проницательные читатели заметят, что я умолчал о проблеме с lparam.
В языке C, если в EnumWindows дается LPARAM, Windows будет уведомлять вашу функцию обратного вызова этим LPARAM. Обычно lparam — указатель на некую структуру или класс, который содержит контекстную информацию, нужную вам для выполнения своих операций. Но запомните: в .NET слово «указатель» произносить нельзя! Так что же делать? Можно объявить ваш lparam как IntPtr и использовать GCHandle в качестве его оболочки:
Не забудьте вызвать Free, когда закончите свои дела! Иногда в C# приходится самому освобождать память. Чтобы получить доступ к «указателю» lparam внутри перечислителя, используйте GCHandle.Target.
Ниже показан написанный мной класс, который инкапсулирует EnumWindows в массив. Вместо того чтобы возиться со всеми этими делегатами и обратными вызовами, вы пишете:
Небольшая программа ListWin (Приложение ListWin.cs), которую я написал для перечисления окон верхнего уровня, позволяет просматривать списки HWND, имен классов, заголовков и/или прямоугольников окон, используя RECT или Rectangle. Исходный код ListWin показан не полностью; весь исходный код можно скачать по ссылке, приведенной в конце статьи.
Создание собственной управляемой библиотеки
Можно создать собственную управляемую библиотеку, из которой можно будет вызывать функции Windows API. Для этого в Visual Studio предусмотрены специальные опции. Новый проект создается как библиотека классов (Class Library). Сборка при этом автоматически получает расширение dll. Использовать управляемую библиотеку в управляемом коде просто. Для этого надо добавить ссылку (используя меню Project | Add Reference…) на библиотечную сборку, указав месторасположение сборки в соответствующем диалоговом окне. После этого Visual Studio копирует сборку в директорию, в которой располагается разрабатываемый код. Далее в коде программы используется либо оператор using, либо полное имя библиотечного модуля с точечной нотацией. Все библиотечные классы и методы готовы к использованию в коде программы.
Также можно воспользоваться командной строкой. Чтобы скомпилировать класс Win32API.cs, введите:
В результате у вас будет создан файл Win32API.dll, доступный для любого проекта на C#.
Пример создания библиотеки и использования функций 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. Если вам понравится этот справочник, то вы можете приобрести его полную версию на моем сайте.
Первая программа на WinAPI
WinAPI или Windows API (Application Programming Interface) — это библиотека для создания классических приложений Windows. Сама библиотека WinAPI написана на языке C и представляет собой коллекцию функций, структур и констант. Она объявлена в определённых заголовочных файлах и реализована в статических (.lib) и динамических (.dll) библиотеках.
В данном уроке мы создадим нашу первую программу на WinAPI. Для начала создайте проект. Выберите меню File -> New -> Project:
В открытом окне в левой панели выберите Other, затем Empty Project (пустой проект). Там же доступен шаблон Windows Desktop Application, но мы напишем программу с нуля, так как шаблон по умолчанию пока слишком сложен для нас. В нижней части выберите имя проекта, его местоположение и хотите ли вы создать решение для него. Местоположение может быть любым, для меня это C:\prog\cpp\
В обозревателе решений щёлкните правой кнопкой мышки и выберите Add -> New Item.
В открывшемся окне выберите C++ File (.cpp) и введите имя файла в нижней части — main.cpp.
Перед тем как мы начнём рассматривать код, давайте поговорим о типах данных в WinAPI и соглашениях вызова (calling conventions).
Типы данных в WinAPI
WinAPI переопределяет множество стандартных типов языка C. Некоторые переопределения зависят от платформы для которой создаётся программа. Например, тип LRESULT, если его скомпилировать для x86, будет типом long. Но если скомпилировать программу для x64, то LRESULT будет типом __int64. Вот так LRESULT определяется на самом деле (он зависит от LONG_PTR, а LONG_PTR может уже быть или __int64, или long):
Соглашения по вызову (Calling Conventions) — __stdcall
В коде ниже перед именами функций вы встретите __stdcall. Это одно из соглашений по вызову функций. Соглашение по вызову функций определяет каким образом аргументы будут добавляться в стек. Для __stdcall аргументы помещаются в стек в обратном порядке — справа налево. Также, __stdcall говорит, что после того как функция завершится, она сама (а не вызывающая функция) удалит свои аргументы из стека. Все функции WinAPI используют __stdcall соглашение.
WinAPI переопределяет __stdcall в WINAPI, CALLBACK или APIENTRY, которые используются в разных ситуациях. Поэтому в примерах из MSDN вы не увидите __stdcall, но нужно помнить что именно оно будет использоваться.
Типы WinAPI пишутся в верхнем регистре.
Описатели/дескрипторы (Handles) в WinAPI
Handle на русский язык сложно перевести однозначно. Наверное, наиболее частое употребление в русском имеет слово дескриптор. По сути это ссылка на ресурс в памяти. Например, вы создаёте окно. Это окно хранится в памяти и оно имеет запись в таблице, которая хранит указатели на все созданные системные ресурсы: окна, шрифты, файлы, картинки. Указатель на ваше окно в данной таблице называется дескриптором окна (handle of the window).
Любой указатель это просто переопределение типа void*. Примеры дескрипторных типов в WinAPI: HWND, HINSTANCE, HBITMAP, HCURSOR, HFILE, HMENU.
Подытожим: дескрипторы используются для получения доступа к каким-либо системным ресурсам.
WinAPI окна
Давайте посмотрим на код самой простой WinAPI программы:
Вначале нужно добавить WinAPI: статичную библиотеку, которая содержит определения различных функций и включить заголовочный файл с объявлениями этих функций, структур и констант. user32.lib содержит основные возможности Windows — всё, что касается окон и обработки событий.
На следующей строке мы объявляем функцию обратного вызова (callback), которая будет вызываться, когда наше приложение получает какое-либо сообщение от операционной системы. Мы вернёмся к этому ниже.
Функция WinMain
WinMain — точка входа в программу, а как мы помним такая функция вызывается операционной системой.
Главная функция приложений под Windows отличается от консольной версии. Она возвращает целое число и это всегда ноль. __sdtcall говорит, что аргументы добавляются в стек в обратном порядке и WinMain сама удаляет их из стека по завершении. WinMain принимает 4 аргумента:
hInstance — дескриптор экземпляра приложения. Можете думать о нём, как о представлении вашего приложения в памяти. Он используется для создания окон.
Второй аргумент — наследие шестнадцатибитных версий Windows. Уже давно не используется.
Третий аргумент представляет аргументы командной строки. Пока мы не будем им пользоваться.
nCmdShow — специальный флаг, который можно использовать при создании окон. Он говорит о состоянии окна: должно ли оно показываться нормально, на полный экран или быть свёрнутым.
Теперь давайте посмотрим как создаются окна.
Классы окон (Window Classes)
Для создания окна нужно определить и зарегистрировать его класс. Windows создаёт свои классы таким же образом. Все стандартные элементы, которые вы видите в Windows являются классами: кнопки, поля редактирования, полосы прокрутки. Windows хранит список всех зарегистрированных классов. Обязательно нужно заполнить только три поля класса: имя класса, дескриптор экземпляра приложения (передаётся в WinAPI в виде параметра) и оконная процедура (адрес функции).
Сначала нужно заполнить структуру WNDCLASS. Пусть вас не смущает название WNDCLASS — это не C++ класс. В данном случае, класс — всего лишь термин используемый в WinAPI:
Здесь мы инициализируем структуру WNDCLASS нулями, определяем обязательные поля и регистрируем класс.
lpfnWndProc имеет тип WNDPROC. Как говорилось выше, это указатель на функцию WindowProc, которую мы объявили в самом начале. У каждого оконного класса должна быть своя оконная процедура.
hInstance — дескриптор экземпляра приложения. Все оконные классы должны сообщать, какое приложение их зарегистрировало. Мы используем первый параметр функции WinMain.
lpszClassName — имя класса, задаётся пользователем. В Windows все классы называются в верхнем регистре (примеры: BUTTON, EDIT, LISTBOX), мы будем делать также в наших уроках.
WNDCLASS содержит больше полей: стиль, иконка, имя меню, но мы можем пропустить их. Некоторые из них мы рассмотрим в следующих уроках. Вы можете посмотреть полный список в документации к WinAPI на MSDN (официальном сайте Microsoft с документацией).
В конце мы регистрируем наш класс с помощью функции RegisterClass. Мы передаём адрес структуры WNDCLASS. Теперь мы можем создать окно.
Первая WinAPI программа — Пустое окно
В WinAPI есть функция для создания окон — CreateWindow:
Первый параметр — имя класса. В данном случае он совпадает с именем класса, который мы зарегистрировали. Второй — имя окна, это та строка, которую пользователи программы будут видеть в заголовке. Следующий — стиль. WS_OVERLAPPEDWINDOW говорит, что WinAPI окно имеет заголовок (caption), кнопки сворачивания и разворачивания, системное меню и рамку.
Четыре числа определяют позицию левого верхнего угла окна и ширину/высоту.
Затем идут два указателя nullptr. Первый — дескриптор родительского окна, второй — меню. У нашего окна нет ни того, ни другого.
hInstance — дескриптор на экземпляр приложения, с которым связано окно.
В последний аргумент мы передаём nullptr. Он используется для специальных случаев — MDI (Multiple Document Interface ) — окно в окне.
CreateWindow возвращает дескриптор окна. Мы можем использовать его для обращения к окну в коде. Теперь мы можем показать и обновить окно.:
ShowWindow (показать окно) использует параметр nCmdShow функции WinMain для контроля начального состояния (развёрнуто на весь экран, минимизировано, обычный размер). UpdateWindow (обновить окно) мы обсудим в следующих уроках.
Главный цикл WinAPI
Далее идёт бесконечный цикл. В этом цикле мы будем реагировать на разные события, возникающие при взаимодействии пользователя с нашей программой.
Перед циклом мы объявляем переменную типа MSG (от message — сообщение) — в этой структуре Windows кодирует события. Если произошло событие WM_QUIT, то мы завершаем цикл.
Операционная система генерирует множество различных сообщений. Они генерируются, когда происходит какое-то событие: изменился размер окна, нажата клавиша мышки или клавиатуры.
Очередь сообщений (Message Queue) в WinAPI
Все оконные приложения управляются событиями (event-driven). В операционной системе существует очередь сообщений. Когда происходит какое-либо событие в любой программе, в эту очередь посылается сообщение. Также, любая программа имеет свою очередь сообщений. Windows проверяет каждое сообщение в системной очереди и посылает их в программные очереди. В действительности всё немного сложнее, так как очереди сообщений связаны с потоками, но мы обсудим это позже. На данный момент запомните, что каждое приложение имеет свою собственную очередь сообщений.
Сообщение это просто структурная переменная MSG. Давайте посмотрим на определение этой структуры:
hwnd — дескриптор окна, которому принадлежит сообщение.
message — идентификатор сообщения (UINT — unsigned integer).
wParam и lParam содержат дополнительную информацию и зависят от идентификатора сообщения.
time — понятно из названия — время, когда было создано сообщение.
pt — позиция курсора на экране в момент генерации сообщения. POINT — тип WinAPI для описания точек с координатами (x,y).
Приложение в бесконечном цикле проверяет свою очередь сообщений, смотрит на свойство message и решает, что делать с данным сообщением (как его обработать).
Функция PeekMessage проверяет очередь и берёт последнее сообщение. Затем, она берёт информацию о сообщении и помещает её в переменную msg. Последний аргумент заставляет удалить сообщение из очереди.Итак, в условии мы проверяем, содержит ли очередь сообщений приложения какое-либо сообщение. Если содержит, мы заполняем переменную msg и удаляем сообщение из очереди. Затем вызываем две функции.
TranslateMessage — генерирует дополнительное сообщение если произошёл ввод с клавиатуры (клавиша с символом была нажата или отпущена). По умолчанию, клавиатура генерирует так называемые сообщения виртуального ключа (virtual key). TranslateMessage генерирует ещё одно сообщение, которое сообщает информацию о символе. Мы поговорим об этом позже. Пока что: вызов TranslationMessage нужен, чтобы обработать ввод символов с клавиатуры.
Функция DispatchMessage посылает сообщение в функцию WindowProc.
Оконная процедура (Window Procedure) WindowProc
Оконная процедура — это специальная функция в которой мы обрабатываем сообщения. В данный момент нам нужно обработать только одно важное сообщение, а для всех остальных выполнить стандартное действие. Давайте посмотрим на наш вариант WindowProc:
Обратите внимание, что мы сами нигде не вызываем WindowProc. Оконная процедура привязана к классу окна. И когда мы вызываем DispatchMessage, система сама вызывает оконную процедуру, связанную с окном. Такие функции называются функциями обратного вызова (callback functions) — они не вызываются напрямую. Также обратите внимание, что данная функция получает только часть свойств MSG структуры.
Внутри WindowProc мы проверяем поле message структуры MSG. Оно содержит идентификатор сообщения. В Windows определено много констант для разных сообщений. В данной программе мы проверяем WM_DESTROY. Это сообщение посылается, когда окно уничтожается, в момент, когда оно уже удалено с экрана. В ответ на это сообщение мы вызываем PostQuitMessage — она говорит системе, что приложение будет закрыто и посылает сообщение WM_QUIT в очередь сообщений программы (не в системную).
Если пришло сообщение, которое мы не хотим обрабатывать сами, мы передаём его в DefWindowProc — действие по умолчанию.
Заключение
В данном уроке мы создали пустое но полностью функциональное стандартное окно операционной системы Windows. В следующих уроках мы обсудим разные части WinAPI, а шаблон из данного урока станет основой для наших программ на DirectX и OpenGL.