Текст
                    Программирование
в Win32 API на Visual Basic
Стивен Роман


Win32 API Programming with Visual Basic Steven Roman
Программирование в Win32 API на Visual Basic Стивен Роман Москва Серия «Для программистов»
УДК 004.451.84 ББК 32.973.26-018.1 Р69 Роман С. Р69 Программирование в Win32 API на Visual Basic: Пер. с англ. – М.: ДМК Пресс. – 480 с.: ил. (Серия «Для программистов»). ISBN 5-94074-102-9 Книга излагает основные сведения о системном программировании на Visual Basic и дает необходимую информацию о назначении функций Win32 API. Среда VB наиболее эффективна для быстрой разработки приложений , однако за простоту в создании программ приходится платить снижением эффективности, потерей гиб­ кости и управляемости. Здесь описывается, как можно обойти требования Visual Basic, обращаясь непосредственно к интерфейсу прикладного программирования Win32. Обсуждается широкий круг практических задач от самых простых, таких как получение основной системной информации, добавление позиций табуляции в окне со списком, запись и извлечение данных в/из реестра или индивидуальных файлов инициализации, до весьма сложных – модификации класса управляющих элементов с целью реализации заданного поведения, установки ловушек для отслеживания и изменения работы мыши или клавиатуры и т.д. В книге затрагиваются вопросы архитектуры 32­разрядной Windows, распределе­ ния адресного пространства, синхронизации различных потоков, межпроцессорного взаимодействия, внедрения DLL во внешние процессы. Кратко освещается формат исполняемых файлов Windows, рассматривается концепция контекстов устройств. Данное издание адресовано профессиональным программистам на Visual Basic, заинтересованным в том, чтобы включить возможности системных сервисов Windows в свои приложения. ББК 32.973.26­018.1 © DMK Press. Authorized translation of the English edition © Steven Roman. This translation is published and sold by permission of O’Reilly & Associates, Inc., the owner of all rights to publish and sell the same. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но, поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответс­ твенности за возможные ошибки, связанные с использованием книги. ISBN 1­56592­631­5 (англ.) Copyright © Steven Roman. All rights reserved. ISBN 5­94074­102­9 (рус.) © Перевод на русский язык, оформление ДМК Пресс
Содержание Предисловие ...................................................................................... 18 Часть I. Объявление APIфункций в Visual Basic .......... 23 Глава 1. Введение ............................................................................. 24 Что такое Win32 API ........................................................................... 25 APIфункции для работы со значками ............................................... 26 Другие API........................................................................................ 26 Windows 9x и Windows NT – два разных Win32 API .............................. 27 Проблемы программирования Win32 API в среде Visual Basic ...... 28 Аккуратность прежде всего .............................................................. 29 Будьте внимательны ......................................................................... 30 Глава 2. Начальные сведения ...................................................... 32 Символьные коды ............................................................................. 32 ASCII ................................................................................................ 32 ANSI ................................................................................................. 32 DBCS ............................................................................................... 33 Unicode ............................................................................................ 33 Поддержка Unicode в Windows .......................................................... 34 Параметры и аргументы ................................................................... 34 Параметры IN и OUT ......................................................................... 34 ByVal и ByRef .................................................................................... 34 Динамически подключаемые библиотеки ...................................... 34 Таблицы экспорта ............................................................................ 35 Роль DLL – динамическое связывание .............................................. 37 Несколько слов о синтаксисе C++ ................................................... 37 Основы ............................................................................................ 38 Объявление переменных.................................................................. 38 Объявление массивов ...................................................................... 38
 Программирование в Win32 API на Visual Basic Объявление функций ....................................................................... 39 Указатели ........................................................................................... 39 Обходными путями к цели ................................................................ 41 Указатели в Visual Basic ................................................................... 41 Глава 3. Объявление APIфункций ............................................. 43 Оператор VB Declare ......................................................................... 43 Public или private .............................................................................. 44 Имя для вызова ................................................................................ 44 Экспортируемая библиотека............................................................ 44 Альтернативные имена..................................................................... 44 Список параметров .......................................................................... 46 Перевод VC в VB ................................................................................ 47 Передача параметров внешней функции ....................................... 47 Пример функции .............................................................................. 47 Передача параметра как Any ............................................................ 48 Снова ByVal ...................................................................................... 50 Мечта VBхакера – CopyMemory ...................................................... 50 Простой пример ............................................................................... 51 Более содержательный пример ....................................................... 52 Реализация оператора раскрытия ссылки в Visual Basic ................... 53 Обсуждение ошибок API ................................................................... 55 Когда программа завершается аварийно ......................................... 55 Сообщения Win32 об ошибках ......................................................... 56 Глава 4. Типы данных ....................................................................... 59 Что такое тип данных ........................................................................ 59 Примеры типов данных .................................................................... 59 Основные и производные типы данных .......................................... 61 Типы данных в Visual Basic ................................................................ 61 Данные типа Variant.......................................................................... 62 Основные типы данных в VC++ ........................................................ 65 Оператор typedef ............................................................................. 65 Символьные типы данных................................................................. 66 Типы данных int ................................................................................ 67 Типы данных с плавающей точкой .................................................... 68 Другие типы данных ......................................................................... 68
 Содержание Резюме ............................................................................................ 69 Преобразование производных типов данных ................................ 70 Операторы typedef в Win32 ............................................................... 71 Утилита rpiAPIData ............................................................................ 71 Зачем такое множество typedef........................................................ 73 Пример преобразования функции SendMessage .......................... 74 Структуры и пользовательские типы ............................................... 75 Флаги .................................................................................................. 78 Побитовое маскирование................................................................. 78 Символьные константы .................................................................... 80 Глава 5. Знаковые и беззнаковые типы данных .................... 81 Знаковое и беззнаковое представления ........................................ 81 Зачем нужны два разных представления ....................................... 82 Беззнаковое представление ............................................................ 83 Знаковое представление ................................................................. 84 Представление в прямом коде со знаком ......................................... 84 Представление в дополнительном двоичном коде ........................... 84 Почему это называется двоичным дополнением .............................. 86 Преобразование между знаковыми и беззнаковыми представлениями .............................................................................. 86 Числа типа integer ............................................................................ 88 Числа типа long ................................................................................ 89 Байты ............................................................................................... 90 Примеры .......................................................................................... 91 Преобразование длины слов ........................................................... 93 Глава 6. Строки .................................................................................. 95 Тип BSTR ............................................................................................ 96 Строки в стиле C: LPSTR и LPWSTR ................................................. 98 Терминология, связанная со строками ........................................... 99 Средства для исследования строк ................................................ 100 Функция StrConv в Visual Basic........................................................ 100 Функции Len и LenB ........................................................................ 101 Функции Chr, ChrB и ChrW............................................................... 101 Функции Asc, AscB и AscW .............................................................. 102
 Программирование в Win32 API на Visual Basic Пустые строки и пустые символы ................................................... 102 Функции VarPtr и StrPtr ................................................................... 103 Преобразование строк в VB ........................................................... 105 Подготовка BSTR ........................................................................... 105 Возвращаемая BSTR ...................................................................... 107 Вызываемые функции .................................................................... 108 Полный цикл строки ....................................................................... 108 Пример использования точки входа Unicode .................................. 109 Передача строк в Win32 API ............................................................ 111 ByVal в сравнении с ByRef .............................................................. 111 Строковые параметры IN и OUT...................................................... 111 Обсуждение входных параметров .................................................. 112 Обсуждение выходных параметров ................................................ 113 Пример работы параметра IN/OUT ................................................. 117 Что случилось с указателем ........................................................... 119 Строки и массивы байтов ............................................................... 123 Преобразование между массивами байтов и строками BSTR......... 123 Преобразование между строками BSTR и LPTSTR .......................... 124 Пример использования массивов байтов ....................................... 127 Пример изменения меню Windows.................................................. 128 Получение адреса переменной пользовательского типа ........... 132 Глава 7. Функции получения системной информации ..................................................................................... 134 Имя компьютера .............................................................................. 134 Пути к системным каталогам Windows .......................................... 135 Версия операционной системы ..................................................... 137 dwOSVersionInfoSize ....................................................................... 137 dwMajorVersion ............................................................................... 137 dwMinorVersion ............................................................................... 137 dwBuildNumber ............................................................................... 137 dwPlatformId ................................................................................... 138 szCSDVersion.................................................................................. 138 Системные метрики ...................................................................... 139 Системные параметры ................................................................... 140 Характеристики системных иконок................................................. 141 Системные цвета ............................................................................ 143
 Содержание Глава 8. Обработка исключений ............................................... 146 Отслеживание GPF .......................................................................... 146 Замена обработчика исключения по умолчанию ......................... 147 Обработчик исключения, заменяющий устанавливаемый по умолчанию .................................................................................. 147 Пример программы обработчика исключения ............................. 151 Часть II. Операционная система Windows .................... 153 Глава 9. Архитектура Windows .................................................... 154 Процессы и потоки .......................................................................... 154 Архитектура Windows ...................................................................... 155 Режим ядра и пользовательский режим ......................................... 156 Сервисы ......................................................................................... 157 Системные процессы ..................................................................... 157 Подсистема Win32.......................................................................... 159 Исполнительная система Windows.................................................. 160 Уровень абстрагирования от аппаратуры (HAL) .............................. 161 Отличие Windows 9x и Windows NT ................................................. 161 Глава 10. Объекты и их дескрипторы ...................................... 162 Дескрипторы ................................................................................... 162 Подсчет используемости ............................................................... 163 Совместное использование объектов несколькими процессами .... 163 Пример отображения файла .......................................................... 164 Когерентность................................................................................ 170 Глава 11. Процессы ........................................................................ 171 Дескрипторы и идентификаторы процессов ................................ 172 Дескрипторы модулей .................................................................... 173 Идентификация процесса .............................................................. 174 Получение дескриптора процесса по его идентификатору ............. 175 Имена файлов и дескрипторы модулей .......................................... 176 Получение идентификатора текущего процесса............................. 179
10 Программирование в Win32 API на Visual Basic Получение идентификатора процесса от окна ................................ 180 Получение имен и дескрипторов модулей ...................................... 181 Псевдодескрипторы процессов .................................................... 181 Перечисление процессов ............................................................... 183 Перечисление процессов в Windows NT.......................................... 183 Перечисление процессов в Windows 9x .......................................... 189 Как определить, выполняется ли приложение ............................. 191 Использование FindWindow ............................................................ 191 Применение подсчета используемости .......................................... 192 Библиотека rpiUsageCount ............................................................. 193 Список процессов .......................................................................... 196 Глава 12. Потоки .............................................................................. 204 Дескрипторы и идентификаторы потоков .................................... 204 Приоритет потоков .......................................................................... 204 Уровни приоритета потоков ........................................................... 205 Назначение приоритета потоку ..................................................... 206 Повышение приоритета потока и квант изменений приоритета .................................................................................... 207 Состояния потоков ......................................................................... 208 Синхронизация потоков ................................................................. 208 Критические секции ....................................................................... 208 Синхронизация с использованием объектов ядра .......................... 209 Ожидание завершения приложения ............................................... 211 Объекты «мьютекс» ........................................................................ 213 Изменение счетчиков с помощью мьютексов ................................. 214 События ......................................................................................... 215 Изменение счетчиков с помощью событий ..................................... 217 Семафоры...................................................................................... 218 Проблемы, связанные с состоянием ожидания ........................... 219 Глава 13. Архитектура памяти Windows .................................. 220 Типы памяти ..................................................................................... 220 Физическая память ........................................................................ 221 Виртуальная память ....................................................................... 221 Страничные блоки памяти .............................................................. 221
11 Содержание Память файла подкачки .................................................................. 221 Файлы, отображаемые в память..................................................... 222 Совместно используемая физическая память ................................ 223 Адресное пространство процесса ................................................ 224 Использование адресного пространства в Windows NT .................. 225 Использование адресного пространства в Windows 9x ................... 228 Пример использования функции GetSystemInfo .......................... 229 Распределение виртуальной памяти ............................................ 231 Защита памяти ............................................................................... 232 Гранулярность при распределении памяти .................................... 233 Дескриптор виртуальных адресов .................................................. 234 Пример использования функции GlobalMemoryStatus ................ 234 Управление виртуальной памятью ................................................ 235 Преобразование виртуальных адресов в физические: попадание ...................................................................................... 235 Преобразование виртуальных адресов в физические: промах ....... 237 Совместно используемые страницы .............................................. 238 Рабочие наборы ............................................................................. 238 База данных страничных блоков ..................................................... 239 Кучи памяти ..................................................................................... 240 Кучи в 32разрядной Windows ......................................................... 240 Функции работы с кучей ................................................................. 241 Пример отображения виртуальной памяти .................................. 241 Глава 14. PEфайлы ....................................................................... 248 Перемещение модуля ..................................................................... 248 Формат PEфайла ........................................................................... 250 Заголовок PEфайла ...................................................................... 251 Таблица разделов .......................................................................... 258 Разделы ......................................................................................... 259 Пример получения информации о PEфайле ............................... 262 Структуры ...................................................................................... 264 Получение информации о версии................................................... 266 Получение характеристик файла .................................................... 267 Получение имен экспорта .............................................................. 268 Получение имен импорта ............................................................... 269
12 Программирование в Win32 API на Visual Basic Часть III. Окна. Программирование User32.DLL ........ 271 Глава 15. Основы ............................................................................ 272 Терминология .................................................................................. 272 Стили окон ....................................................................................... 273 Стили, которые определяют общие характеристики окон............... 273 Стили оконных рамок ..................................................................... 274 Стили, которые влияют на неклиентскую область окна ................... 274 Стили, которые влияют на начальное состояние окна..................... 275 Стили родителей и потомков.......................................................... 275 Расширенные стили ....................................................................... 275 Подчиненные окна .......................................................................... 276 Упорядоченность по Zкоординате ............................................... 276 Функция BringWindowToTop ............................................................ 277 Функция SetWindowPos .................................................................. 277 Функция GetTopWindow .................................................................. 278 Перечисление окон ......................................................................... 278 Функции перечисления .................................................................. 279 Использование утилиты EnumWins ................................................. 280 Функции размера и положения ..................................................... 283 Функция SetWindowPlacement ........................................................ 283 Функция MoveWindow ..................................................................... 285 Функция SetWindowPos .................................................................. 286 Функции GetWindowRect и GetClientRect ......................................... 286 Функции ClientToScreen и ScreenToClient........................................ 287 Глава 16. Сообщения Windows ................................................... 289 Очереди сообщений потока ........................................................... 291 Система сообщений Windows ........................................................ 292 Доступ к очереди сообщений потока .............................................. 292 Циклы обработки сообщений ......................................................... 292 Более пристальный взгляд на GetMessage ..................................... 295 Синхронные и асинхронные сообщения ....................................... 296 Установка таймаута ...................................................................... 297 Уведомляющие сообщения ............................................................ 298
13 Содержание Пример отправки сообщений управляющему элементу Listbox .............................................................................. 298 Установка позиций табуляции ........................................................ 298 Установка горизонтальной протяженности..................................... 300 Извлечение данных управляющего элемента Listbox ...................... 301 Маршаллинг между процессами ................................................... 303 Копирование данных между процессами ..................................... 304 Состояние локального ввода ......................................................... 306 Приоритетный поток ...................................................................... 306 Ввод с клавиатуры ......................................................................... 307 Захват мыши .................................................................................. 307 Активное окно и окно переднего плана ........................................... 308 Эксперименты ............................................................................... 308 Глава 17. Классы окон и процесс создания окна ............... 312 Классы окон ..................................................................................... 312 Предопределенные классы окон ................................................... 313 Оконная процедура класса окна .................................................... 314 Создание окна ................................................................................. 315 Стили окон ....................................................................................... 316 Изменение стиля окна.................................................................... 317 Управляющие элементы Windows и VB ......................................... 318 Пример слежения за окнами .......................................................... 319 Глава 18. Модификация класса окна ....................................... 323 Модификация класса окна ............................................................. 323 Надстройка класса ......................................................................... 324 Пример модификации класса VB Checkbox ................................. 324 Глава 19. Ловушки Windows ........................................................ 328 Глобальные ловушки и ловушки потока ......................................... 329 Установка ловушки .......................................................................... 330 Процедуры ловушек ........................................................................ 331 Типы ловушек .................................................................................. 331 Цепочки ловушек ............................................................................. 332
14 Программирование в Win32 API на Visual Basic Пример локальной ловушки ........................................................... 332 Пример глобальной ловушки ......................................................... 336 Модуль frmRpiHook ........................................................................ 338 Модуль basRpiHook ........................................................................ 339 Библиотека rpiGlobalHook.dll .......................................................... 341 Установочное приложение ............................................................. 342 Глава 20. Внедрение DLL и доступ к внешнему процессу ................................................................... 344 Доступ к внешнему процессу. Граф отслеживаемых потоков .... 344 Функция rpiVirtualAlloc .................................................................... 346 Функция rpiVirtualFree..................................................................... 349 Тестирование функций выделения памяти ..................................... 349 Выделение памяти внешнего процесса ........................................ 350 Функция rpiVirtualWrite .................................................................... 350 Функция rpiVirtualRead ................................................................... 351 Пример извлечения данных управляющего элемента другого процесса ............................................................................ 351 Пример исправления системы помощи VB6 ................................ 353 Часть IV. Windows GDI ............................................................... 357 Глава 21. Растровые изображения .......................................... 358 Прямоугольники .............................................................................. 358 Растры .............................................................................................. 359 Строки развертки ........................................................................... 360 Аппаратнонезависимые растровые изображения ......................... 360 Функции для работы с растровыми изображениями .................. 362 Функция BitBlt ................................................................................ 363 Пример перемещения игральных карт ........................................... 364 Использование растровых изображений в меню ........................ 371 Глава 22. Обзор контекстов устройств .................................. 373 Как Windows управляет рисованием окна ..................................... 374 Словарь областей .......................................................................... 374 Функции, влияющие на области окна ............................................. 375
15 Содержание Область, требующая перерисовки, и сообщения WM_PAINT . . . . . . . . . . . 376 Контексты устройств ...................................................................... 377 Использование контекста устройства ............................................ 378 Свойства, устанавливаемые по умолчанию .................................... 379 Режимы контекста устройства........................................................ 380 Контексты устройства в Visual Basic ............................................... 381 Перья ............................................................................................. 384 Кисти ............................................................................................. 385 Пути ............................................................................................... 388 Глава 23. Типы контекстов устройств ..................................... 390 Информационные контексты устройства ..................................... 390 Контексты устройства памяти ....................................................... 392 Контексты устройств принтера ...................................................... 393 Перечисление принтеров ............................................................... 395 Перечисление драйверов принтеров ............................................. 398 Печать ............................................................................................ 400 Контексты устройств дисплея ........................................................ 401 Кэшируемые и некэшируемые контексты дисплея ......................... 402 Классы и контексты дисплея .......................................................... 402 Координатные системы .................................................................. 403 Физические устройства ................................................................. 403 Глава 24. Координатные системы контекстов устройств ................................................................... 405 Координатные системы GDI ........................................................... 407 Виртуальное пространство ............................................................ 409 Пространство устройства .............................................................. 410 Пространство страницы ................................................................. 411 Сдвиг ............................................................................................. 411 Масштабирование ......................................................................... 412 Отражение ..................................................................................... 413 Из виртуального пространства в физическое .............................. 413 Пример .......................................................................................... 414 Установка логических координат в физическом пространстве .. 416 Пример .......................................................................................... 417
1 Программирование в Win32 API на Visual Basic Режимы отображения ..................................................................... 419 Режим отображения текста ............................................................ 419 Метрические режимы отображения ............................................... 420 Анизотропный режим отображения ................................................ 421 Изотропный режим отображения ................................................... 421 Мировое пространство ................................................................... 422 Поворот ......................................................................................... 423 Отражение ..................................................................................... 423 Масштабирование ......................................................................... 423 Наклон ........................................................................................... 423 Глава 25. Шрифты .......................................................................... 425 Семейства шрифтов ....................................................................... 426 Технологии создания шрифтов ...................................................... 426 Наборы символов ............................................................................ 426 Логические и физические шрифты ............................................... 427 Структуры, связанные со шрифтами .............................................. 427 Получение текущего логического/физического шрифта................. 429 Перечисление шрифтов ................................................................. 430 Часть V. Приложения ................................................................. 433 Приложение 1. Буфер обмена ................................................... 434 Буфер обмена Windows ................................................................... 434 Копирование текста в буфер обмена .............................................. 435 Пример .......................................................................................... 437 Вставка текста из буфера обмена .................................................. 438 Другие функции буфера обмена..................................................... 439 Пример создания окна просмотра буфера обмена ..................... 439 Приложение 2. Оболочка Windows ........................................... 446 Перетаскивание .............................................................................. 447 Ассоциации файлов ........................................................................ 449 Системная область значков на панели задач ............................... 451 Пример .......................................................................................... 453
1 Содержание Операции с файлами ...................................................................... 454 Корзина ............................................................................................ 456 Приложение 3. Реестр и индивидуальные инициализационные файлы ..................................................... 457 Реестр Windows ............................................................................... 457 APIфункции, связанные с реестром .............................................. 459 Примеры ........................................................................................ 460 Индивидуальные инициализационные файлы ............................. 466 APIфункции индивидуальных инициализационных файлов............ 466 Предметный указатель ................................................................ 472
Предисловие Издание предназначено для профессиональных программистов на Visual Basic. Книга в основном посвящена рассмотрению следующих тем:  Win32 API и его использование в Visual Basic версий 5.0 и 6.0;  основы функционирования операционных систем Windows NT и Windows 9x. Интерфейс прикладного программирования Win32 API (Application Program­ ming Interface – API) – это программный интерфейс, который используется для управления операционной системой Windows. Win32 API состоит из набора функ­ ций и подпрограмм, поставляемых в виде динамически подключаемых библио­ тек (Dynamic Link Libraries – DLL), которые обеспечивают программный доступ к возможностям операционной системы. Рассмотрение первой из упомянутых выше тем имеет практическую направ­ ленность: Win32 API может интенсивно использоваться для расширения возмож­ ностей Visual Basic. Практическая ценность второй темы не столь очевидна, но ее изучение не менее важно, поскольку в документации Microsoft нечасто принимает­ ся во внимание уровень знаний пользователя. Поэтому понимание основ операци­ онной системы Windows поможет программистам VB свободнее ориентироваться в документации Microsoft. Безусловно, данные темы не являются полностью независимыми друг от друга. Назначение Win32 API – реализовывать сервисы (или, если хотите, возможности) операционной системы Windows. Следовательно, для того чтобы разобраться в функциях Win32 API, важно иметь некоторое представление о том, как работает сама Windows. Эта книга не является энциклопедией по Win32 API. Она написана с целью дать достаточно полную информацию для формирования общего представления об операционной системе Windows и Win32 API и позволить вам в дальнейшем совершенствовать знания с помощью документации Microsoft. Читательская аудитория Требования к читателю этой книги просты: знание языка Visual Basic 4­й и последующих версий и желание использовать его возможности в области сис­ темного программирования. Данная книга поможет вам в решении конкретных задач, например в работе над приложением, реализация которого требует боль­ шего, чем может предложить VB. Или же вы захотите глубже разобраться в том, как работает Windows, без мучительного преодоления слишком крутого порога знаний, связанного с программированием для Windows в стиле VC++. Для чтения этой книги не потребуется ни знание основ VC++ (или C++, или C), ни предварительный опыт работы с Win32 API. Посвящается Донне
1 Содержание архива с примерами Содержание архива с примерами На сайте издательства «ДМК Пресс» www.dmkpress.ru содержится програм­ мный код нескольких приложений, которые подробно рассматриваются в книге. Вы можете использовать этот код в исходном виде или изменить по своему ус­ мотрению. Однако следует учесть, что эти приложения были написаны в качестве учебных пособий. В них почти нет кода, обрабатывающего ошибки, а их интерфейс намечен лишь в общих чертах. (Например, я уделял мало внимания вопросам, связанным с изменением разрешения экрана.) Обратите также внимание на то, что этими программами можете пользоваться только вы – они не предназначены для дальнейшего распространения. (Кста­ ти, префикс rpi в именах большинства приложений является сокращением от Roman Press Inc., названия моей компьютерной консультационно­издательской компании.) Если вы обнаружите серьезные ошибки в любом из приложений или сможете предложить для данных программ какое­нибудь интересное применение, расскажите об этом на сайте www.romanpress.com . Файлы rpiAPI.bas и rpiExampleCode.bas Включенный в состав архива стандартный программный модуль rpiAPI.bas содержит основные декларации ряда функций (в том числе и импортируемых из DLL) и большинство вспомогательных программ, используемых в примерах данной книги. Программы, которые больше связаны с конкретными примерами, находятся в модуле rpiExampleCode.bas. Смысл такого разделения состоит в том, что программы из rpiAPI.bas вы сможете использовать в собственных разработках, а программы из rpiExampleCode.bas по своей сути имеют учебно­иллюстративный характер для частных случаев применения тех или иных функций. Приложения Ниже приводятся примеры программ, которые разбираются в книге и вклю­ чены в состав архива:  rpiAllocMemory демонстрирует возможности rpiAccessProcess.dll, с помо­ щью которой можно получать доступ к внешнему процессу для выделения памяти, передачи данных и т.д .;  rpiBitBlt демонстрирует использование функции BitBlt;  rpiClipViewer позволяет просматривать буфер обмена;  rpiEnumProcsNT и rpiEnumProces95 отображает существующие в данный момент в системе процессы;  rpiEnumWins показывает информацию обо всех существующих в данный момент окнах;  rpiFileMapping демонстрирует отображение файлов в память;  rpiGlobalHook – пример глобальной ловушки мыши. Использует rpiGlobal Hook.DLL;  rpiLocalHook – пример локальной ловушки мыши;  rpiPEInfo предоставляет данные о загрузочном файле;  rpiSpyWin извлекает информацию об окне;  rpiSubClass демонстрирует модификацию (subclassing);
20 Предисловие  rpiThreadSync представляет данные о синхронизации потоков (мьютексы, события и семафоры);  rpiDLL – DLL с разнообразными функциями, включая rpiVarPtr, кото­ рая выполняет те же действия, что и недокументированная функция VB VarPtr по отношению к нестроковым данным, и может использоваться для имитации StrPtr. Включены также функции rpiGetTargetByte, rpiGetTargetInteger, rpiGetTargetLong и rpiGetTarget64, кото­ рые реализуют операцию раскрытия ссылки в VB;  rpiUsageCount.dll – DLL, с помощью которой можно определить, выполня­ ется ли данное приложение VB в настоящий момент. Приложение rpiAPIData В архив также включено приложение rpiAPIData, являющееся, по своей сути, внешним интерфейсом базы данных, в таблицах которой содержится следующая информация:  более 6000 констант Win32 и их значений;  более 1500 деклараций API­функций на VB;  около 1000 идентификаторов сообщений;  приблизительно 200 констант стиля;  более 400 деклараций структур (типов);  более 600 деклараций типов данных Win32. На рис. 1 и 2 показаны основные окна этого приложения. Его можно применять для извлечения деклараций функций, констант, типов и т.д . и их использования в проектах VB. Если потребуется преобразовать это приложение в надстройку VB, то необходимую информацию о создании надстроек VB вы можете найти в моей книге Developing Visual Basic Add-ins, опубликованной в издательстве O’Reilly. Рис. 1. Приложение rpiAPlData
21 Ресурсы и справочная информация Очевидно, что программирование в среде Win32, неважно, на языке VC++ или VB, является достаточно сложной задачей. Помимо этой книги вам потребуются справочные материалы, из которых можно порекомендовать следующие:  начиная с Visual Basic 6.0, Microsoft перенесла всю документацию из Visual Studio (VB6, VC6, VJ6 и т.д .) в библиотеку MSDN, которая поставляется с VB6. Таким образом, вы получаете документацию не только по VB6, но и по VC6 и Win32 API. Несомненно, это самая полезная документация для программирования Win32 API на VB. Версия библиотеки MSDN, которая включена в Visual Studio, называется Visual Studio Edition MSDN. Честно говоря, мне не удалось установить, чем она отличается от той версии библиотеки MSDN, которая распространяется по подписке. (Подробную ин­ формацию о подписке можно получить на сайте http://msdn.microcoft.com/.) Если в книге используется термин «документация» без каких­либо по­ яснений, то имеется в виду библиотека MSDN. Помимо всего прочего, в библиотеке содержатся некоторые очень полезные статьи, а также «база знаний» Microsoft (Microsoft Knowledge Base), которая постоянно попол­ няется;  если вы хотите больше узнать об операционной системе Windows, то я могу порекомендовать книгу Джеффри Рихтера «Windows для профессионалов» (Jeffrey Richter. Advanced Windows), третье издание, Microsoft Press – Рус­ ская Редакция, 2001 год. Однако следует учесть, что в указанной книге нет описания программ на VB, она целиком посвящена программированию на Рис. 2 . Просмотр типов данных с помощью приложения rpiAPlData Ресурсы и справочная информация
22 Предисловие VC++ и довольно сложна для чтения. Если вам необходимо больше узнать о функционировании Windows NT (операционной системы Microsoft, ши­ рокое распространение которой ожидается в ближайшем будущем), читай­ те книгу Дэвида Соломона «Основы Windows NT» (David Solomon. Inside Windows NT), второе издание, Microsoft Press. Но учтите, что она предназна­ чена для специалистов, и в ней совсем нет примеров программ. Обозначения, принятые в книге В данной книге используются следующие шрифтовые выделения:  курсивом помечены смысловые выделения в тексте;  полужирным шрифтом выделяются названия элементов интерфейса: пунк­ тов меню, пиктограмм и т.п.;  моноширинный шрифт применяется в листингах (программном коде). Информационная поддержка Информация, содержащаяся в этой книге, многократно тестирована и про­ верена. Но в тексте, возможно, остались ошибки. Кроме того, могли измениться какие­либо характеристики. Пожалуйста, сообщите о любых ошибках, которые вы обнаружите, по адресу nuts@oreilly.com. Вопросы технического характера и комментарии к книге посылайте по адресу bookquestions@oreilly.com . Получить техническую информацию о программировании на Visual Basic, при­ нять участие в работе конференции по VB вы сможете на сайте http://vb.oreilly.com. Благодарности Я хотел бы поблагодарить редактора Рона Петрушу (Ron Petrusha) и сотруд­ ника редакции Тару Макголдрик (Tara McGoldrick) за их помощь в подготовке книги. Хочу выразить особую благодарность Ману Чайлдзу (Man Childs) за со­ ставление подробной технической рецензии. Также благодарю Джеффри Лиггетт (Jeffrey Liggett), выпускающего редак­ тора; Эди Фридмена (Edie Freedman), дизайнера обложки; Майка Сьерра (Mike Sierra) за техническую поддержку; Рона Портера (Rhon Porter) за иллюстрации; Дэвида Футато (David Futato); Джэффа Хоулкома (Jeff Holcomb); Клер Клотье Леблан (Claire Cloutier LeBlanc) и Элен Траутман Заиг (Ellen Troutman Zaig).
Глава 1. Введение Глава 2. Начальные сведения Глава 3. Объявление API-функций Глава 4. Типы данных Глава 5. Знаковые и беззнаковые типы данных Глава 6. Строки Глава 7. Функции получения системной информации Глава 8. Обработка исключений Часть I Объявление API-функций в Visual Basic
Глава 1. Введение Как уже упоминалось в предисловии, эта книга преследует две цели:  описать Win32 API и его использование в Visual Basic версий 5.0 и 6.0;  описать основные принципы функционирования операционных систем Windows NT и Windows 9x. Возникает закономерный вопрос: зачем программисту на Visual Basic вникать в работу Win32 API? Ответ прост: Win32 API­функции предоставляют доступ к неограниченным возможностям операционной системы Windows. Плохо это или хорошо, но среда программирования Visual Basic скрывает от программиста мощный потенциал операционной системы Windows. Частично это связано с большей сложностью системного программирования, частично с вполне понятной позицией Microsoft, согласно которой программистов VB следует обе­ регать от всех этих сложностей (и, следовательно, от совершения более серьезных ошибок, чем это возможно в VB). Win32 API предоставляет программистам дополнительные возможности. Ниже перечислено то самое простое, что вы научитесь делать в Visual Basic с помощью Win32 API:  добавлять в окно списка VB позиции табуляции;  включать в окне списка VB горизонтальную полосу прокрутки;  размещать растровые картинки в пунктах меню VB;  определять состояние приложения VB на данный момент и не допускать запуска нескольких копий одного приложения;  получать системную информацию, включающую номер версии Windows, установленной на компьютере (Windows 95, 98 или NT), полный путь к ка­ талогу Windows, разрешающую способность экрана или количество кнопок у подключенной мыши;  добавлять пиктограмму в системную область значков Windows. Кроме того, вы изучите и сможете использовать в приложениях VB ряд более сложных приемов программирования, своего рода секретов мастерства (how to). Вы узнаете:  как получить список всех активных приложений;  как синхронизировать два приложения VB так, чтобы они могли работать во взаимодействии друг с другом (выполнение одного приложения при­ останавливается до завершения задачи другим приложением);  как получать данные из управляющего элемента другого приложения;  как модифицировать (subclass) окна и управляющие элементы так, чтобы изменить их поведение;
25  как устанавливать локальные и глобальные ловушки (hooks) для управле­ ния мышью или клавиатурой;  как внедрять DLL в адресное пространство другого процесса и исполнять ее код;  как пользоваться функциями рисования Windows GDI;  как настраивать окно буфера обмена так, чтобы сохранять результаты не­ скольких операций копирования для последующей вставки. Наконец, вы узнаете некоторые из секретов работы Windows:  как устроена область памяти, выделяемая процессу;  как определить формат исполняемых файлов, какие функции экспортирует DLL и к каким DLL обращается данный исполняемый файл. Что такое Win32 API В настоящее время Microsoft предлагает две 32­разрядные операционные сис­ темы – Windows 98 и Windows NT. Интерфейс прикладного программирования (Application Programming Interface Win32 – Win32 API) – это программный интер­ фейс, который используется для управления этими операционными системами. Или, более конкретно, Win32 API состоит из набора функций и подпрограмм, предоставляющих программный доступ к возможностям операционной системы. Иначе говоря, программные интерфейсы приложений представляют собой на­ боры функций (в этот обобщенный термин мы включаем и подпрограммы), которые обеспечивают сервисы данного приложения. Win32 API не является исключением и содержит более 2000 функций для реализации всех видов сервисов операцион­ ной системы. В табл. 1.1 приведены некоторые категории API­функций, которые дают хорошее представление о возможностях Win32 API. Таблица 1.1. Некоторые категории APIфункций APIфункции Растровых изображений Для работы с сообщениями и очередями сообщений Кисти Для работы с метафайлами Буфера обмена Ввода данных от мыши Цвета Работы с мультимедиа Диалоговых окон Управления несколькими дисплеями Коммуникаций Многодокументного интерфейса Пространства координат и преобразования Рисования и черчения Курсора Пути (Path Functions) Библиотеки декомпрессии данных Управления питанием Отладки Управления печатью Контекста устройства Управления процессами и потоками Диалога Работы с прямоугольниками Что такое Win32 API
2 Определение отношений Таблица 1.1. Некоторые категории APIфункций (окончание) APIфункции Динамического обмена данными (DDE) Меню Динамически подключаемых Работы с реестром библиотек (DDL) Ресурсов Обработки ошибок Полос прокрутки Регистрации в системе Работы со строками Работы с файлами Получения системной информации Файловой системы Отключения системы Работы со шрифтами и текстом Резервирования на магнитной ленте Работы со значками Времени Быстрого вызова с клавиатуры Управления таймером Ввода с клавиатуры Для работы с наборами символов и Unicode Операций с большими целыми числами Работы с окнами Рисования линий и кривых Оконного терминала и рабочего стола Управления памятью Windows для работы в сети API-функции для работы со значками Чтобы получить представление о том, для чего используются такие функции, давайте бегло ознакомимся с функциями работы со значками (icon):  CopyIcon копирует значок;  CreateIcon и CreateIconIndirect создают значок;  CreateIconFromResource создает значок из необработанных данных в файле ресурса;  DestroyIcon уничтожает значок;  DrawIcon и DrawIconEx выводят значок в заданном положении;  ExtractIcon, ExtractIconEx и ExtractAssociatedIcon извлекают значки из исполняемого файла (.exe или .dll);  GetIconInfo получает информацию о значке;  LoadIcon загружает значок из указанного модуля или экземпляра прило­ жения;  LookupIconIdFromDirectory и LookupIconIdFromDirectoryEx вы­ полняют поиск значка, который удовлетворяет некоторым условиям. Другие API Microsoft Windows – это не единственная система, имеющая API. Например, можно сказать, что объектная модель Microsoft Excel – это программный интер­ фейс приложения Excel, модель Word – API Microsoft Word, а компонентная мо­ дель объектов доступа к данным (Data Access Objects – DAO) – API процессора баз данных Jet. API есть даже у PC BIOS и операционной системы DOS. Они используются при программировании на языке ассемблера для получения доступа к серви­ сам BIOS и DOS. Сервисы BIOS довольно примитивны. Например, в BIOS есть
2 дисковые сервисы для чтения и записи секторов и форматирования дорожек. API­ сервисы DOS являются более высокоуровневыми, чем сервисы BIOS, и поэтому их обычно легче использовать. Это особенно касается операций доступа к диску (отсюда название – дисковая операционная система). Например, в DOS API есть следующие функции для работы с файлом: открытие и закрытие, чтение, запись, поиск, удаление, переименование и т.д . Короче говоря, если BIOS API понимает только язык секторов и дорожек, то DOS API оперирует элементами файловой системы. Windows 9x и Windows NT – два разных Win32 API Любому, кто работал и с Windows 9x и Windows NT, хорошо известно, что это совершенно разные операционные системы. Пользовательский интерфейс у них практически одинаков, но внутреннее устройство различно. Помимо того, что Windows 9x не поддерживает механизмы защиты и симмет­ ричной многопроцессорной обработки (одновременное использование нескольких процессоров), наиболее значительные с точки зрения программирования API отличия заключаются в том, что Windows 9x не поддерживает набор символов Unicode и не защищает свое адресное пространство от случайного воздействия некорректно выполняющихся приложений. В результате Windows 9x работает менее стабильно, чем Windows NT. Странно, что Windows 9x защищает адресное пространство каждого прило­ жения от доступа других приложений, но не предохраняет свою собственную память от такого доступа. Следовательно, неправильно работающая программа не может причинить вреда другой прикладной программе, однако она может разрушить операционную систему, что приводит к полному отказу машины. Windows NT, наоборот, защищает собственное адресное пространство от выпол­ няющихся приложений и, значит, является гораздо менее предрасположенной к сбоям, возникающим в результате случайных попыток записи в предохраняемую область памяти. Для рядового пользователя компьютера, который редактирует тексты доку­ ментов, хранит на ПК балансы чековой книжки, подключается к Internet и иг­ рает в какие­нибудь игры, это, может быть, не так уж и важно. Но для опытного пользователя и особенно для программиста стабильность операционной системы имеет большое значение, потому что это существенно уменьшает вероятность ее вынужденной перезагрузки. По этим причинам я отдаю решительное предпочтение Windows NT. Правда, есть и оборотная сторона медали. Windows NT менее дружественна с точки зрения удобства пользовательского интерфейса, и, что более серьезно, у нее нет такой широкой поддержки аппаратных средств, как у Windows 9x. К сожалению, для кого­то это может стать причиной отказа от Windows NT. Во всяком случае я совершенно не хочу, чтобы у вас создалось впечатление, что в среде Windows 9x нельзя успешно программировать. Я делал это многие годы до того, как начал пользоваться Windows NT. Это просто означает, что вам следует ожидать более или менее регулярных перерывов в работе из­за отказов системы. Что такое Win32 API
2 Определение отношений Поскольку две операционные системы Windows так различны, не должно удивлять и то, что каждая из них реализует Win32 API по­своему. Некоторые API­ функции реализованы только в Windows 9x, другие – только в Windows NT. Это может создавать определенные трудности при программировании. Например, при­ дется написать две абсолютно разные версии программы, формирующей список всех активных процессов (исполняемых приложений), – одну, которая работает в Windows 9x, и другую, которая выполняется в Windows NT. К счастью, в документации обычно разъясняется, какая из операционных сис­ тем поддерживает данную API­функцию. Проблемы программирования Win32 API в среде Visual Basic Можно выделить два аспекта в изучении того, как использовать Win32 API в среде Visual Basic:  перевод функций Win32 API на язык Visual Basic, чтобы их можно было использовать в приложениях VB;  получение общего представления о диапазоне задач, решаемых с помощью Windows API. В книге сделана попытка достичь и той, и другой цели. Тем не менее это издание – не справочник по Win32 API, и в том, что касается второй цели, ударение следует делать на словах «общее представление». При использовании Windows API труднее всего выяснить, существует ли такая функция, которая может решить поставленную задачу, причем точно такую, какую решает имен­ но эта функция. Подобная задача может превратиться в серьезное (чреватое стрессом) испытание, так как документация Win32 не всегда написана с абсо­ лютной ясностью. Документация Win32 API, которая является теперь частью библиотеки MSDN, нацелена на использование Visual C++ (хотя сама по себе документация по API не предполагает применения объектно­ориентированного подхода, в отличие от библи­ отеки основных классов Microsoft (Microsoft Foundation Classes – MFC). Соответс­ твенно, можно потерять много времени, обучаясь переводить объявления функций и типов данных Visual C++ на язык Visual Basic. После прочтения этой книги объявления VC++ станут для вас почти такими же простыми и понятными, как и декларации VB. Следует отметить, что перевод объявлений функций Win32 с VC++ на Visual Basic не такая уж легкая задача, так как в объявлениях функций Win32 использует тысячи различных типов данных. Для того чтобы перевести объявление API­фун­ кции на VB, нужно заменить любые из этих типов данных на те немногие, кото­ рые доступны VB. Не менее важно, что VB интерпретирует строки и структуры (пользовательские типы) не совсем так или совершенно не так, как Win32. Но самое главное, секретом успешных преобразований является практика, и ее у вас при изучении данного материала будет предостаточно.
2 Аккуратность прежде всего Win32 API, конечно, не панацея от любой головной боли программиста. Мно­ гие задачи имеют несколько способов решения. Например, программисту VB требуется создать текстовый файл на жестком диске. Эту задачу можно выполнить несколькими способами:  использовать привычный оператор Open, синтаксис которого следующий: Open pathname For mode [Access access] [lock] _ As [#] filenumber [Len=reclength]  воспользоваться новой объектной моделью FileSystemObject, реализо­ ванной в библиотеке сценариев времени выполнения Windows (Windows Scripting Runtime Library): Dim fso As New FileSystemObject Dim ts As TextStream Set ts = fso.CreateTextFile("d:\temp\doc.txt", True)  задействовать Win32 API­функцию CreateFile, синтаксис которой до­ вольно внушителен: Private Declare Function CreateFile Lib "kernel32" _ Alias "CreateFileA" ( _ ByVal lpFileName As String, _ ByVal dwDesiredAccess As Long, _ ByVal dwShareMode As Long, _ ByVal lpSecurityAttributes As Long, _ ByVal dwCreationDisposition As Long, _ ByVal dwFlagsAndAttributes As Long, _ ByVal hTemplateFile As Long _ ) As Long Функция CreateFile возвращает низкоуровневый идентификатор (handle) файла. Конечно, использовать ее нужно только в том случае, если она дает какие­ то преимущества по сравнению с другими, высокоуровневыми и, значит, более простыми вариантами. Вы познакомитесь с таким примером в главе 10, где для отображения части файла в память применяется его идентификатор. Для создания текстового файла правильнее использовать один из наибо­ лее простых высокоуровневых методов. Почему бы не воспользоваться объ­ ектом FileSystemObject, который создает файл с помощью API­функции CreateFile? На рис. 1.1 показаны API­функции, импортируемые (вызываемые) файлом SCRRUN.DLL, в котором содержится объект FileSystemObject. Об­ ратите внимание, что список включает CreateFileA и CreateFileW, которые являются ANSI­ и Unicode­версиями функции CreateFile. (Кстати, программа rpiPEInfo, окно которой показано на рис. 1.1, является одним из приложений, создание которого описывается в данной книге.) Win32 API – это самый низкий из доступных программисту уровней. Он открывает программам на VC++ доступ ко всем возможностям операционной Аккуратность прежде всего
30 Определение отношений системы Windows, а программам на VB позволяет приблизиться к этой цели в значительно большей степени, чем при использовании только VB. Тем не менее при решении проблем программирования разумнее выбирать на­ иболее высокоуровневое решение, за исключением, возможно, тех случаев, когда предъявляются повышенные требования к характеристикам. По мере чтения этой книги вам станет ясно, что существует множество ситуаций, в которых Win32 API как раз и является этим «наиболее высоким» уровнем. Будьте внимательны Работа с Win32 API – это не то же самое, что работа с Visual Basic. Win32 API позволяет гораздо ближе подойти к операционной системе, где защита значитель­ но слабее, чем в Visual Basic. В действительности VB – очень защищенная среда, но за это приходится платить ограниченным доступом к операционной системе и непосредственно к памяти. В частности, вы рискуете при чтении из памяти или записи в память (но в этой операции нет ничего такого, что нельзя было бы исправить перезагрузкой). Если попытаться (умышленно или нечаянно) записать в защищенную область Рис. 1.1. Функции, импортируемые библиотекой сценариев времени выполнения
31 памяти или прочитать из нее, операционная система, скорее всего, выдаст ошибку общего нарушения защиты (GPF). На рис. 1.2 показано диалоговое окно с сооб­ щением о GPF (в Windows NT 4.0). Рис. 1.2 . Общее нарушение защиты (GPF) При нажатии на кнопку OK происходит одно из двух событий:  операционная система аварийно завершит сеанс VB, но Windows и все дру­ гие приложения продолжают работать нормально;  вся система становится нестабильной. В этом случае все несохраненные данные будут потеряны и потребуется перезагрузка системы. Я программировал какое­то время, используя Windows API, и в Windows 95, и в Windows NT 4 (но не в Windows 98) и могу сказать по собственному опыту, что по последнему сценарию события развиваются гораздо чаще в Windows 95, чем в Windows NT. Это легко объяснить сказанным выше о защите памяти. Windows NT у меня отказывала крайне редко. Во всяком случае, это наводит на одну очень важную мысль о программиро­ вании с помощью Win32 API (или любом другом программировании): следует сохранять данные во всех приложениях, включая проект VB, перед выполнением любой программы, особенно если в ней присутствуют вызовы API­функций. Будьте внимательны
Глава 2. Начальные сведения В этой главе содержатся некоторые предварительные сведения, необходимые для освоения материала следующих частей книги. Символьные коды Символьный код – это отображение упорядоченного множества целых чисел (начиная с нуля) на множество символов. В Windows API используется несколько разных символьных кодов. ASCII Первоначально Американский стандартный код для обмена информацией (American Standard Code for Information Interchange – ASCII) состоял из 128 сим­ волов, включая буквы латинского алфавита, арабские цифры и различные знаки пунктуации. В соответствие символам были поставлены первые 128 неотрицатель­ ных целых чисел в виде 8­разрядных двоичных чисел. В 1981 году, выпустив IBM PC, IBM расширила первоначальный код ASCII, дополнив его еще 128 символами, включая некоторые символы из алфавитов других языков, небольшое количество математических символов и некоторые графические символы для рисования ра­ мок, фона и других элементов псевдографики в символьном режиме дисплея. Этот код, состоящий из 256 символов, известен как расширенный код ASCII (extended ASCII). ANSI Microsoft заимствовала код Американского национального института стандар­ тов (American National Standard Institute – ANSI), или символьный код ANSI, для Windows 1.0, выпущенной в 1985 году. Первые 128 символов и их коды были те же, что и у ASCII, но набор остальных символов был другой. По некоторым причинам не всем из дополнительных кодов были назначены соответствующие символы. До сих пор приходится слышать «разумные» объяснения этой, по всей видимости, пустой траты ценных ресурсов. В 1987 году Microsoft предложила идею создания кодовой страницы (code page), которая является все тем же отображением символов на числа. Исходный расши­ ренный символьный код ASCII, введенный IBM, носит название кодовой страницы 437, или MS-DOS Latin US. Первые 128 символов у всех кодовых страниц полно­ стью совпадали, но следующие 128 символов менялись от страницы к странице. Хотя количество кодовых страниц быстро увеличивалось, они не решали сути проблемы, которая заключалась в том, что 256 символов просто недостаточно для
33 нормальной работы. В качестве примера можно привести китайский или японский языки, каждый из них содержит более 20000 символов. А ведь еще существуют сотни математических и других часто используемых символов. DBCS Для вмещения большего количества символов был изобретен набор двухбай­ товых символов (double­byte character set), или код DBCS. Сказать, что код DBCS неудобен в применении, – значит быть слишком деликатным. Можно много чего добавить по поводу кода, в котором некоторые символы кодируются 8­разрядным кодом, в то время как другие – 16­разрядным. Нетрудно представить себе и воз­ никающие при этом проблемы. Например, невозможно по длине двоичной строки определить количество символов в ней. К счастью, код DBCS поддерживается только в вариантах Windows, разрабо­ танных специально для стран, которым именно такой код и необходим. Мы его полностью проигнорируем, но упоминание о нем можно встретить в документации Win32. Unicode Проект Unicode был создан в 1988 году. Unicode – это двухбайтовый символь­ ный код, позволяющий представить 65536 различных символов. Версия 2.0 стан­ дарта Unicode включает 38885 символов. Microsoft использует термин «широкий» символ (wide character) как синонимом символа Unicode, хотя другие используют термин «широкий» для обозначения любого двухбайтового символьного кода. Первые 256 символов Unicode те же, что и у расширенного символьного кода ASCII. Старший байт каждого такого кодового слова установлен в нуль. Кроме того, имеется несколько блоков с последовательными значениями кодов, отображаемых на соответствующие наборы символов, и множество неиспользуемых промежутков между блоками для будущих расширений. Например, греческий алфавит находится в диапазоне &H370–&H3FF вместе с некоторыми другими символами (буква «аль­ фа» нижнего регистра имеет код &H3B1). Консорциум Unicode (Unicode Consortium) является ответственным за кон­ троль развития Unicode и предоставление технической информации, связанной с этим кодом. Консорциум сотрудничает с ISO в области дальнейшей специфи­ кации Unicode и расширения набора символов. В консорциум входят крупные компьютерные компании (такие как IBM, Apple, Hewlett­Packard и Xerox), компа­ нии, производящие программное обеспечение (Microsoft, Adobe, Lotus и Netscape), международные организации, университеты и даже некоторые частные лица. До­ полнительную информацию о символьном коде и консорциуме Unicode можно получить на сайте http://www.unicode.org/unicode/contents.html. Важно отметить, что при размещении в памяти двухбайтового целого числа (такого как кодовое слово Unicode) первым записывается младший байт. Напри­ мер, кодовое слово Unicode &H0041 хранится в обратном порядке – 4100. Символьные коды
34 Начальные сведения Поддержка Unicode в Windows В Windows NT Unicode является основным символьным кодом. Другими сло­ вами, Windows NT специально проектировалась с учетом его использования. Для обеспечения совместимости также поддерживается ANSI. Однако Windows 9x, за исключением некоторых особых случаев, не поддерживает Unicode. В частности, Unicode используют все API­функции, связанные с OLE, и некоторые другие. Как упоминалось ранее, это один из основных недостатков Windows 9x. С другой стороны, Visual Basic использует Unicode для внутреннего представ­ ления строковых данных (при работе и с Windows NT, и с Windows 9x). Отсутствие полной поддержки Unicode в Windows 9x приводит к существенным проблемам, которые обсуждаются в главе 6. Параметры и аргументы Следует провести четкую грань между параметром и аргументом. Параметр (parameter) – это заместитель (описание) объекта, используемый при объявлении функции; аргумент (argument) – это сам объект, передаваемый функции. Таким об­ разом, параметр используется при объявлении функции, аргумент – при ее вызове. В ряде случаев используются термины «формальный» и «фактический» параметр. Параметры IN и OUT Параметр функции может использоваться в следующих случаях:  для передачи значения в функцию;  для возвращения значения функцией. Параметр, в котором значение передается в функцию, называется IN­парамет­ ром, а тот, в котором значение возвращается функцией, называется OUT­парамет­ ром. IN/OUT­параметр соответствует обоим этим действиям. Обозначения IN и OUT иногда используются в документации. ByVal и ByRef Различие между параметрами, передаваемыми по значению и по ссылке, мож­ но изложить следующим образом. Использование в VB параметра ByVal пред­ полагает передачу значения аргумента, а применение ByRef – передачу адреса аргумента (указателя на аргумент). Четкое понимание различий между передачей параметров по значению и по ссылке особенно важно, так как при работе с вызовами API­функций любая не­ точность приводит к аварийному завершению приложения (GPF). Динамически подключаемые библиотеки API­функции Windows входят в состав динамически подключаемых библио­ тек, поэтому необходимо дать четкое определение DLL и кратко изложить основ­ ные принципы их работы. DLL будут не раз обсуждаться в следующих главах. Динамически подключаемая библиотека (Dynamic Link Library – DLL) явля­ ется исполняемым файлом, который содержит несколько экспортируемых функций
35 (exportable functions), то есть функций, к которым могут обращаться другие испол­ няемые приложения (EXE или DLL). Файлы DLL намного проще файлов EXE, например, в них нет кода, который управлял бы графическим интерфейсом или обрабатывал сообщения Windows. Уточняя терминологию, следует сказать, что исполняемый файл (executable file), который называется также файлом образа задачи (image file), является фай­ лом, соответствующим спецификации формата загружаемого кода PE (Portable Executable). PE­файлы имеют расширение .exe или .dll . Термин «исполняемый файл» часто используется для обозначения только exe­файлов, что не совсем точ­ но. (Формату PE­файлов посвящена целая глава этой книги.) К сожалению, Visual Basic не позволяет формировать обычные DLL. С его по­ мощью можно создавать DLL специального вида – сервер ActiveX (ActiveX server). Однако такие DLL не могут экспортировать функции обычным образом. Вместо этого они экспортируют объекты автоматизации (automation objects) вместе с их свойствами и методами. Поэтому их также называют серверами автоматизации (automation servers). В книге подобная разновидность DLL не описывается. Сказать, что в операционной системе Windows DLL применяются повсемест­ но – это значит сильно принизить их роль. Например, в той системе Windows NT, которая использовалась для подготовки этой книги, было не менее 1029 различ­ ных DLL, которые занимали на жестком диске около 93 Мб. Для размещения API­функций Windows использует несколько DLL. В действи­ тельности большая часть из 2000 функций Win32 API содержится в трех DLL:  KERNEL32.DLL – содержит около 700 функций, которые предназначены для управления памятью, процессами и потоками;  USER32.DLL – предоставляет порядка 600 функций для управления пользова­ тельским интерфейсом, например, созданием окон и передачей сообщений;  GDI.DLL – экспортирует около 400 функций для рисования графических образов, отображения текста и работы со шрифтами. Кроме этих библиотек Windows также содержит несколько других DLL более узкой специализации. Здесь приводятся некоторые из них:  COMDLG32.DLL – открывает доступ почти к 20 функциям управления стан­ дартными диалоговыми окнами Windows;  LZ32.DLL – хранит примерно 12 функций архивирования и разархивиро­ вания файлов;  ADVAPI32.DLL – экспортирует около 400 функций, связанных с защитой объектов и работой с реестром;  WINMM.DLL – содержит около 200 функций, относящихся к мультимедиа. Таблицы экспорта Нет необходимости говорить, что DLL бесполезна, если неизвестно, какие именно функции она экспортирует. (Еще одна причина обзавестись хорошей доку­ ментацией.) Каждая DLL содержит таблицу имен этих функций, так называемую таблицу экспорта (export table) DLL. Некоторые функции экспортируются из DLL только по позиции, но в данном случае это неважно. Динамически подключаемые библиотеки
3 Начальные сведения Помимо этого в каждой DLL есть таблица импорта (import table), в которой перечислены внешние функции, вызываемые из данной DLL. Это может показаться несколько неожиданным, но увидеть таблицы экспорта и импорта DLL не так­то просто. Создается впечатление, что существовал тайный заговор с целью скрыть данную информацию. Особенно это касается Visual Basic, в котором вообще нет средств для просмотра указанных таблиц. В Visual C++ для этого используется программа DUMPBIN.EXE. В принципе, утилита QuickView, входящая в состав Windows, способна показывать эти таблицы в режиме просмотра DLL. Вы можете возразить, что нет смысла знать только имена экспортируемых функций, так как без информации о параметрах и возвращаемых значениях (то есть о том, как эти функции использовать) сами функции не очень­то и полезны. Тем не менее в документации порой не указывается, какая из нескольких DLL экспортирует данную функцию. В таких случаях можно извлечь пользу из поиска в таблице экспорта. Иногда также таблица импорта DLL позволяет понять, каким образом реализована функция. Во всяком случае, одно из главных приложений, которые будут обсуждаться в данной книге – это утилита, выдающая информацию о PE­файле. Ее основное окно показано на рис. 2.1 . (Данное приложение включено в состав архива, который Рис. 2 .1. Утилита, информирующая о PEфайле
3 находится на сайте издательства «ДМК Пресс» www.dmkpress.ru .) Разработка этого приложения позволит приобрести некоторый опыт обращения с Win32 API. Роль DLL – динамическое связывание Приложения Windows независимо от использованного при их разработке языка программирования чрезвычайно сложны – гораздо более сложны, чем это нужно для автономных приложений. Действительно, приложение Windows требует боль­ шого количества внешних функций, включая Win32 API­функции и разнообразные функции времени выполнения VB или C, которые обычно входят в предварительно скомпилированные модули или библиотеки исполняемых модулей разного типа. В общем случае имеется два способа включить внешний код в приложение. Самый простой – это вставить внешний код непосредственно в исполняемое при­ ложение в момент его создания (то есть во время компоновки). Такой способ называется статическим связыванием (static linking). У статического связывания есть и преимущества, и недостатки. В числе досто­ инств – простота, так как результатом действий является автономное приложение. Но в этом же состоит и один из недостатков: такое приложение, содержащее весь необходимый код, имеет слишком большой объем. Статическое связывание решает также и проблему поддержки новых версий, поскольку исполняемый файл содержит в себе все, что необходимо для каждой версии. Но есть и оборотная сторона медали. Если обнаруживается ошибка во внешнем программном модуле, исполняемый файл требуется перекомпоновать с исправленной библиотекой. Однако основная трудность заключается в том, что статическое связывание способствует дублированию кода. Один и тот же библи­ отечный модуль может входить в состав множества разных приложений на одном и том же компьютере. Альтернативой статического связывания является динамическое связывание (dynamic linking). В этом случае один внешний программный модуль может обслу­ живать несколько приложений. Проще говоря, приложение компонуется вместе со ссылками на экспортируемые функции внешних динамически подключаемых библиотек. Как вы увидите в дальнейшем, единственный экземпляр DLL может отображаться в адресные пространства нескольких приложений одновременно. Таким образом, существует только один экземпляр DLL в физической памяти, и несмотря на это каждое приложение работает так, как будто имеет свою копию DLL в собственной области памяти. Поэтому вызов DLL­функций не менее эф­ фективен, чем вызов программы внутри самого приложения. В некотором смысле DLL становится частью приложения. Более детально эти темы рассматриваются в главе 13, поэтому не волнуйтесь, если сейчас чего­либо не поняли. Несколько слов о синтаксисе C++ Как упоминалось ранее, у вас будет много возможностей увидеть объявление API­функций на VC++. Поэтому необходимо ознакомиться с основами синтаксиса C++. Следует отметить, что здесь не описываются объектно­ориентированные аспекты этого языка. Немного синтаксиса C++
3 Начальные сведения Основы Основные положения синтаксиса языка C++ заключаются в следующем:  в C++ используется двойной слэш (//) для обозначения комментария. Это аналог апострофа (') в Visual Basic;  дополнительные пробельные символы (пробелы и возвраты каретки) в C++ игнорируются. В частности, не требуется вводить символ продолжения стро­ ки. Например, объявление функции VOID CopyMemory (PVOID Destination, CONST VOID *Source, DWORD Length); эквивалентно следующему варианту, более удобному для чтения: VOID CopyMemory ( PVOID Destination, // Указатель на адрес блока для копии. CONST VOID *Source, // Указатель на адрес блока для копирования. DWORD Length // Размер блока для копирования в байтах. ); Такой способ форматирования позволяет добавлять комментарии к каждому объявляемому параметру. Это свойство было бы очень полезно при докумен­ тировании деклараций VB;  почти все строки программ C++ заканчиваются точкой с запятой. Фигурные скобки используются для выделения программных блоков, состоящих из нескольких строк;  в языке C++ большое значение имеет регистр. Это также относится ко всем именам Win32 API­функций. Объявление переменных Переменная, которая в Visual Basic объявляется как Dim VarName as VarType в C++ определяется более лаконично: VarType VarName; Например, Dim x as Long ' Объявление переменной типа long. становится int x; // Объявление переменной типа integer. В C++ размер типа integer составляет 4 байта. Объявление массивов Для объявления массива, например, из 100 элементов целочисленного типа, нужно записать следующее: int iArr[100];
3 Необходимо отметить, что индекс этого массива изменяется от 0 до 99, так что (в отличие от VB) значение iArr (100) является некорректным. Заметьте также, что в C++ для индексации массива используются квадратные, а не круглые скобки. Объявление функций Функция, которая в Visual Basic объявляется как Function FName(Para1 as Type1, Para2 as Type2, ... ) as ReturnType в C++ определяется с использованием более лаконичного синтаксиса: ReturnType FName(Type1 Para1, Type2 Para2, ... ) Например, Function Sum(x as Long, y as Long) as Long становится int Sum(int x, int y); Указатели Говоря простым языком, указатель (pointer) – это адрес блока памяти. В Win32 длина всех адресов памяти составляет 32 разряда. Ссылочная (указательная) пере­ менная (pointer variable), чаще называемая просто указатель, является переменной типа указатель, то есть переменной, которую компилятор (VB или VC++) интерпретирует как переменную, хранящую адрес. На рис. 2.2 показана такая переменная. На этом рисунке Var – переменная произ­ вольного типа (целая, длинная целая, символь­ ная и т. д.). Ее содержимым является yy...yy, а ее адрес – bbbb. Переменная pVar – это перемен­ ная, относящаяся к типу указатель. В ней хра­ нится адрес Var и поэтому, как у всех указателей, ее длина составляет 32 разряда. Го­ ворят, что pVar указывает на Var и что Var – объект (target), на который ссылается указатель. Если бы переменная Var имела, например, тип Integer, то можно было бы сказать, что pVar представляет собой указатель на целый тип (integer pointer). Указатели и переменные такого типа – это очень мощные средства, которые повсеместно используются в Win32. Соответственно, VC++ поддерживает и ука­ затели, и операции с ними в полном объеме. Примером использования указателей в языке C++ может служить адресная арифметика (pointer arithmetic), которая непосвященному может показаться странной. В следующем фрагменте программы (его синтаксис более подробно рассмат­ ривается позже) объявляется переменная­указатель на целый тип pi и затем выводятся значения и pi, и pi+1: int i = 1; // Объявляем и инициализируем переменную целого типа. int *pi; // Объявляем указатель на целый тип. Указатель pVar:aaaa Целевой объект Var:bbbb Рис. 2 .2 . Указатель Указатели
40 Начальные сведения pi=&i; // Указатель ссылается на i (&  аналог AddressOf // в Visual Basic). cout << pi << " / " << pi+1; // Распечатывает значения pi и pi+1. Получился неожиданный результат: 0x0012FF78 / 0x0012FF7C где 0x – префикс, указывающий в VC++, что константа представлена в шестнад­ цатеричном коде. Обратите внимание, что pi+1 больше pi на 4. Причина получения такого результата заключается в следующем. VC++ учи­ тывает и то, что pi указывает на четырехбайтовое целое (хранит его адрес), и то, что единственное основание прибавить 1 к указателю – это перевести его на сле­ дующий элемент (здесь – на следующее четырехбайтовое целое) в памяти, адрес которого на 4 больше адреса текущего элемента. Поэтому VC++ прибавляет 4 (а не 1) к величине указателя. Поначалу это кажется несколько странным, но, как вы уже поняли, может быть очень удобным, так как предоставляет возможность просто прибавлять 1 к любому указателю, чтобы сместиться на следующий эле­ мент независимо от типа элементов (целое, длинное целое, символ и т.д .) . В отличие от VC++ в Visual Basic указатели скрыты от программиста. При­ чина заключается в том, что программирование с указателями может быть опас­ ным: случайное (или преднамеренное) неправильное использование указателя легко может привести к попытке доступа к защищенной памяти. На самом деле, как вы увидите в главе 13, операционная система Windows умышленно остав­ ляет незанятым некоторый участок адресного пространства памяти для того, чтобы безошибочно обнаруживать случайное использование null­указателей. Например, довольно распространенная ошибка – это использование указателя, ко­ торый забыли проинициализировать. Инициализация требует предварительного определения объекта, на который должен ссылаться указатель, как это делалось в предыдущем фрагменте программы. Если указатель не проинициализировать, то в результате он будет ссылаться на null. В любом случае, поскольку в Win32 указатели используются постоянно, нужно научиться обращаться с ними в VB. В VB можно делать почти все, что требуется, за исключением вызова функции с использованием указателя на эту функцию (в VC++ это сделать очень легко). Объявление указателя в VC++ выглядит просто. Делается это таким обра­ зом: targetdatatype *pointervariable; Пробелы до и после звездочки являются необязательными, но, по крайней мере, какой­нибудь один из них для ясности следует включать. Например, для того чтобы объявить указатель на целое, запишем: int *pi; Можно объявить указатель на объект неопределенного типа, то есть на объект, тип которого вам не известен. Это делается так: void *pWhatever;
41 Кстати, как будет видно из дальнейшего изложения, в VC++ широко исполь­ зуются синонимы типов данных. Поэтому можно встретить и такое эквивалентное предыдущему объявление: LPVOID pWhatever; где LPVOID является синонимом для дальнего указателя (long pointer) на неоп­ ределенный тип (void). Обходными путями к цели Звездочка (*) известна как оператор раскрытия ссылки (indirection operator). Она используется в двух случаях: для объявления указателя и для извлечения того значения, на которое он ссылается, то есть значения той переменной, на которую он указывает. Например, фрагмент программы int i; // Объявляем целое. int *pi; // Объявляем указатель. pi = &i; // Устанавливаем указатель на адрес целого. i=5; // Присваиваем целому числу значение. // Вывод на консоль. cout << "Pointer: " << pi << " Target: " << *pi; приводит к следующему выводу в окне консоли: Pointer: 0x0012FF78 Target: 5 Заметьте, что в последней строке фрагмента программы выражение *pi означает то, на что ссылается указатель pi или, более кратко, целевой объект (target) pi. Следует также отметить, что оператор &, называемый оператором взятия адре­ са (address­of operator), возвращает адрес своего операнда. Таким образом, &var является адресом переменной var. Этот оператор очень полезен при присво­ ении значений указателям. Указатели в Visual Basic Операторы раскрытия ссылки (*) и взятия адреса (&) – это ключи к использо­ ванию указателей в VC++. Как это и бывает, оператор взятия адреса имеет недо­ кументированный эквивалент в Visual Basic, а оператор раскрытия ссылки можно без особых проблем «подделать», используя API­функцию CopyMemory. В Visual Basic есть недокументированная функция, называемая VarPtr, ос­ тавшаяся от эпохи QuickBasic. Документация QuickBasic сообщает, что VarPtr возвращает смещение переменной, а VarSeg возвращает сегмент. Времена сегмен­ тной адресации, к счастью, миновали, но VarPtr осталась. Теперь она возвращает просто адрес переменной, то есть VarPtr(var) Указатели
42 Начальные сведения является адресом переменной var. Если вам не нравиться пользоваться недоку­ ментированными функциями VB, можете применить функцию rpiVarPtr из rpiAPI.dll, динамически подключаемой библиотеки из архива. Выражение rpiVarPtr(var) эквивалентно VarPtr(var). Однако, по причинам, которые описаны в главе 6, rpiVarPtr не работает со строковыми переменными. (Это связано с преобразо­ ванием из Unicode в ANSI, которое VB осуществляет автоматически, при вызове внешней функции со строкой в качестве параметра.) В этой связи вам может быть интересно, как выглядит текст этой функции на C: int WINAPI rpiVarPtr(int pVar) { return pVar; } Функция просто возвращает передаваемое ей значение аргумента. При объяв­ лении ее в VB, которое происходит следующим образом: Public Declare Function rpiVarPtr Lib "rpiAPI.dll" ( _ ByRef pVar As Any _ ) As Long аргумент переходит функции по ссылке. То есть VB передает адрес аргумента этой функции, которая просто возвращает значение аргумента в виде VB long. Заметьте, что использование As Any в объявлении функции rpiVarPtr не дает VB проверять тип данных передаваемого параметра, и можно передавать, таким образом, переменную любого типа. Это означает, например, что не нужно иметь rpiVarPtrByte, rpiVarPtrInteger, rpiVarPtrLong. (Необходимо еще раз напомнить, что rpiVarPtr не работает со строками.) Для реализации оператора раскрытия ссылки требуется способ, с помощью которого можно извлекать значение объекта, на который ссылается указатель. Самым простым способом является использование API­функции CopyMemory, которая копирует байты из одного адреса памяти в другой. В следующей главе будет подробно обсуждаться, как использовать CopyMemory в общем случае и ре­ ализовать раскрытие ссылки в частности. В библиотеку rpiAPI.dll, содержащуюся в архиве, входит также несколько функций для реализации оператора раскрытия ссылки.
Глава 3. Объявление APIфункций Как говорилось ранее, Windows API содержит более 2000 функций. Так как API предназначен для использования в среде VC++, все объявления API­функций написаны на языке Visual C++ (точнее, на языке C++ с расширениями Microsoft VC++). Ниже приводится пример API­декларации, взятый из документации по Win32 из архива с библиотекой MSDN: LRESULT SendMessage ( HWND hWnd, // Дескриптор (handle) окна  адресата сообщения. UINT Msg, // Передаваемое сообщение. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam // Второй параметр сообщения. ); Одно из возможных преобразований в VB (как вы увидите позже, есть и дру­ гие варианты) выглядит так: Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As Long, _ ByVal lMsg As Long, _ ByVal wParam As Long, _ ByRef lParam As Any ) _ As Long Задача, поставленная в данной главе, – сформулировать несколько основ­ ных принципов для осуществления таких преобразований. Для этого необходимо знать концепцию передачи параметров по значению (by value) и по ссылке (by reference). Для удобства будем называть любую процедуру (функцию или подпрограм­ му), которая принадлежит DLL и предназначена для экспорта, внешней функцией (external function). Процедура является внешней для приложения VB, которое вызывает эту функцию. Оператор VB Declare Оператор Visual Basic Declare используется для вызова внешних функций из DLL. Он должен определяться на уровне модуля, а не на уровне процедуры. Существует два варианта его синтаксиса – один для функций, другой для под­ программ: [Public | Private] Declare Sub name Lib "libname" _ [Alias "aliasname"] [([arglist])] и также
44 Объявление API-функций [Public | Private] Declare Function name Lib "libname" _ [Alias "aliasname"] [([arglist])] [As type] Элементы в квадратных скобках обозначают необязательные параметры, а вертикальная черта разделяет альтернативные варианты. Детальную информа­ цию о синтаксисе можно получить в справочной системе VB. Давайте обсудим основные составляющие оператора Declare. Public или private Если оператор Declare размещается в стандартном программном модуле, то для того, чтобы сделать функцию доступной всему VB­проекту, можно использо­ вать ключевое слово Public. Однако в модулях формы или класса потребуется применить ключевое слово Private, иначе компилятор VB будет «выражать недовольство». Имя для вызова Вместо слова name должно быть указано имя функции, которую вам нужно вызывать из VB­программы. Далее будет разъясняться, почему это имя иногда должно отличаться от того имени, которое в действительности определено в DLL. Чтобы использовать другое имя, следует применить синтаксис Alias, также об­ суждаемый ниже. Важно заметить, что в отличие от VB в именах DLL­функций важен буквенный регистр (это является нормой для языка Visual C++). Экспортируемая библиотека Выражение Lib "libname" используется для указания исходной DLL, кото­ рая экспортирует данную функцию. Для вызова Win32 API источником обычно, но не всегда, является одна из «большой тройки» DLL:  KERNEL32.DLL;  USER32.DLL;  GDI32.DLL. Для этих трех DLL можно опускать расширение в имени библиотеки libname, как это было сделано в примере, помещенном в начале этой главы. Альтернативные имена Как уже упоминалось, есть несколько причин для вызова функций из DLL с присвоением имен, отличающихся от тех настоящих, которые определены в дан­ ной DLL. Для этого предназначено ключевое слово Alias. Ниже приводятся три безусловных причины использовать альтернативные имена (aliases). Замена ключевых слов VB и некорректных символов Некоторые API­функции имеют те же имена, что и функции VB. SetFocus относится именно к такому случаю: HWND SetFocus(HWND hWnd); Нельзя объявить эту функцию в VB как Declare Function SetFocus Lib "user32" (ByVal hwnd As Long) As Long
45 из­за того, что SetFocus является ключевым словом VB. Объявление будет вы­ глядеть следующим образом: Declare Function SetFocusAPI Lib "user32" Alias "SetFocus" ( _ ByVal hwnd As Long) As Long Кроме того, в некоторых API­функциях присутствуют символы, которые не­ льзя использовать в VB. В качестве примера можно привести такие функции для работы с файлами, как _hread, _hwrite, _lclose и т.п ., которые начинаются с символа подчеркивания. Для них создаются альтернативные имена, в которых нет символа подчеркивания. К счастью, не так уж много API­функций, которые совпадают с ключевыми словами VB или содержат некорректные символы. Но иногда совпадения случа­ ются, и использование таких функций в VB без создания альтернативных имен было бы невозможным. ANSI и Unicode API­функции Win32, в которых используются параметры­строки, обычно существуют в двух версиях – одна для ANSI и другая для Unicode. Функция SendMessage, которая была определена в начале этой главы, иллюстрирует имен­ но такой случай. На самом деле есть две разные функции SendMessage, которые определены в библиотеке USER32.DLL. Версия ANSI называется SendMessageA, а версия Unicode – SendMessageW (буква W означает «широкий» от англ. wide). Исполь­ зуя компьютерный жаргон, можно сказать, что функция SendMessage имеет две точки входа (entry points). Чем делить программу на части A и W, имеет смысл сделать выбор только раз, при объявлении функции, и затем во всей программе применять само слово SendMessage без каких­либо добавлений. Это сделало бы более простыми любые изменения в будущем. Как уже говорилось, Windows NT поддерживает Unicode. Следовательно, она реализует для API­функций, в которых есть параметры­строки, точки входа Unicode. Для поддержания совместимости Windows NT реализует также точки входа ANSI, но обычно эти реализации просто выполняют необходимые преобра­ зования, вызывая соответствующие Unicode­версии функций, а результат преоб­ разуют обратно в ANSI. С другой стороны, Windows 9x не поддерживает Unicode, кроме некоторого ограниченного числа случаев. Согласно тому, что утверждает Microsoft, Windows 95 не поддерживает версии Unicode большинства функций Win32, которые прини­ мают параметры­строки. За некоторым исключением, эти функции реализованы в виде заглушек (stub), которые просто успешно завершаются, не внося в аргументы никаких изменений. Это выглядит неосмотрительным. Не лучше ли было бы возвращать значение, отличное от значения, соответствующего успешному завершению, если функция ничего не меняет? В любом случае есть несколько исключений из этого правила. В частности, Windows 9x поддерживает точки входа Unicode в API­функциях, относящихся к OLE, также в небольшом количестве других функций, приведенных в табл. 3.1 . Оператор VB Declare
4 Объявление API-функций Таблица 3.1. API функции с поддержкой Unicode, реализованные в Windows 9x Имена функций EnumResourceLanguages FindResourceEx GetTextExtentPoint EnumResourceNames GetCharWidth lstrlen EnumResourceTypes GetCommandLine MessageBoxEx ExtTextOut GetTextExtentExPoint MessageBox FindResource GetTextExtentPoint32 TextOut Кроме того, в среде Windows 9x реализованы две функции преобразования – MultiByteToWideChar и WideCharToMultiByte. Давайте специально отметим, что в Windows 9x отсутствует Unicode­версия функции lstrlen (которая называется lstrlenW). Она возвращает количество знаков в символьном массиве Unicode, завершающемся нулевым символом. Каким образом данное обстоятельство может быть полезным, будет рассказано несколько позже. Как будет видно из главы 6, лучшее, что может сделать VB­программист, это просто вызывать функции через точки входа ANSI, так как такие функции сов­ местимы и с Windows 9x, и с Windows NT. Объявления типа параметра Третья причина использовать альтернативные имена связана с объявлением типа параметров в операторе Declare. Позже в этой главе данная тема будет исследована более детально. А сейчас достаточно сказать, что вам может понадобится объявить несколько версий одной и той же функции, используя разные типы данных для некоторых из ее параметров. Один из доводов в пользу этого заключается в том, чтобы из­ влечь выгоду из способности VB проверять типы и отслеживать их неправильные объявления до того, как они будут переданы DLL, где, вероятнее всего, приведут к фатальной ошибке «общего нарушения защиты» (GPF). Чтобы это сделать, придется использовать альтернативные имена. Список параметров Элемент синтаксиса arglist является списком параметров и их типов дан­ ных. Его упрощенный синтаксис выглядит так: [ByVal | ByRef] varname[()] [As type] где необязательные в общем случае круглые скобки, стоящие после varname, требуются для объявления переменных, являющихся массивами. Ключевое слово type может иметь одно из следующих значений: Byte, Boolean, Integer, Long, Currency, Single, Double, Date, String (толь­ ко переменной длины), Variant, определенный пользователем или объектный. В действительности строки фиксированной длины могут быть аргументами про­ цедур, но до того как будут переданы функции, они преобразуются в строки пе­ ременной длины.
4 Перевод VC в VB Перевод объявления API­функции из VC++ в VB включает следующие шаги: 1. Узнать название библиотеки (имя DLL и путь) для параметра Lib деклара­ ции VB. 2. Перевести тип возвращаемого функцией значения из типа данных VC++ в тип данных VB. 3. Перевести каждый параметр декларации из VC++ в VB. Этот этап включает выбор типа данных VB и принятие решения о способе передачи параметров ByVal или ByRef. 4. Если необходимо, задать альтернативное имя. Это может быть сделано из соображений удобства описания или по причинам, которые были изложены ранее. Без сомнения, наиболее трудным этапом описанного процесса преобразования является этап перевода типа данных параметров, поэтому давайте поработаем над этим. Передача параметров внешней функции Порядок объявления и передачи параметров внешним функциям DLL и обыч­ ным функциям VB довольно похожи за исключением трех случаев: строк, адресов и структур (пользовательских типов). Однако работа со строками и структурами всего лишь маскирует работу с адресами, поэтому все сводится к адресам, то есть к указателям. Итак, давайте сразу займемся указателями. Строки будут обсуждаться в главе 6, а структуры – в главе 4. Наиболее существенным, что нужно помнить об объявлении и передаче па­ раметров внешним функциям DLL, является то, что работа производится не с одной, а с двумя декларациями – декларацией определения в DLL и декларацией VB в виде оператора Declare. Требуется «подогнать» оператор VB Declare под декларацию определения в DLL. В частности, очень важно не забывать, что любые присутствующие в операторе Declare ключевые слова ByVal и ByRef являются указаниями для Visual Basic, а не для DLL. Пример функции Эти положения можно проиллюстрировать на примере простой внешней функции: short WINAPI rpiAddOne(short *pVar) { return ++(*pVar); } Обратите внимание, что функция принимает в качестве аргумента указа­ тель на тип VC++ short, который совпадает с целым типом в VB. Другими слова­ ми, требуется передать адрес целочисленной переменной. Функция затем просто Перевод VC в VB
4 Объявление API-функций вычислит инкремент переданного целочисленного значения, но это уже не играет роли, поскольку нас интересует только то, как объявить и передать параметр. Теперь остается рассмотреть три варианта. Заметим, что хотя они не являются взаимоисключающими, часто будет существовать возможность выбора из этих вариантов. Вариант 1 В ряде случаев вы будете располагать адресом целочисленной переменной, хранящимся в другой переменной, то есть у вас будет указатель на нее. Это бывает, если адрес получен, например, от другой API­функции. Допустим, он хранится в переменной pTargetVar типа long. В этом случае необходимо передать содер­ жимое данной переменной. Можно использовать следующую VB­декларацию (обратите внимание на альтернативное имя, так как будут и другие версии): Public Declare Sub rpiAddOneByValAsLong Lib "rpiAPI.dll" Alias "rpiAddOne" ( _ ByVal pTargetVar As Long) и затем осуществить следующий вызов: rpiAddOneByValAsLong pTargetVar Вариант 2 Если адреса в явном виде нет, его всегда можно создать, используя функции VarPtr или rpiVarPtr. Предположим, что целое число хранится в переменной iTargetVar. Тогда можно использовать ту же самую декларацию, что и в первом случае, а затем реализовать такой вызов: rpiAddOneByValAsLong VarPtr(iTargetVar) В обоих случаях, как и требуется, вы передаете реально существующий адрес целочисленной переменной. Вариант 3 Если адреса в явном виде не существует и нет желания использовать функ­ ции VarPtr или rpiVarPtr, то можно передать целевую переменную по ссылке, перекладывая на VB заботу об определении адреса переменной и его передачу функции. В конце концов, именно это и означает передачу параметра по ссылке. В таком случае VB­декларация будет другой: Public Declare Sub rpiAddOneByRefAsInteger Lib "rpiAPI.dll" Alias _ "rpiAddOne" ( Var As Integer) Затем делаем вызов: rpiAddOneByRefAsInteger iTargetVar Заметьте, что объявление ByRef является зависимым от типа, в данном случае от типа Integer. Передача параметра как Any Многие API­функции ожидают указатель в качестве аргумента, но не налагают никаких ограничений на тип целевой переменной. Функция rpiVarPtr – одна
4 из них. Чтобы подчеркнуть это качество, можно определить ее в rpiAPI.dll таким образом: void* WINAPI rpiVarPtr(void *pVar) { return pVar; } Объявление параметра void *pVar; говорит о том, что pVar – указатель на void. В VC++ это означает, что тип пере­ менной, на которую ссылается данный указатель, может быть любым. В API­функ­ циях указатели на void встречаются часто. Так как мне было все равно, указатель на какой тип данных передается фун­ кции и так как все указатели являются в VC++ целыми, казалось проще сделать следующее объявление: int WINAPI rpiVarPtr(int pVar) { return pVar; } Суть этого такова: запись декларации VB в виде варианта 3, приведенного выше, с использованием передачи параметра ByRef потребовала бы отдельной декларации (альтернативного имени) для каждого типа данных целевой (переда­ ваемой) переменной. В качестве примера можно привести следующее: Public Declare Function rpiVarPtrByRefAsByte Lib "rpiAPI.dll" _ Alias "rpiVarPtr"( _ ByRef pVar As Byte _ ) As Long Public Declare Function rpiVarPtrByRefAslnteger Lib "rpiAPI.dll" _ Alias "rpiVarPtr"( _ ByRef pVar As Integer _ ) As Long Public Declare Function rpiVarPtrByRefAsLong Lib "rpiAPI.dll" _ Alias "rpiVarPtr"( _ ByRef pVar As Long _ ) As Long Во избежание этого в VB предусмотрена декларация As Any: Public Declare Function rpiVarPtrByRefAsAny Lib "rpiAPI.dll" _ Alias "rpiVarPtr"( _ ByRef pVar As Any _ ) As Long Данная независимая от типа декларация является более гибкой, но у нее есть и недостаток – она отключает контроль типов VB. Это может быть опасным, так как если ошибка в типе данных минует VB и достигнет DLL, то это, скорее всего, приведет к ошибке GPF. Передача параметров внешней функции
50 Объявление API-функций Однако применение As Any может сэкономить время. Некоторые авторы вы­ ступают против этой декларации, но на самом деле все зависит от вас. Будете пользоваться этим осмотрительно – сможете избежать лишней работы. Будете пользоваться этим небрежно – и то время, которое сэкономите, покажется мело­ чью по сравнению с тем, которое потратите, устраняя причины ошибок GPF. Снова ByVal Следует отметить одну малоизвестную особенность ByVal. Можно перео­ пределить установку ByRef по умолчанию для внешней функции – и только для внешней функции – путем включения слова ByVal в вызов функции. Например, рассмотрим функцию CopyMemory: VOID CopyMemory( PVOID Destination, // Указатель на адрес копии. CONST VOID *Source, // Указатель на адрес копируемого блока. DWORD Length // Размер копируемого блока в байтах. ); Первый параметр является адресом переменной, в которую выполняется копи­ рование. (Позже об этой функции будет рассказано подробно, а пока принимайте все как есть.) Далее приводится одно из возможных объявлений функции CopyMemory в Visual Basic: Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"( _ Dest As Any, _ Source As Any, _ ByVal cbCopy As Long) Следующая программа VB скопирует содержимое переменной целого типа iSource в переменную целого типа iDest. Dim iSource As Integer Dim iDest As Integer iSource = 5 CopyMemory iDest, iSource, 2 После выполнения этой программы iDest будет содержать число 5. В качестве альтернативного варианта можно было бы заменить предыдущий вызов функции CopyMemory следующим вызовом: CopyMemory ByVal VarPtr(iDest), iSource, 2 Хотя в данном примере нет причин поступать таким образом, могут быть случаи, когда необходимо передать параметр не по ссылке (по умолчанию), а по значению, но нет желания создавать для этих целей отдельную декларацию VB. Мечта VB-хакера – CopyMemory Чаще будет использоваться всего одна из API­функций – CopyMemory. Ее назначение – простое побайтовое копирование блока данных из одного адреса па­
51 мяти в другой. Это открывает множество новых возможностей, так как сам VB ли­ шен такого рода качеств, за исключением функции LSet в довольно ограниченной форме, но в документации не рекомендуется использовать LSet таким образом. У функции CopyMemory довольно интересная предыстория. В действитель­ ности CopyMemory – это альтернативное имя API­функции RtlMoveMemory. По­видимому, имя CopyMemory впервые употребил Брюс Мак­Кинни (Bruce McKinney), автор книги «Крепкий орешек Visual Basic». Тем не менее CopyMemory присутствует теперь в официальной документации Microsoft в следующем виде: VOID CopyMemory( PVOID Destination, // Указатель на адрес копии. CONST VOID *Source, // Указатель на адрес копируемого блока. DWORD Length // Размер копируемого блока в байтах. ); Ключевое слово CONST означает, что функция CopyMemory гарантирует не­ изменность аргумента Source. Так как PVOID является синонимом VOID *, оба параметра Source и Destination имеют один и тот же тип данных – указатель на целевую переменную, тип которой заранее не известен. Далее приводится самое простое объявление функции CopyMemory в VB: Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"( _ lpDest As Any, _ lpSource As Any, _ ByVal cbCopy As Long) Здесь lpDest – адрес первого байта области памяти, в которую осуществля­ ется копирование, lpSource – адрес первого байта памяти, из которой произ­ водится копирование, и cbCopy – количество копируемых байтов (префикс cb в cbCopy означает количество байтов от англ. count of bytes). В книге часто будет использоваться эта форма объявления, но не следует забывать также и о преимуществах, связанных с возможностью переопределения установки ByRef по умолчанию, путем включения в оператор вызова этой функ­ ции ключевого слова ByVal, как это было описано ранее. Простой пример Давайте рассмотрим пример. Следующая программа побайтно копирует пере­ менную lng типа long в четырехбайтовый массив и затем распечатывает эти байты: Dim lng As Long Dim i As Integer Dim bArray(1 To 4) As Byte lng = &H4030201 CopyMemory bArray(1), lng, 4 Fori=1To4 Debug.Print Hex(bArray(i)); Next Мечта VB-хакера – CopyMemory
52 Объявление API-функций Результат будет таким: 1234 Эта программа демонстрирует три интересных особенности. Во­первых, байты переменной типа long хранятся в памяти, начиная с младшего байта, который имеет самый младший адрес, как показано на рис. 3.1 . 1 2 3 4 Addr Addr+1 Addr+2 Addr+3 Рис. 3.1. Значение &H4030201 переменной типа long, хранящейся в памяти Этот способ хранения машинных слов в памяти называется прямым порядком байтов (little indian). Более подробно с этой темой можно ознакомиться в моей книге Understanding Personal Computer Hardware, опубликованной Springer­Verlag, New York. Во­вторых, адресом переменной типа long является адрес ее младшего байта. В­третьих, приведенная программа – это еще один способ извлечения отдельных байтов из переменных типа integer или long. Более содержательный пример Функция CopyMemory очень полезна для проведения исследований, касаю­ щихся взаимодействий VB с Win32 API. Давайте проиллюстрируем это утверж­ дение небольшим экспериментом со строками. Дана строка VB. Требуется разместить в памяти массив байтов, скопировать байты из строки в массив и затем исследовать байты массива. Ниже приводится программный код. Причина использования оператора ByVal с исходной строкой будет обсуждаться в главе 6, сейчас это не должно вас вол­ новать. Dim sString As String Dim aBytes(l To 20) As Byte Dim i As Integer sString = "help" CopyMemory aBytes(1), ByVal sString, LenB(sString) ' Вывод байтов Fori=1To20 Debug.Print aBytes(i); Next Вывод этой программы следующий: 1041011081120000000000000000
53 Формально строка заканчивается на первом нулевом символе (ASCII 0). За­ метьте, вывод выглядит так, как будто строка представлена в форме ANSI, то есть один байт на символ. Но не торопитесь. Другой способ извлечения отдельных байтов строки VB – объявить перемен­ ную типа long и установить ее на тот же символьный массив, что и саму строку. Более детально этот способ будет рассмотрен в главе 6, а здесь ограничимся такой программой: Dim sString As String Dim aBytes(1 To 20) As Byte Dim i As Integer Dim lng As Long sString = "help" ' Получить содержимое переменной типа BSTR. lng = String(sString) ' Теперь копировать строку в массив. CopyMemory aBytes(1), ByVal lng, LenB(sString) ' Вывод байтов. Fori=1To20 Debug.Print aBytes(i); Next На этот раз вывод будет такой: 1040101010801120000000000000 Это явно Unicode (два байта на символ), что, похоже, противоречит предыду­ щему результату. Как вы увидите в дальнейшем, при передаче строки внешней функции, такой как CopyMemory, VB преобразует строку из Unicode в ANSI. Вот почему первый результат имеет форму ANSI. Однако это преобразование VB можно обойти, как это сделано во втором варианте вызова CopyMemory, полностью отказавшись от передачи VB­строк. Поэтому второй результат имеет форму Unicode. Эта тема более подробно будет изложена в главе 6. А сейчас лишь обратите внимание, насколько полезной может быть функция CopyMemory для проникно­ вения во внутренние механизмы работы VB. Реализация оператора раскрытия ссылки в Visual Basic Как упоминалось в главе 2, CopyMemory предоставляет простой способ реа­ лизации оператора C++ раскрытия ссылки (*). Этот оператор извлекает значение той переменной, на которую ссылается указатель. В частности, предположим, что имеется указатель pVar и нужно извлечь содержимое целевой переменной, что в C++ может быть легко сделано с использованием выражения *pVar. В Visual Basic можно воспользоваться следующим альтернативным вариантом CopyMemory: Мечта VB-хакера – CopyMemory
54 Объявление API-функций Declare Sub VBGetTarget Lib "kernel32" Alias "RtlMoveMemory"( _ Target As Any, _ ByVal lPointer As Long, _ ByVal cbCopy As Long) Для работы с данной функцией нужно объявить переменную того же типа, что и целевая переменная (target): Dim Target As TargetType и сделать вызов: VBGetTarget Target, lPointer, BytesToCopy Поскольку Target передается по ссылке, функция получает адрес этой пе­ ременной. С другой стороны, так как Pointer передается по значению, функция получает адрес самой целевой переменной. Поэтому содержимое целевой пере­ менной размещается в переменной Target. Если вы хотите это проверить, просто выполните следующую программу: Dim Pointer As Long Dim Target As Integer Dim i As Integer i=123 ' Получить указатель. Pointer = VarPtr(i) ' Получить целевую переменную. VBGetTarget Target, Pointer, LenB(Target) Debug.Print Target В качестве альтернативы можно использовать следующие функции из rpiAPI DLL: rpiGetTargetByte rpiGetTargetInteger rpiGetTargetLong rpiGetTarget64 Эти функции определены в DLL таким образом: unsigned char WINAPI rpiGetTargetByte(unsigned char *pByte) { return *pByte; // Возвращает значение целевой переменной типа Byte. } short int WINAPI rpiGetTargetInteger(short int *pShort) { return *pShort; // Возвращает значение целевой переменной типа integer. } int WINAPI rpiGetTargetLong(int *pLong) { return *pLong; // Возвращает значение целевой переменной типа long. }
55 _ int64 WINAPI rpiGetTarget64(_int64 *p64) { return *p64; // Возвращает значение целевой переменной типа long. } Каждая функция принимает указатель на целевую переменную, передаваемый по значению, и возвращает значение целевой переменной. Их объявления в VB выглядят следующим образом: Public Declare Function rpiGetTargetByte Lib "rpiAPI.dll" ( _ ByVal pByte As Long) As Byte Public Declare Function rpiGetTargetInteger Lib "rpiAPI.dll" ( _ ByVal pInteger As long) As Integer Public Declare Function rpiGetTargetLong Lib "rpiAPI.dll" ( _ ByVal pLong As long) As Long Public Declare Function rpiGetTarget64 Lib "rpiAPI.dll" ( _ ByVal p64 As long) As Currency Обсуждение ошибок API Существует два типа ошибок, связанных с вызовом API­функций: ошибки, приводящие или не приводящие к аварийному завершению приложения. Когда программа завершается аварийно Если вызов API­функции завершается ошибкой GPF (см. рис. 3.2), то можно извлечь некоторую полезную информацию из тех адресов памяти, которые по­ казаны в диалоговом окне, но по большей части проблемы приходится решать самим. Рис. 3.2 . Ошибка общего нарушения защиты (GPF) Потребуется выполнить следующие действия, необязательно в указанном по­ рядке:  установите в своей программе контрольную точку на том операторе вызова API­функции, который приводит к GPF. Это может потребовать несколь­ ких проб (и нескольких аварийных завершений) для определения строки, которая приводит к GPF. Внимательно проанализируйте аргументы, пе­ Обсуждение ошибок API
5 Объявление API-функций редаваемые API­функции, чтобы проверить, нет ли там явно неприемлемых значений. Например, равный нулю аргумент, который должен содержать значение адреса или дескриптора, часто приводит к GPF;  проверьте объявление функции на предмет корректного использования ByVal и ByRef. В большинстве случаев причина заключается именно в этом;  проверьте перевод из C++ в VB, чтобы убедиться, что параметры преобра­ зованы правильно. Сообщения Win32 об ошибках Если предположить, что ошибка не привела к аварийному завершению про­ граммы, то в таком случае многие API­функции возвращают какую­либо полезную информацию об ошибке. Это может происходить по­разному. Некоторые функции возвращают нулевое значение в случае успешного завершения и ненулевое зна­ чение в случае неудачи. Ненулевое значение может указывать (или не указывать) на конкретную причину ошибки. Потребуется свериться с документацией, чтобы понять смысл возвращенного значения. Многие API­функции, наоборот, возвра­ щают нулевое значение в случае ошибки и ненулевое – в случае успешного завер­ шения. В этой ситуации возвращаемое значение не поможет отследить причины неудачи. Однако еще не все потеряно. Свойство LastDLLError Несколько сотен API­процедур устанавливают внутренние коды ошибок, до­ ступные через API­функцию GetLastError, синтаксис которой прост: DWORD GetLastError (void); или в VB: Declare Function GetLastError Lib "kernel32" () As Long Однако очевидно, что Visual Basic может видоизменить значение, возвращен­ ное функцией GetLastError, до того как представится возможность его прове­ рить. К счастью, VB сохраняет это значение в свойстве LastDLLError объекта App, и, таким образом, его можно узнать. Однако значение свойства должно быть считано немедленно после вызова функции. Заметьте, что ошибка в API­функции не выводится VB, поэтому приходится писать собственные программы, отслеживающие и перехватывающие ошибки вызова API­функций. Функция FormatMessage API­функция FormatMessage может использоваться для извлечения текста сообщения Win32 по известному коду ошибки. Ниже приводится довольно внушительное объявление функции FormatMessage: DWORD FormatMessage( DWORD dwFlags, // Источник и опции обработки. LPCVOID lpSource, // Указатель на источник сообщения. DWORD dwMessageId, // Идентификатор требуемого сообщения.
5 DWORD dwLanguageId, // Идентификатор языка требуемого сообщения. LPTSTR lpBuffer, // Указатель на буфер сообщения. DWORD nSize, // Максимальный размер буфера сообщения. va_list *Arguments // Адрес вставки массива сообщения. ); Версия VB выглядит так: Declare Function FormatMessage Lib "kernel32" Alias "FormatMessageA" ( _ ByVal dwFlags As Long, _ lpSource As Any, _ ByVal dwMessageId As Long, _ ByVal dwLanguageId As Long, _ ByVal lpBuffer As String, _ ByVal nSize As Long, _ ByVal Arguments As Long _ ) As Long Не будем вдаваться в детали этой сложной функции. Вместо этого просто воспользуемся ею для получения форматированного сообщения об ошибке Win32, как показано ниже на примере функции GetErrorText. Помимо включения этой программы в проекты VB можно было бы создать небольшой дополнительный модуль (add­in), который для ускорения отладки возвращал бы сообщение об ошибке по ее коду. Для того чтобы пользоваться этой функцией, придется включить декларацию VB­функции FormatMessage вместе с соответствующими объявлениями конс­ тант: Public Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000 Public Const FORMAT_MESSAGE_IGNORE_INSERTS = &H200 Public Function GetAPlErrorText(ByVal lError As Long) As String Dim sOut As String Dim sMsg As String Dim lret As Long GetErrorText = "" sMsg = String$(256, 0) lret = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM Or _ FORMAT_MESSAGE_IGNORE_INSERTS, _ 0&, lError, O&, sMsg, Len(sMsg), O&) sOut = "Error: " & lError & "(&H" & Hex(lError) & "): " If lret <> 0 Then ' Проверить завершающий vbcrlf. sMsg = Trim0(sMsg) If Right$(sMsg, 2) = vbCrLf Then sMsg = Left$(sMsg, Len(sMsg)  2) sOut = sOut & Trim0(sMsg) Обсуждение ошибок API
5 Объявление API-функций Else sOut = sOut & "<Нет такой ошибки>" End If GetErrorText = sOut End Function Представим теперь, что только что вызвали API­функцию, которая возвра­ щает нуль в случае ошибки и, в соответствии с документацией, устанавливает GetLastError. Например, следующая программа для получения имени класса окна по его дескриптору (handle) использует функцию GetClassName. Имена классов и дескрипторы будут обсуждаться в главах, посвященных окнам. Эта функция возвращает нуль в случае ошибки: Dim s As String a = String(256, 0) hnd = Command1.hwnd lret = GetClassName(hnd, s, 255) ' Проверка на ошибку. Iflret=0Then MsgBox GetAPIErrorText (Err.LastDllError) End If Если Command1 – это реально существующая уп­ равляющая кнопка, то программа должна выполняться без ошибок. Если заменить Command1.hWnd на нуль, то на экран будет выведено диалоговое окно, изобра­ женное на рис. 3.3 . Мы будем время от времени работать со следующей функцией, которая, использует GetAPIErrorText для выдачи сообщения об ошибке API. Обратите внима­ ние, что она прибавляет достаточно большое число к исходному номеру ошибки, чтобы не было пересечений с другими номерами ошибок: Public Sub RaiseApiError(ByVal e As Long) Err.Raise vbObjectError + 29000 + e, App.EXEName & ".Windows", _ GetAPIErrorText(e) End Sub Теперь, когда вы узнали некоторые общие принципы обработки ошибок API, можно временно забыть об этом, так как в целях экономии места в большинстве примеров, представленных в книге, данные принципы не реализованы. Рис. 3.3. Сообщение об ошибке API
Глава 4. Типы данных Для того чтобы понять Windows API и уметь пользоваться API­функциями, важно глубоко вникнуть в концепцию типа данных (data type). Win32 API использует более тысячи разных типов данных, не считая структур. Для успешного применения Windows API в Visual Basic прежде всего необходи­ мо понять, как преобразуются исходные типы данных Windows в какие­либо типы данных из небольшого количества доступных в Visual Basic. В большинстве случаев это довольно просто, но, как вы увидите позже, есть здесь и свои хитрости, когда дело доходит до беззнаковых типов данных, строк и структур. Что такое тип данных Создается ощущение, что каждый автор, имеющий дело с компьютерной тема­ тикой, по­своему излагает концепцию типа данных. В этой книге выбрана такая форма изложения, которая поможет наиболее эффективно решать задачу преоб­ разования типов из VC++ в VB. Тип данных (или просто тип) – это объект со следующими свойствами:  множеством значений, которое называется базовым диапазоном, или облас­ тью определения (underlying set), этого типа данных;  способом представления этих значений в памяти компьютера. Для типов данных, где представление каждого значения базового диапазона использует одно и то же число битов, это число является размерностью (size). Напри­ мер, в VB каждый элемент данных типа Integer занимает 16 бит памяти, и, соответственно, этот целочисленный тип данных имеет размерность 16 бит и поэтому называется 16­разрядным. С другой стороны, целочисленный тип в VC++ является 32­разрядным;  множеством операций, которые могут выполняться над данными этого типа. Вряд ли понадобиться перечислять эти операции в явном виде;  кроме того, некоторые типы требуют отсылочного (ancillary) типа данных. Например, отсылочным типом для типа указатель на целое (pointer to integer) является целочисленный (integer) тип данных. То есть отсылочный тип для данных типа указатель (pointer) – это тот тип, на который ссылается дан­ ный указатель. Отсылочным типом для массива (array) является тип отде­ льных элементов массива. Здесь не будут описываться массивы, элементы которых могут иметь разные типы данных. Примеры типов данных Чтобы проиллюстрировать данное выше определение, давайте рассмотрим несколько примеров типов данных.
0 Типы данных Тип данных unsigned integer в Win32 В Win32 тип данных unsigned integer (беззнаковое целое) имеет базовый диапазон значений, состоящий из математических целых чисел от 0 до 232–1 вклю­ чительно. Каждое беззнаковое целое представляется 32­разрядным двоичным чис­ лом, следовательно, unsigned integer является 32­разрядным типом данных. Операции этого целочисленного типа данных – хорошо знакомый набор операций двоичной арифметики с обнаружением переполнения, вместе с несколькими дру­ гими группами операций, например логическими операциями. Тип данных integer в Win32 В Win32 тип данных integer (его называют также signed integer – знаковое целое) состоит из множества математических целых чисел в диапазоне от –231 до 231–1 включительно. Каждое такое значение представляется в виде 32­разрядного двоичного числа в дополнительном коде. Если говорить простым языком, представление в виде дополнительного кода для обозначения знака числа использует самый старший двоичный разряд, назы­ ваемый знаковым разрядом (sign bit). У отрицательных чисел знаковый разряд равен единице, а у положительных – нулю. Конечно, это оказывает существенное влияние на те арифметические операции, которые относятся к данному цело­ численному типу, и которые, конечно, отличаются от аналогичных операций с беззнаковым целым типом данных. По­видимому, это нужно кратко пояснить. Сложение беззнаковых целых (то есть элементов с типом данных unsigned integer) происходит так же, как этому учат в начальной школе: 0100 0000 0000 0000 0000 0000 0000 0000 + 0100 0000 0000 0000 0000 0000 0000 0000  1000 0000 0000 0000 0000 0000 0000 0000 Безусловно, это неправильно для знакового типа данных, так как сумма двух положительных чисел (знаковый бит равен нулю) не может равняться отрица­ тельному числу. Поэтому операция сложения чисел для беззнаковых и знаковых типов данных должна определяться по­разному. О знаковых и беззнаковых типах данных и их представлении в дополнитель­ ном двоичном коде более подробно будет рассказано в следующей главе. Тип данных integer в Visual Basic Тип данных integer в Visual Basic отличается от типа данных integer в Win32 (как он реализован, например, в VC++). Тип данных integer в Visual Basic – это 16­разрядный знаковый тип. В частности, диапазон его значений вклю­ чает целые числа от –215 до 215–1, каждое из которых представляется 16­разрядным двоичным числом в дополнительном коде. Тип данных BSTR в Visual Basic Тип данных String (строка), который используется в VB для хранения строк, называется типом данных BSTR. Он представляет собой указатель на массив символов
1 в кодировке Unicode предопределенной размерности, заканчивающийся нулевым байтом (length­preceded, null­terminated array of Unicode characters). Основные и производные типы данных Основной (fundamental) тип данных – это тип, который не выводится из дру­ гих типов. Например, типы данных integer и long в VB являются основными. Производный (derived) тип данных – это тип, который создается на базе основных типов. Ниже представлены некоторые из производных типов:  массивы;  пользовательские (user­defined) типы (далее они будут называться струк- турами, как это принято в C++)1;  указатели. Массивы и указатели имеют отсылочные типы данных, тогда как у структур их нет. Типы данных в Visual Basic Типы данных Visual Basic перечислены в табл. 4.1 . Таблица 4.1. Типы данных VB Тип Размер в байтах Диапазон значений Byte 1 от0до255 Boolean 2 Зарезервированные значения True и False Integer 2 от –32768 до +32767 Long (long integer) 4 от –2147483648 до +2147483647 Single (действительное 4 Приблизительно с одинарной точностью) от –3.4E38 до +3.4E38 Double (действительное 8 Приблизительно с двойной точностью) от –1.8E308 до +4.9E324 Currency (нормированное 8 Приблизительно целое) от –922337203685477.5808 до +922337203685477.5807 Date 8 от 01.01.100 до 31.12.9999 Object 4 String (BSTR) См. главу 6. Строка фиксированной длины 2 байта на каждый символ Основные и производные типы данных 1 Не совсем точно, поскольку в С++ пользовательские типы называются классами, а структура является частным случаем класса. – Прим. науч. ред.
2 Типы данных Таблица 4.1. Типы данных VB (окончание) Тип Размер в байтах Диапазон значений Variant 16 Array Пользовательский (userdefined) тип (или структура – structure) Сделаем несколько замечаний об этих типах данных:  тип данных Boolean (булевский или логический) – это особый 16­разряд­ ный тип данных с базовым множеством значений {True, False}. Однако True (истина) хранится как –1, а False (ложь) как 0;  тип данных Byte в VB является единственным беззнаковым типом дан­ ных;  термин строка (string) употребляется в Visual Basic в нескольких смыслах. Это объясняется в главе 6;  пользовательские (user­defined) типы данных определяются как структуры (это название характерно для VC++);  В Visual Basic нет данных типа указатель, но данные типа BSTR являются указателями. Данные типа Variant Данные типа Variant (универсальный тип) не часто используются при про­ граммировании Win32 API, но встречаются в ситуациях, связанных с OLE, поэ­ тому давайте их кратко обсудим. Тип данных Variant специально предназначен для хранения данных разных типов. Он может хранить данные любого другого VB­типа, за исключением строки фиксированной длины. В это число входят и дан­ ные производных типов – массивы и пользовательские типы. Кроме того, данные типа Variant могут содержать одно из четырех значений особого типа:  Null – указывает на то, что переменная не содержит достоверных данных;  Nothing – специальный объектный тип, свидетельствующий, что перемен­ ная Variant содержит данные объектного типа (это определяет функция VarType), но в данный момент не указывает ни на один из существующих объектов;  Empty – указывает на то, что данной переменной еще не присваивалось никаких значений, то есть переменная неинициализирована (uninitialized);  код ошибки – свидетельствует о том, что содержимое является номером ошибки. Чтобы понять смысл этих значений и универсального типа в целом, следует рассмотреть внутреннее устройство типа Variant. Все данные универсального типа являются 16­разрядными. Первые два байта содержат код, который указывает текущий тип данных, хранящихся в данной пере­ менной Variant. Этот код возвращает функция VarType. Конкретные значения, возвращаемые данной функцией, приведены в табл. 4.2 .
3 Таблица 4.2 . Возвращаемые значения функции VarType Константа Значение Описание vbEmpty 0 Пусто (неинициализировано) vbNull 1 Null (нет достоверных данных) vbInteger 2 Целое vbLong 3 Целое (длинное) vbSingle 4 Число с плавающей точкой одинарной точности vbDouble 5 Число с плавающей точкой двойной точности vbCurrency 6 Деньги vbDate 7 Дата vbString 8 Строка vbObject 9 Объект vbError 10 Код ошибки vbBoolean 11 Булевское значение vbVariant 12 Variant (используется только с массивами элементов типа Variant) vbDataObject 13 Объект доступа к данным (DAO) vbDecimal 14 Десятичное значение vbByte 17 Байт vbUserDefinedType 36 Переменная типа Variant, в которой хранятся данные пользовательского типа vbArray 8192 Массив Функция TypeName возвращает строку описания типа хранящихся в Variant данных. Процедура ShowVariant в листинге 4.1 может быть использована для чтения данных внутри Variant. Листинг 4.1. Процедура ShowVariant Sub ShowVariant() Dim i As Integer Dim aBytes(1 To 50) As Byte Dim v As Variant GoSub PrintVariant Set v = Nothing GoSub PrintVariant v=Null GoSub PrintVariant v = Empty GoSub PrintVariant v = CVErr(123) GoSub PrintVariant v = aBytes GoSub PrintVariant Типы данных в Visual Basic
4 Типы данных v=123 GoSub PrintVariant v = 100000000 Gosub PrintVariant V = "sssssssssssssssssssssssss" Gosub PrintVariant V = CSng(1.1) CoSub PrintVariant v = 10000000000.9 GoSub PrintVariant v = CCur(23.34) GoSub PrintVariant v = CByte(123) GoSub PrintVariant v=True Gosub PrintVariant v = #12/12/1998# GoSub PrintVariant Exit Sub PrintVariant: Dim bVar(1 To 16) As Byte CopyMemory bVar(1), v, 16 Debug.Print VarType(v) & "/"; TypeName(v) & ":"; Fori=1To16 Debug.Print bVar(i) & "/"; Next Debug. Print Return End Sub Вывод программы: 0/Empty: 0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/ 9/Nothing: 9/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/ 1/Null:1/0/0/0/0/0/0/0/0/010/0/0/0/0/0/ 0/Empty:0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/ 10/Error:10/0/0/0/0/0/0/0/123/0/10/128/0/0/0/0/ 8209/Byte():17/32/0/0/0/0/0/0/208/254/31/0/0/0/0/0/ 2/Integer:2/0/0/0/0/0/0/0/123/0/18/0/0/0/0/0/ 3/Long:3/0/0/0/0/0/0/0/0/225/245/5/0/0/0/0/ 8/String:8/0/0/0/0/0/0/0/116/34/29/0/0/0/0/0/ 4/Single:4/0/0/0/0/0/0/0/205/204/140/63/0/0/0/0/ 5/Doub1e:5/0/0/0/0/0/0/0/5l/5l/7/32/95/160/2/66/ 6/Currency:6/0/0/0/0/0/0/0/184/143/3/0/0/0/0/0/ 17/Byte:17/0/0/0/0/0/0/0/123/0/3/0/0/0/0/0/ 11/Boolean:11/0/0/0/0/0/0/0/255/255/3/0/0/0/0/0/ 7/Date:7/0/0/0/0/0/0/0/0/0/0/0/l60/165/225/64/ Обратите внимание: TypeName выводит индикатор того, что данные, содер­ жащиеся в переменной Variant, являются массивом, добавляя пару скобок после имени типа данных элементов массива.
5 Заметьте также, что для того, чтобы установить переменную Variant в значе­ ние Nothing, нужно применить оператор Set, так как Nothing – это специаль­ ного вида объектная переменная. Вам, наверное, известно назначение типа данных Error – предоставить функции возможность возвращать код ошибки, который можно было бы отличить от других возвращаемых значимых чисел. Например, рассмотрим следующую функцию: Function vLength (s As String) As Variant Ifs<>""Then vLength = Len(s) Else ' Преобразовать возвращаемое значение в код ошибки. vLength = CVErr(1) End If End Function Эта функция возвращает значение типа long, указывающее длину непустой строки. Если строка пустая, возвращаемое значение равно единице. Но как отли­ чить в таком случае пустую строку от строки с длиной, равной единице? Ответ содержится в следующем коде: Debug.Print IsError (vLength("help") ) Debug.Print IsError (vLength("") ) Первая команда возвращает False, а вторая – True. Вероятно, вы сможете определить по этому небольшому примеру, как использовать коды ошибок. Если вам интересна эта тема, то в моей книге Concepts of Object-Oriented Programming with Visual Basic, опубликованной Springer­Verlag, New­York, есть целая глава об обработке ошибок. Основные типы данных в VC++ Набор типов данных в VC++ значительно более сложен, чем в VB. Снача­ ла рассмотрим основные типы данных с позиций создателя языка C++ Бьерна Страуструпа и компании Microsoft. Но перед этим давайте обсудим концепцию оператора typedef. Оператор typedef Назначение оператора typedef в VC++ – дать программисту возможность определять новое имя, синоним (synonym) для существующего типа данных. Опе­ ратор имеет следующий синтаксис: typedef typedeclaration synonym; Например, typedef long LPARAM; определяет LPARAM в качестве синонима типа данных long. Можно также объ­ явить одновременно несколько синонимов в одном операторе: typedef long LPARAM, WPARAM; Основные типы данных в VC++
 Типы данных В Visual Basic отсутствует непосредственный эквивалент оператора typedef VC++. Теперь можно продолжить обсуждение типов данных VC++. Символьные типы данных Из основных типов данных наиболее сложны символьные. Эта сложность обусловлена тем, что подобные типы используются для представления символов, которые поддерживает операционная система, а Windows в общем случае подде­ рживает два набора символов – ANSI и Unicode. Позиция разработчика C++ Тип char создан для хранения символов из основного набора, используемого операционной системой. Типы данных char, signed char и unsigned char различаются, хотя и могут быть реализованы компилятором одинаково. Каждому из них выделяется один и тот же объем памяти. Позиция Microsoft В документации указывается, что char – это интегральный тип, который обыч­ но содержит элементы набора символов исполнительной системы. В Microsoft C++ к данному типу относится ASCII. Компилятор C++ интерпретирует пере­ менные типов char, signed char и unsigned char как имеющие разный тип. По умолчанию тип char – знаковый (но это положение может быть изменено установкой ключей компилятора). Поэтому будем интерпретировать char как однобайтовый знаковый тип дан­ ных, эквивалентный signed char, с диапазоном значений от –128 до 127. С другой стороны, unsigned char – это однобайтовый тип данных с диапазоном значений от 0 до 255. Из этого следует, что unsigned char более подходит для представления ASCII­ или ANSI­символов. Подобное может удивить: почему Microsoft для типа char не выбрала интерпретацию по умолчанию как unsigned? Во всяком случае, Microsoft определяет тип данных BYTE как синоним unsigned char. Символьные типы Unicode Теперь рассмотрим, что сделано Microsoft в отношении символов Unicode. В VC++ есть еще один тип данных, который называется wchar_t, 16­разряд­ ный беззнаковый тип целых для хранения «широких» символов Unicode. Таким образом, char и wchar_t оперируют двумя типами символов Win32. В VC++ определены также несколько синонимов для этих двух символьных типов данных. typedef char CHAR; typedef wchar_t WCHAR; Более того, чтобы дать программисту возможность писать одну и ту же програм­ му для систем с окружением ANSI и Unicode, в VC++ определен родовой (generic) тип данных TCHAR, использующий следующую схему условной компиляции:
 #ifdef UNICODE typedef WCHAR TCHAR; typedef WCHAR TBYTE; #else typedef char TCHAR; typedef unsigned char TBYTE; #endif Условная компиляция работает в VC++ так же, как и в VB. Если константа условной компиляции UNICODE определена в программе, то будет задействован первый набор операторов typedef: typedef WCHAR TCHAR; typedef WCHAR TBYTE; Таким образом, TCHAR и TBYTE являются синонимами типа символов Unicode WCHAR, который, в свою очередь, является синонимом wchar_t. И наоборот, если константа UNICODE не определена, то TCHAR и TBYTE будут синонимами CHAR, которая, со своей стороны, является синонимом char. В итоге имеем следующие типы символьных данных (вместе с их беззнако­ выми версиями):  char и CHAR – символ ANSI;  wchar_t и WCHAR – символ Unicode;  TCHAR и TBYTE – родовой символ, может быть символом либо ANSI, либо Unicode (но не тем и другим одновременно). Типы данных int Тип данных integer способен выражать целые числа в диапазоне, определя­ емом количеством выделенных под его хранение байтов. Язык C++ поддерживает несколько целочисленных типов данных. Определение разработчика C++ Возможны три размера у целочисленных типов данных: они объявляются как short int, int и long int. Используя оператор sizeof для определения количества битов, занимаемых типом данных в памяти, получаем следующий результат: sizeof (short int) <= sizeof (int) <= sizeof (long int) Однако обратите внимание на знаки равенства, которые в принципе допуска­ ют, что у этих трех типов данных может быть один и тот же размер. Тип int имеет естественный размер, определяемый машинной архитектурой. В противном случае вопрос о размере зависит от компилятора. Нередко int имеет тот же размер, что и short int или long int. Следующие выражения эквива­ лентны: Основные типы данных в VC++
 Типы данных short int = short = signed short int = signed short int = signed int long int = long = long short int = long short Для каждого из типов – short int, int и long int имеются соответству­ ющие беззнаковые версии, которые имеют такой же размер, что и их знаковые эквиваленты. Выражения unsigned int и unsigned являются синонимами. Определение Microsoft Так же, как и в случае с символьными типами данных, Microsoft следует опи­ санию разработчика C++, устанавливая следующие размеры для Win32: sizeof (short) = 16 sizeof (int) = 32 sizeof (long) = 32 Все типы данных char и int со всеми их вариациями рассматриваются как целочисленные (integral) типы. Типы данных с плавающей точкой Тип данных с плавающей точкой допускает представление чисел с дробной частью, определяемое их форматом и количеством выделяемых байтов. Определение разработчика C++ Существует три типа данных с плавающей точкой: float, double и long double. Каждый тип в указанной последовательности имеет, по крайней мере, такую же точность, как его предшественник, или выше. Определение Microsoft Microsoft также определила указанные три типа данных с плавающей точкой с размерами sizeof(float) = 4 sizeof(double) = 8 sizeof(long double) = 8 и установила, что представление long double и double идентично. Тем не менее, согласно Microsoft, long double и double являются независимыми типами. В этой книге такие нецелочисленные типы данных почти не используются. Другие типы данных Давайте кратко познакомимся с некоторыми другими часто встречающимися типами данных. Тип данных Void И разработчик C++, и Microsoft определяют тип данных void как специаль­ ный тип, который может использоваться в качестве возвращаемых функциями значений, но никакая переменная не может быть объявлена как имеющая тип void. Однако этот тип также применяется для объявления указателя на данные, тип которых не известен, как в следующем примере: void *pWhatever;
 Тип данных FARPROC Тип данных FARPROC появляется достаточно часто в функциях Win32 API. В документации он описан как «указатель на функцию обратного вызова». Функции обратного вызова (callback function) будут обсуждаться в главе 15, а сейчас главное, что FARPROC – указатель, а значит, 32­разрядный целый тип unsigned long. Дескрипторный тип В Windows много объектов разных типов, таких как окна, картинки, шрифты, курсоры, меню, ловушки, метафайлы, перья, кисти и т.д . Когда создается объ­ ект какого­либо типа, Windows обычно возвращает его дескриптор (handle). Де­ скриптор используется для программного управления тем объектом, которому он принадлежит. Это значит, что большинству API­функций, которые управляют каким­нибудь объектом, требуется его дескриптор. Нельзя сказать, что дескрипторы для VB­программистов есть нечто абсолютно неведомое. Можно получать доступ к дескрипторам многих объектов, используя их свойство hWnd. Тип HANDLE является синонимом void*, хотя для типа, который так часто встречается, можно было бы назначить основной тип данных. Тип данных Boolean В VC++ определены также некоторые булевские (boolean), или логические, типы данных:  bool – однобайтовый тип данных, который принимает два значения – true и false (оба слова, так же как и в VB, являются ключевыми словами);  BOOL – 32­разрядный тип данных, который также принимает значения true и false (следует напомнить, что в VC++ регистр имеет значение);  boolean и BOOLEAN – еще один 32­разрядный тип данных, который прини­ мает значения true и false. Резюме В табл. 4.3 объединена информация о стандартных (основных и близких к основным) типах данных VC++, с которыми вы будете часто встречаться, изучая объявления функций Win32 API. Таблица 4.3. Стандартные типы данных VC++ Тип Размер Ближайший Диапазон значений/ в байтах эквивалент в VB Примечания char 1 Byte От –128 до 127 signed char 1 Byte От –128 до 127 unsigned char 1 Byte От0до255 BYTE 1 Byte От0до255 wchar_t 2 Integer От 0 до 65535 TCHAR 1или2 Byte/Integer См. раздел «Символьные типы данных» (signed) short (int) 2 Integer От –32768 до 32767 Основные типы данных в VC++
0 Типы данных Таблица 4.3. Стандартные типы данных VC++ (окончание) Тип Размер Ближайший Диапазон значений/ в байтах эквивалент в VB Примечания unsigned short (int) 2 Integer От 0 до 65535 (signed) int 4 Long От –2147483648 до 2147483647 unsigned int 4 Long От 0 до 4294967295 (=232–1) (signed) long (int) 4 Long От –2147483648 до 2147483647 unsigned long 4 Long От 0 до 4294967295 (=232–1) float 4 Long Приблизительно от –3.4E38 до 3.4E38 double 8 Double Приблизительно от –1.8E308 до 4.9E324 long double 8 Double Приблизительно от –1.8E308 до 4.9E324 (используется нечасто) bool 1 Byte true или false BOOL 4 Long true или false boolean 4 Long true или false * 4 Long Обозначает указатель void* 4 Long Указатель на неопределенный тип HANDLE 4 Long Дескриптор объекта. Официально void* FARPROC 4 Long Указатель на функцию обратного вызова (callback function) _int8, _int16, 1,2,4,8 Byte/Integer/ Специфицирован в документации. _int32, _int64 Long/none Используется весьма редко LONGLONG 8 Double 64разрядное знаковое целое. Используется нечасто VARIANT 16 Variant Схож с типом Variant, имеющимся в VB Как видно из табл. 4.3, не составляет особой проблемы преобразовать стан­ дартные типы данных VC++ в типы данных VB. Основная трудность заключа­ ется в том, что в VB нет беззнаковых вариантов основных типов. Эта тема будет подробнее обсуждаться в следующей главе, там, где речь идет о сути знакового (дополнительный двоичный код) и беззнакового представлений чисел. Преобразование производных типов данных Казалось бы, если вам уже известно, как переводить основные типы данных из VC++ в Visual Basic, то перевод производных типов данных должен быть более или менее рутинным делом. Это верно, но с одним­двумя исключениями. В конце концов, так как указатели имеют размер 32 бита, указатель VC++ нужно преобразовывать в тип VB long. И уже не имеет значения, на какой тип данных ссылается преобразуемый указатель, по крайней мере, до тех пор, пока речь идет о преобразовании только указателя. Естественно, может потребоваться преобразовать и тот тип данных, на который указатель ссылается.
1 Массив в VC++ является массивом и в VB, в этом случае требует перевода только отсылочный тип данных. Однако возникает небольшая проблема, когда дело доходит до преобразования структур VC++ в структуры VB (пользовательские типы). Немного позже тема вы­ равнивания (alignment) элементов структуры будет рассмотрена более детально. Операторы typedef в Win32 Хотя основных типов данных в VC++ насчитывается около дюжины, иногда выглядит удручающим то обстоятельство, что Win32 определяет буквально сотни синонимов этих типов данных с помощью операторов typedef. В VC++ разнообразные операторы typedef содержатся в коллекции из более чем 300 включаемых (include) файлов. Это текстовые файлы, название которых обычно имеет расширение .h. Утилита rpiAPIData В архиве, содержащем примеры программ к данной книге, имеется прило­ жение с базой данных, которое называется rpiAPIData. Оно может отображать информацию о типах данных Win32, как показано на рис. 4.1 . Утилита rpiAPIData является не более чем внешним интерфейсом базы данных, содержащей объявления синонимов типов в Win32. С ее помощью можно находить в алфавитно­упорядоченной мешанине переименованных типов тот основной тип данных VC++, который наиболее точно соответствует данному синониму. База дан­ ных этого приложения включает более чем 650 объявлений переименования типа. Рис. 4 .1. Просмотр информации о типе с помощью приложения rpiAPIData Операторы typedef в Win32
2 Типы данных Следует повторить, что в отличие от VB, VC++ и Win32 являются средами, в которых важное значение имеет регистр символов. Необходимо учитывать это обстоятельство, иначе неизбежны ошибки. Нужно также обратить внимание на то, что include­файлы содержат сотни операторов typedef, информация о которых не вошла в базу данных приложе­ ния rpiAPIData. Тем не менее нет необходимости заносить в каталог сведения о каждом из них отдельно, их достаточно легко узнать и без этого. Во­первых, многие операторы typedef используются для объявления двух версий, ANSI и Unicode, одного и того же типа данных. Например, ключевые слова OPENFILENAMEA и OPENFILENAMEW являются соответственно ANSI­ и Unicode­версиями одного и того же типа данных (в данном случае, структуры). Следовательно, имеются два оператора typedef, объявляющих OPENFILENAME (так же, как и в случае с операторами typedef TCHAR, о которых говорилось выше в разделе «Символьные типы данных»). Во­вторых, особенность Win32 – объявлять новые синонимы типа данных для указателей путем добавления префиксов P, NP или LP к имени исходного типа данных. В качестве примера рассмотрим тип данных int. Win32 объявляет следующие операторы typedef: typedef int* PINT; typedef int* LPINT; Это происходит с сотнями различных стандартных типов данных. Не стоит включать все такие вариации, пытаясь сделать приложение rpiAPIData более удобным. Префиксы NP и LP обозначают соответственно ближний указатель (near pointer) и дальний указатель (far, long pointer). Эта терминология – след, остав­ ленный эпохой 16­разрядной Windows и процессорами Intel 80286. Тогда адреса памяти записывались в сегментном (segment:offset, сегмент:смещение) формате, чтобы приспособиться к 16­разрядным регистрам процессора, и следовательно, могли адресовать только 216 = 64 Кб памяти. Ближний указатель использовался в пределах одного 64­килобайтового сегмента памяти, поэтому требовал хранения только величины смещения. Дальний указатель ссылался на другой сегмент па­ мяти. Если эта терминология вам незнакома – считайте, что вам повезло. (Более подробно обо всем этом написано в моей книге Understanding Personal Computer Hardware, опубликованной Springer­Verlag, New­York.) Разработчикам VC++, похоже, нравится создавать объявления typedef, ис­ пользуя другие объявления typedef. Например, в двух разных include­файлах можно найти следующее: typedef unsigned long DWORD; и typedef DWORD LPARAM; Это не представляет проблемы для программистов VC++, но создает про­ блему для программистов VB. В конце концов, программист VC++ может не волноваться по поводу того, какой размер имеют данные типа LPARAM, потому
3 что он просто пользуется самим ключевым словом LPARAM, как в следующем выражении: LPARAM lparam; Однако программистам VB необходимо заменить тип LPARAM подходящим типом данных VB, следовательно, нужно знать размер этого типа данных. Поэтому, с точки зрения программиста VB, было бы хорошо, если бы все объяв­ ления typedef записывались в терминах некоторого небольшого набора стандар­ тных типов данных. Вместо этого приходится ввязываться в погоню за typedef. Поле Origin на рис. 4.1 предназначено для вывода информации о конечном (более или менее) пункте этой погони, то есть о стандартном типе из табл. 4.3, для кото­ рого данное имя является синонимом. Учтите, что для утилиты rpiAPIData это первое воплощение и она несомненно содержит много ошибок. Риск за счет покупателя! Пожалуйста, пришлите мне по электронной почте сообщение, если найдете ошибки. Зачем такое множество typedef В том, что Microsoft использует так много объявлений typedef, есть своя логика (хотя кто­то, возможно, возразит, что такая ситуация сложилась скорее случайно). Проще говоря, предназначение многочисленных typedef заключается в том, чтобы сделать текст программы более понятным. (Последнее слово в данном случае всегда остается за вами.) Например, в соответствии с документацией Win32, тип LPARAM является прос­ то типом unsigned long, но с особой миссией – это тип параметра 32­разрядного сообщения. Если быть более точным, то LPARAM появляется в декларации API­ функции SendMessage: LRESULT SendMessage( HWND hWnd, // Дескриптор окнаприемника сообщения. UINT Msg, // Сообщение для передачи. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam // Второй параметр сообщения. ); Так, например, объявление переменной VC++ LPARAM avar; сообщает, что переменная avar предназначена для хранения параметра 32­раз­ рядного сообщения для применения в SendMessage. Напротив, из альтернативы такому объявлению: unsigned long avar; ничего не ясно о запланированном применении переменной avar. Дело в том, что typedef с LPARAM придает смысл, или интерпретацию, любой переменной, объявляемой с использованием этого синонима. Иных отличий в этих двух декларациях нет: LPARAM avar1; unsigned long avar2; Операторы typedef в Win32
4 Типы данных Если два типа данных связаны оператором typedef: typedef DWORD LPARAM; то можно говорить о них как о синонимичных типах данных. Пример преобразования функции SendMessage На протяжении всей этой книги у вас еще будет много возможностей попрак­ тиковаться в преобразовании типов данных, а сейчас давайте ненадолго прервемся, чтобы выполнить небольшой пример – преобразование функции SendMessage: LRESULT SendMessage( HWND hWnd, // Дескриптор окнаприемника сообщения. UINT Msg, // Сообщение для передачи. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam // Второй параметр сообщения. ); Так как из документации Win32 следует, что SendMessage экспортируется USER32.DLL, и так как сейчас нужна версия ANSI, то декларацию можно частич­ но преобразовать следующим образом: Declare Function SendMessage Lib "user32" Alias " SendMessageA" ( _ HWND hWnd, _ UINT Msg, _ WPARAM wParam, _ LPARAM lParam) _ As LRESULT Далее следует преобразовать типы данных этой декларации из VC++ в VB и определиться с использованием ByVal или ByRef для каждого параметра. Значения полей Origin и VB Declaration утилиты rpiAPIData для каждого типа данных приведены в табл. 4.4 . Таблица 4.4 . Соответствие типов данных VC++, VB и основных типов для параметров APIфункции SendMessage Тип VC++ Origin Тип VB LRESULT long Long HWND void* Long UINT unsigned int Long WPARAM unsigned int Long LPARAM long Long Далее по документации нужно определить значение преобразуемых парамет­ ров функции. В соответствии с документацией, параметр hWnd хранит дескриптор окна­получателя сообщения. Так как VB заботливо предоставляет этот дескриптор в качестве возвращаемого значения свойства hWnd, следует передавать этот пара­ метр по значению (by value): SendMessage(lstWhatever.hWnd, . . .
5 Параметр Msg является константой, которая идентифицирует данное сообще­ ние (ID сообщения). Его тоже надо передать по значению. Значения параметров WPARAM и LPARAM являются контекстно­зависимыми, поэтому документация по SendMessage в вопросе о том, как передавать эти пара­ метры – по значению или по ссылке, – не слишком поможет нашим рассуждениям. Это может зависеть от самого передаваемого сообщения. Итак, приходим к следующему объявлению для SendMessage в VB: Declare Function SendMessage Lib "user32" Alias " SendMessageA" ( _ ByVal hwnd As Long, _ ByVal lMsg As Long, _ wParam As Any, _ lParam As Any _ ) As Long Как это часто бывает при использовании данной функции, в конкретных ситу­ ациях можно проявить большую точность в объявлении типа параметров wParam и lParam. Например, для поиска элемента в окне списка нужно послать сообщение LB_ FINDSTRING, то есть переменная wMsg должна принять значение символьной константы LB_FINDSTRING. Как определить значение символьной константы, вы узнаете в главе 16. В этом случае wParam равен индексу того элемента окна списка, с которого начинается поиск, или равен –1, если поиск начинается с начала списка. Ясно, что требуется передать это число по значению. lParam – это также строка поиска. Строки будут рассматриваться в следующей главе, а пока заметим, что подходя­ щим выбором для этого случая будет ByVal lParam As String Таким образом, для пересылки сообщения LB_FINDSTRING (и для огромного множества других LB­сообщений) можно пользоваться такой декларацией: Declare Function SendMessageByString Lib "user32" Alias " SendMessageA" ( _ ByVal hwnd As Long, _ ByVal lMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As String _ ) As Long Если lParam имеет тип long, используйте следующее объявление: Declare Function SendMessageByLong Lib "user32" Alias " SendMessageA" ( _ ByVal hwnd As Long, _ ByVal lMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long _ ) As Long Структуры и пользовательские типы Как уже упоминалось ранее, возможна небольшая проблема, когда дело дохо­ дит до преобразования структур VC++ в пользовательские типы VB. И хотя она Преобразование функции SendMessage
 Типы данных возникает очень редко, важно понимать ее причину, особенно потому, что Win32 использует буквально сотни структур в объявлениях API­функций. Изначально можно подумать, что это просто задача преобразования каждого члена структуры из VC++ в VB. Например, структуру VC++ struct tagMisaligned { char aByte; short int anInteger; } можно преобразовать так: Type utMisaligned aByte As Byte anInteger As Integer End Type Но, к сожалению, здесь и возникает проблема: размещенная в памяти структу­ ра реально занимает больше места, чем это может показаться на первый взгляд. То есть появляется еще одно потенциально опасное в плане ошибок перевода типов место. Поэтому необходимо учесть, что когда Visual Basic сохраняет структуру в памяти (и только в памяти, это не касается хранения на диске), он выравнивает члены структуры на те границы, которые называются их естественными грани- цами (natural boundaries). Это очень просто: естественная граница переменной, которая занимает n байт памяти – это любой адрес памяти, кратный n. Так, нужно запомнить следующее:  переменная размером один байт может размещаться с любого адреса в па­ мяти;  переменная типа integer размещается с адресов памяти, кратных 2;  переменная типа long размещается с адресов памяти, кратных 4;  переменная типа double размещается с адресов памяти, кратных 8;  переменная типа указатель, включая строковый тип VB BSTR, размещается с адресов памяти, кратных 4;  символы Unicode размещаются с адресов памяти, кратных 2. В частности, это относится к символам строки с фиксированной длиной. Кроме того, вся структура выравнивается на те адреса памяти, которые делятся на 4, но это уже к сути дела не относится. Давайте проверим сказанное на такой структуре: Public Type utMisaligned aByte As Byte aLong As Long anInteger As Integer aString As String End Type Следующая программа создает побайтовую копию указанной структуры в том виде, в каком она представлена в Windows.
 Dim i As Integer Dim aBytes(1 To 50) As Byte Dim ma As utMisaligned ma.aByte = &H11 ma.aLong = &H22334455 ma.anInteger = &H6677 ma.aString = "help" CopyMemory aBytes(1), ma, LenB(ma) ' Распечатать длину ma, используя и Len и LenB. Debug.Print "Len:" & Len(ma), " LenB:" & LenB(ma) ' Распечатать массив байтов. For i = 1 To LenB(ma) Debug.Print Hex(aBytes(i)) & "/"; Next Debug.Print Вывод программы: Len:11 LenB:16 11/0/0/0/55/44/33/22/77/66/0/0/B4/4A/20/0/ Результаты показывают, что, во­первых, Len(ma)и LenB(ma)возвращают разные значения. Как указано в документации VB, Len(ma)возвращает коли­ чество байтов, занимаемых самой структурой, которое равно 1 + 4 + 2 + 4 = 11 (строка VB является 4­байтовым указателем). С другой стороны, LenB(ma) возвращает количество байтов, выделяемых для размещения данной структуры в памяти, которое, в данном случае, равно 16. Глядя на массив байтов, вы видите следующее:  1 байт отводится под ma.aByte;  3 байта смещения требуются для выравнивания ma.aLong на естественную границу;  4 байта отводится под ma.aLong;  2 байта требуются под ma.anInteger, которая не нуждается в дополни­ тельном выравнивании, так как попадает на свою естественную границу;  2 байта смещения нужны для выравнивания ma.aString на естественную 4­байтовую границу;  4 байта отводится под ma.aString, являющуюся адресом. К счастью, в подавляющем большинстве структур API­члены выровнены на их естественные границы, так что пользовательские типы VB являются точным отра­ жением структур VC++. В тех редких случаях, когда члены структур VC++ не вы­ ровнены на естественные границы, следует каким­то образом делать это «вручную». Так как такие случаи бываю редко, проще пользоваться для данных целей готовыми программами. Альтернативой этому может быть создание массива байтов и его ак­ куратное заполнение соответствующим образом расположенными данными. Структуры и пользовательские типы
 Типы данных Флаги Флаг (flag) – это двоичное слово, которое отражает состояние некоторого объекта. Часто флаги бывают однобитовыми. Например, для определения того, является ли окно активным, требуется всего один бит – нуль для неактивного и единицу для активного. С другой стороны, некоторым флагам требуется больше одного бита. Как вам, конечно, известно, флаг, состоящий из n бит, может отобра­ жать любое из 2n состояний. В Win32 несколько флагов часто упаковывают в одно 16­ или 32­разрядное слово, которое обычно определяют как unsigned short или unsigned int со­ ответственно. Часто все такое слово называют флагом. Поэтому важно иметь возможность извлекать отдельные биты флага. Это несложно сделать как в VC++, так и в VB. Сначала немного о терминологии. Если числовую переменную рассматри­ вают как флаг, то относятся к ней как к двоичному слову (binary word). О таком слове говорят также, как о слове со значимыми битами (bit­significant). Наименее значимый бит – крайний справа, который также называется битом 0. (Термин «наименее значимый» теряет свой смысл для слова­флага из значимых битов, но терминология продолжает использоваться.) Побитовое маскирование Логические операторы Visual Basic And, Or и Not действуют также как по­ разрядные операторы, в том смысле, что примененные к числовым типам данных, они выполняют поразрядную конъюнкцию и дизъюнкцию соответственно. В час­ тности: 0And0=0 0And1=0 1And0=0 1And1=1 0Or0=0 0Or1=1 1Or0=1 1Or1=1 Not0=1 Not1=0 Эти операторы могут использоваться для установки (set) или очистки (clear) любого разряда двоичного слова. Для установки двоичного разряда следует вы­ полнить поразрядную операцию Or с логической 1. Для очистки двоичного раз­ ряда – поразрядную And с логическим 0. Например, если x – целое, можно установить его шестой двоичный разряд, записав такую строку: x Or 100000(binary)
 Безусловно, VB не понимает двоичных слов, поэтому придется выбирать между x Or &H20 и xOr2^5 Также легко можно извлекать биты: FifthBit = Iif((x And &H20) = 0, 0, 1) Возможно, выбор подходящего числа для маски может оказаться немного уто­ мительным делом. Ниже приведены несколько советов, в которых используются данные табл. 4.5:  маску легче конструировать в шестнадцатеричном коде;  чтобы создать маску для очистки битов, например, 16­разрядного слова, начните с маски из единицы во всех двоичных разрядах: (1111)(1111)(1111)(1111) Потом замените 1 на 0 в тех разрядах, которые вы хотите очистить. Напри­ мер, для очистки разрядов 0, 2, 5 и 10 используйте такую маску: (1111)(1011)(1101)(1010) Преобразуйте это число с помощью табл. 4.5 в шестнадцатеричный код. Это дает &H0FBDA, следовательно, для очистки заданных разрядов нужно запи­ сать: x = x And &H0FBDA  чтобы создать маску для установки битов, например, 16­разрядного слова, начните с маски из 0 во всех двоичных разрядах: (0000)(0000)(0000)(0000) Потом замените 0 на 1 в тех разрядах, которые вы хотите установить. На­ пример, для установки разрядов 1, 7, 8 и 15 используйте такую маску: (1000)(0001)(1000)(0010) Преобразуйте это число с помощью табл. 4.5 в шестнадцатеричный код. Это дает &H8182, следовательно, для установки заданных разрядов нужно записать: x = x Or &H8182  если вам нужно установить одни и очистить другие двоичные разряды, сде­ лайте это в два этапа. Таблица 4.5 . Перевод чисел из двоичного кода в шестнадцатеричный Hex Binary 0 0000 1 0001 2 0010 3 0011 Флаги
0 Типы данных Таблица 4.5 . Перевод чисел из двоичного кода в шестнадцатеричный (окончание) Hex Binary 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001 A 1010 B 1011 C 1100 D 1101 E 1110 F 1111 Символьные константы В Win32 множество символьных констант – больше 6000. Они определяются в include­файлах (файлы с расширением .h) с помощью операторов typedef. На­ пример, include­файл Winbase.h содержит следующие определения констант: #define MAX_COMPUTERNAME_LENGTH 15 Это определение эквивалентно следующему определению в VB: Public Const MAX_COMPUTERNAME_LENGTH = 15 Программисты VC++, включая подходящие include­файлы в свои программы с помощью оператора #include <Winbase.h> и дальше могут ссылаться на константы по имени. В VB необходимо добавлять оператор Const для каждой константы, которую требуется использовать. Однако проблема тут в том, что в документации нет значений этих констант – програм­ мистам VC++ они просто не нужны. Следовательно, программистам VB пригодится хорошая утилита текстового поиска для поиска нужных значений в include­файлах. Например, результатом по­ иска строк, которые содержат и #define, и MAX_COMPUTERNAME_LENGTH, будет строка с декларацией этой константы. Еще одна проблема возникает в связи с описанным методом поиска, так как данный оператор #define может оказаться частью выражения условной компи­ ляции и придется проверять все выражение, чтобы удостовериться, что найдено правильное значение.
Глава 5. Знаковые и беззнаковые типы данных В этой главе вы ближе познакомитесь со знаковыми и беззнаковыми типами данных. Указанная тема имеет особое значение, так как беззнаковые типы данных широко используются в Win32 API и полностью отсутствуют в Visual Basic, что нередко при­ водит к возникновению проблем при переводе чисел без знака в соответствующие числа со знаком. Знаковое и беззнаковое представления Как вам уже известно, VC++ и Win32 API используют и знаковые, и беззна­ ковые целочисленные типы данных, тогда как в VB есть только один беззнаковый тип – Byte. Это может создавать некоторые проблемы при передаче в API­фун­ кции беззнаковых типов данных и при возвращении этих значений из функций. Чтобы понять, с чем это связано, необходимо познакомиться с внутренним уст­ ройством этих типов и с тем, как выглядит их представление в памяти. Уместно будет начать с уточнения терминологии. Все примеры сформулиро­ ваны в выражениях 16­разрядных слов, но это применимо также к словам любой длины. 16­разрядное слово представляет собой строку из 16 двоичных бит: w = 1111000011110000 Самое главное, двоичное слово – это не число, до тех пор пока оно не проин­ терпретировано как число. До этого оно является просто строкой битов. В большинстве компьютеров, включая ПК, существуют два основных способа представления целых чисел в виде 16­разрядных слов. Беззнаковое представление (unsigned representation) используется только для представления неотрицательных целых чисел, а знаковое представление в дополнительном двоичном коде (two’s complement signed representation) – для представления и отрицательных, и неотри­ цательных целых чисел. На последнее представление в документации Microsoft обычно сокращенно ссылаются как на знаковое представление, и мы будем поступать точно так же, хотя имеются и другие виды знаковых представлений (включая знаковые представления в обратном и прямом кодах). Важно понять, что знаковым или беззнаковым является не само число, а его представление как числа в виде двоичного слова. Числа сами по себе – ни знако­ вые, ни беззнаковые. (Число может быть положительным, отрицательным или нулем, но это не то же самое, что быть знаковым или беззнаковым, так как у всех чисел есть какой­нибудь знак.) Поэтому вводит в заблуждение общепринятый
2 Знаковые и беззнаковые типы данных термин «знаковое целое». Его следовало бы читать как «знаковое представление целого числа». Тем не менее эта общепринятая терминология очень удобна, поэ­ тому в книге она тоже используется. Когда в Visual Basic объявляют переменную целого типа и присваивают ей значение Dim i As Integer i=5 VB представляет целое число, используя знаковое представление в дополнитель­ ном двоичном коде. Тут у программиста нет выбора. Другими словами, VB интер­ претирует двоичное слово как целое число, используя двоичное представление этого слова в дополнительном коде. VC++ является более гибким, позволяя вы­ бирать представление: unsigned int ui; ui = 65000; int i; // Или signed int i; i = 30000; Здесь ui – это беззнаковое целое, то есть VC++ содержит ui в памяти в виде 32­разрядного двоичного слова с беззнаковым представлением, в то время как i – знаковое целое, то есть i хранится в виде знакового представления в допол­ нительном коде. Зачем нужны два разных представления Довод в пользу беззнакового представления целых чисел прост: его использо­ вание позволяет представлять положительные целые числа в большем диапазоне, чем при знаковом представлении. Но при этом утрачивается возможность работы с отрицательными числами. В частности, 16­разрядное слово, использующее знаковое представление в дополнительном двоичном коде, может представлять любое целое число в диапа­ зоне от –32768 до 32767, тогда как 16­разрядное слово, которое применяет беззна­ ковое представление, может представлять целые числа в диапазоне от 0 до 65535. Существуют убедительные доводы за включение в язык программирования и знаковых, и беззнаковых представлений. По­видимому, не стоит комментировать тот факт, что язык программирования был бы сильно ограничен, если бы не мог представлять отрицательные числа. Но с другой стороны, беззнаковое представ­ ление полезно по следующим двум причинам:  если необходимо выполнять арифметические операции только с положи­ тельными числами, то при беззнаковом представлении диапазон возможных значений больше. Это касается, например, адресов. Если размер машинного слова, скажем, 32 разряда, тогда наиболее естественный способ, предостав­ ляющий возможность обращаться ко всем 232 доступным адресам памя­ ти, – это использование беззнакового представления;  многие виды цифровых данных не требуют применения арифметики. На­ пример, дескрипторы окон являются 32­разрядными числами, но не имеет
3 никакого смысла складывать, вычитать или выполнять другие такие же операции с этими числами. Они предназначены только для идентификации. Таким образом, тип данных HANDLE является типом unsigned long. Теперь пора рассмотреть, как в действительности работают беззнаковое и зна­ ковое представления в дополнительном двоичном коде. Беззнаковое представление Если 16­разрядное слово интерпретируется как беззнаковое, надо считать в двоичном коде справа налево, начиная с нуля, как в следующем списке (двойная стрелка ↔ заменяет слово «представляет»): 0000 0000 0000 0000 ↔ 0 0000 0000 0000 0001 ↔ 1 0000 0000 0000 0010 ↔ 2 . . . 0111 1111 1111 1111 ↔ 2^15 – 1 = 32767 1000 0000 0000 0000 ↔ 2^15 = 32768 . . . 1111 1111 1111 1111 ↔ 2^16 – 1 = 65535 Таким образом, беззнаковая интерпретация 16­разрядного слова дает возмож­ ность представить все целые числа в диапазоне от 0 до 216 – 1 = 65535. Как вам, вероятно, известно, каждый разряд в двоичном представлении числа является степенью 2, также как разряд в десятичном представлении числа – степенью 10. Табл. 5.1 можно использовать в качестве шаблона для создания беззнаковых пред­ ставлений чисел. Каждая колонка – просто последовательность степеней 2. Таблица 5.1. Шаблон для беззнаковых представлений 215 2 14 213 212 211 2 10 29282726252 4 23222120 327681638481924096 20481024 512 256128 64 32 16 8 4 2 1 В табл. 5.2 показан пример заполнения этого шаблона для определения беззнакового представления целого числа 50000. Последовательно вычитая степе­ ни 2, начиная с наибольшей подходящей, вы получите следующий результат: 50000=32768+16384+512+256+64+16 Далее поместите эти числа в третью строку табл. 5.2 и затем проставьте 1 под вписанными во второй строке числами и 0 во всех других клеточках. Получится четвертая строка табл. 5.2, которая и есть искомое беззнаковое представление. 1100 0011 0101 0000 ↔ (unsigned) 50000 Беззнаковое представление
4 Знаковые и беззнаковые типы данных Таблица 5.2 . Пример беззнакового представления числа 50000 215 214 213 212 211 210 29282726252423222120 3276816384819240962048102451225612864 32 16 8 4 2 1 32768 16384 512 256 64 16 1 1 0 0 0 0 1 1010 1 0000 Знаковое представление Методика, применяемая для создания представления в дополнительном дво­ ичном коде, заключается в использовании старшего значащего разряда в качестве индикатора знака данного числа. Такой старший (крайний левый) разряд называ­ ется знаковым разрядом (sign bit). Если знаковый разряд равен нулю, то слово ин­ терпретируется как неотрицательное целое. Если знаковый разряд равен единице, то такое число интерпретируется как отрицательное целое: 0xxx xxxx xxxx xxxx ↔ неотрицательное целое число 1xxx xxxx xxxx xxxx ↔ отрицательное целое число Представление в прямом коде со знаком Наиболее очевидный способ заполнения остальных разрядов – это заполне­ ние их модулем (magnitude), или абсолютным значением (absolute value), данного числа. Например, для представления положительного числа 5 следует записать: 00 0000 0000 0101 ↔ 5 тогда как двоичное представление 5 – это 101. Для отрицательного числа –5 прос­ то измените знаковый разряд: 1000 0000 0000 0101 ↔ 5 Этот способ совместного представления положительных и отрицательных чисел называется представлением в прямом коде (signed­magnitude representation). Такое представление довольно просто, но приводит к некоторым проблемам. Одна из трудностей заключается в том, что арифметические операции с числами, пред­ ставленными таким образом, требуют учета особых случаев, связанных с кон­ кретными знаками конкретных чисел. (Просто попробуйте сложить двоичное представление 5 и –5 .) К тому же появляется два представления для одного и того же числа 0 (+0 и –0): 0000 0000 0000 0000 ↔ 0 1000 0000 0000 0000 ↔ 0 Представление в дополнительном двоичном коде Гораздо лучшим способом, используемым в большинстве современных ком­ пьютеров, является представление в дополнительном двоичном коде. Его легче пояснить с помощью таблицы. Аналогом табл. 5.1 для знакового представления в дополнительном двоичном коде является табл. 5.3 . Единственное отличие этих двух таблиц – знак «минус» в первом столбце.
5 Таблица 5.3. Шаблон для представления чисел в дополнительном двоичном коде –2 15 214 2 13 212 211 2 1029282726252 4 23222120 –32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1 В качестве примера в табл. 5.4 приведены выкладки для знакового представ­ ления целого числа –15536. Обратите внимание, что оно в точности совпадает с беззнаковым представлением числа 50000: 1100 0011 0101 0000 ↔ (signed)  15536 Таблица 5.4 . Пример знакового представления числа –15536 –215 214 213 212 21121029282726252423222120 –32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1 –32768 16384 512 256 64 16 1 1 0 0 0 0 1 10 1010000 Так как единственное отличие табл. 5.1 и 5.3 заключается в том, что числа в первом столбце становятся отрицательными, очевидно, что двоичное слово со знаковым разрядом, равным 0, является представлением одного и того же цело­ го числа независимо от того, какое представление используется – знаковое или беззнаковое. Иначе говоря, для целых чисел в диапазоне от 0 до 32767 оба эти представления идентичны. К тому же, так как сумма всех чисел первой строки равна –1, очевидно, что число является отрицательным только в том случае, если знаковый разряд равен 1. Далее следует список, в котором показывается, как действует знаковое пред­ ставление. Он отсортирован в порядке возрастания двоичного слова. Обратите внимание на резкий скачок от положительных к отрицательным целым в середине списка. 0000 0000 0000 0000 ↔ 0 0000 0000 0000 0001 ↔ 1 0000 0000 0000 0010 ↔ 2 . . . 0111 1111 1111 1111 ↔ 2^15 – 1 = 32767 ' Положительное. 1000 0000 0000 0000 ↔ 2^15 = 32768 ' Отрицательное. 1000 0000 0000 0001 ↔ 2^15 + 1 = 32767 . . . 1111 1111 1111 1101 ↔ 3 1111 1111 1111 1110 ↔ 2 1111 1111 1111 1111 ↔ 1 Знаковое представление
 Знаковые и беззнаковые типы данных Почему это называется двоичным дополнением Данное название дополнительного двоичного кода объясняется тем, как из­ меняется знак числа, представленного в этой форме. Рассмотрим произвольное число x, представленное в форме дополнительного двоичного кода. Давайте вос­ пользуемся числом из табл. 5.4 (x = –15536): x ↔ 1100 0011 0101 0000 Теперь рассмотрим поразрядное дополнение до одного (инверсию, обратный код) этого двоичного слова, то есть слово, которое получается из исходного путем замены всех 0 на 1 и всех 1 на 0: xc ↔ 0011 1100 1010 1111 Сложение этих двух двоичных чисел дает такой результат: x+xc↔1111111111111111 Заметим, что результат будет таким независимо величины x. Но двоичное слово, все разряды которого равны 1, является представлением числа –1, итак, получается: x+xc =–1 откуда следует, что x+(xc+1)=0 или –x=x c +1 Таким образом, чтобы изменить знак любого числа, нужно инвертировать его знаковое представление и затем прибавить единицу. Результирующее двоичное слово называется поразрядным дополнением до двух (two’s complement) исход­ ного двоичного слова. Таким образом, чтобы получить отрицательное значение любого числа, представленного в форме дополнительного двоичного кода, просто возьмите дополнительный код двоичного представления этого числа. Преобразование между знаковыми и беззнаковыми представлениями Теперь рассмотрим главную тему – преобразование между знаковыми и без­ знаковыми представлениями. Здесь два основных вопроса для обсуждения. Во­первых, вам может понадобиться передать в API­функцию число, которое выходит за диапазон соответствующего знакового типа данных VB. Например, нужно передать 16­разрядное представление числа, относящегося к верхнему, «беззнаковому», диапазону чисел от 32768 до 65535, например, число 50000. В VC++ вы могли бы написать следующим образом: unsigned short usVar; usVar = 50000; но в VB такой код
 Dim iVar As Integer iVar = 50000 приведет к ошибке переполнения (overflow) во время работы программы. Заметь­ те, что нельзя использовать код Dim iVar As Long iVar = 50000 поскольку функция ожидает 16­разрядное двоичное слово. Вторая проблема представляет собой полную противоположность первой. Допустим, API­функция должна возвратить 16­разрядное значение в диапазо­ не чисел от 32768 до 65535, например 50000. Безусловно, возвращаемое зна­ чение должна принять переменная VB (раз уж работа проводится в VB). Но VB проинтерпретирует эту переменную как относящуюся к знаковому типу данных, как –15536, потому что, как вы видели, это число имеет знаковое представление, совпадающее с беззнаковым представлением числа 50000. Итак, вопрос состоит в следующем: как восстановить то значение, которое подразу­ мевалось? Эта проблема решается просто, если правильно ее рассматривать. Согласно рис. 5.1, суть заключается в том, что VB проинтерпретирует 16­разрядное дво­ ичное слово как знаковое представление, а Win32 – как беззнаковое (здесь пред­ полагается, что Win32 принимает или возвращает целое число типа unsigned short). Знаковое представление в интерпретации Visual Basic Беззнаковое представление в интерпретации Win32 Рис. 5 .1. Передача чисел между VB и Win32 В соответствии с рис. 5.1, если w – это 16­разрядное двоичное слово, то для обозначения числа, полученного из w в предположении, что w – это беззнаковое представление, следует писать un(w) и, соответственно, si(w), если число полу­ чено из w в предположении, что w – это знаковое представление. Таким образом, для примеров, приведенных в табл. 5.2 и 5.4, имеется un(1100 0011 0101 0000) = 50000 si(1100 0011 0101 0000) = 15536 Следует запомнить главное: на самом деле вы передаете и получаете двоичное слово, а не число. И VB и Win32 затем интерпретируют это двоичное слово как число. Трудности возникают тогда, когда интерпретации у VB и у Win32 разные. VB использует знаковую интерпретацию целого числа, а Win32 (это исходное предположение) использует беззнаковую интерпретацию. Преобразование между представлениями
 Знаковые и беззнаковые типы данных Таким образом, в соответствии с рис. 5.1, если требуется передать число un(w) API­функции, то необходимо, чтобы VB передал число si(w). Впоследствии Win32 проинтерпретирует двоичное слово w, которое на самом деле и передается через стек как un(w). И наоборот, при получении числа VB воспримет его как si(w), и нужно будет преобразовать число к этому виду, что, кстати, потребует изменения типа данных VB на тип с большим диапазоном значений для правиль­ ного представления полученного числа. Итак, все сводится к преобразованию между si(w) и un(w). Можно поразмышлять, как осуществить это преобразование, не делая никаких лишних шагов, ведь единственное различие между табл. 5.1 и 5.3 – это знак «ми­ нус» в первом столбце. Соответственно следует рассмотреть два случая. Первый случай, когда число si(w) является неотрицательным или, что одно и то же, число un(w) находится в нижней половине беззнакового диапазона (от 0 до 32767). Неважно, передается или принимается значение, но вы всегда правильно распознаете любое из этих чисел. В данном случае знаковый бит w равен нулю. Следовательно, как вы убедились, получается un(w) = si(w) Таким образом, здесь для передачи числа можно использовать обычный целый тип VB, и наоборот, возвращаемое значение, принятое переменной VB целого типа, является тем самым числом, которое было передано (преобразования не нужны). Теперь представим, что число si(w) является отрицательным или, что то же самое, un(w) находится в верхней половине диапазона от 32768 до 65535. В этом случае знаковый бит w равен единице. Этот бит, находясь в первом столбце табл. 5.4, вносит в общую сумму числа un(w) 215. И наоборот, в общую сумму числа si(w) он вносит –215. Так как вклад всех других столбцов и в un(w), и в si(w) одинаков, то вычитание соответствующих значений первого столбца должно дать одно и то же значение, то есть un(w)  2 15 = si(w) – (2 15 ) Небольшое алгебраическое преобразование дает две формулы: un(w) = si(w) + 2^16 si(w) = un(w)  2^16 Эти формулы и есть общее решение задачи. Можно подвести итог. Числа типа integer Чтобы передать число un(w) в диапазоне от 0 до 65535 в переменной VB типа integer, присвойте этой переменной число si(w). При передаче в обратном направлении, если переменная целого типа принимает число и VB представляет это число в виде si(w), то переданное число на самом деле является un(w). Ниже показана взаимосвязь между si(w) и un(w). Для si(w) >= 0 или 0 <= un(w) <= 32767 (= 215 – 1): un(w) = si(w)
 Для si(w) < 0 или 32768 <= un(w) <= 65535 (= 216 – 1): un(w) = si(w) + 2^16 si(w) = un(w)  2^16 Рис. 5.2 поясняет данные рассуждения. Когда число находится в общей части диапазонов signed и unsigned (от 0 до 32767), тогда при передаче или получе­ нии числа преобразования не нужны. До того как число будет передано из верхней части диапазона unsigned, из него вычитается 216 для приведения к диапазону signed. При получении числа из нижней части диапазона signed его исходное значение вычисляется путем прибавления 216 для перевода в соответствующий диапазон unsigned. Общий диапазон Преобра зования не нужны Прибавить 216 Вычесть 216 Знаковый диапазон Беззнаковый диапазон При получении При передаче 32768 (=2 15 ) 0 32767 (=2 15 1) 65535 (=2 16 1) Рис. 5 .2 . Преобразование между знаковыми и беззнаковыми числами типа integer Числа типа long Естественно, тот же самый принцип применим и к 32­разрядным целым типа long. Чтобы передать число un(w) в диапазоне от 0 до 232 – 1 с помощью переменной VB типа long, присвойте этой переменной число si(w). При передаче в обратном направлении, если переменная типа long принимает число и VB представляет это число в виде si(w), то переданное число на самом деле является un(w). Далее представлена взаимосвязь между si(w) и un(w). Для si(w) >= 0 или 0 <= un(w) <= 231 – 1: un(w) = si(w) Для si(w) < 0 или 231 <= un(w) <= 232 – 1: un(w) = si(w) + 2^32 si(w) = un(w)  2^32 Рис. 5.3 иллюстрирует процесс преобразования. Преобразование между представлениями
0 Знаковые и беззнаковые типы данных Байты Ситуация для байтов прямо противоположна ситуации с числами типа integer и long, так как тип Byte в VB является беззнаковым (unsigned). Пробле­ ма здесь возникает тогда, когда API­функция ожидает или возвращает знаковый байт. Тем не менее принцип действия аналогичен. Чтобы передать число si(w) в диапазоне от –128 до 127 в байте VB, присвойте этому байту число un(w). При передаче в обратном направлении, если байт при­ нимает число и VB представляет это число в виде un(w), тогда переданное число в действительности является si(w). Ниже показана взаимосвязь между si(w) и un(w). (Рис. 5.4 иллюстрирует процесс преобразования.) Для si(w) >= 0 или 0 <= un(w) <= 127 (= 27 – 1): un(w) = si(w) 2 31 0 2 311 2 321 Общий диапазон Преобра зования не нужны Прибавить 232 Вычесть 232 Знаковый диапазон Беззнаковый диапазон При получении При передаче Рис. 5.3. Преобразование между знаковыми и беззнаковыми числами типа long  128 (=2 7) 127 (=2 71) 255 (=2 81) 0 Общий диапазон Преобра зования не нужны Прибавить 28 Вычесть 28 Знаковый диапазон Беззнаковый диапазон При получении При передаче Рис. 5 .4 . Преобразование знаковых и беззнаковых байтов
1 Для si(w) < 0 или 128 <= un(w) <= 255 (= 28 – 1): un(w) = si(w) + 2^8 si(w) = un(w)  2^8 Примеры Пример 1 Передать число в диапазоне от 0 до 65535, которое хранится в переменной VB lng типа long в API­функцию с параметром типа unsigned short . . . APIFunction(unsigned short param) . . . с объявлением в VB: Declare . . . APIFunction(param As Integer) . . . Решается эта задача таким образом: Dim param As Integer ' Тот же размер, что и у параметра APIфункции. If lng >= 0 And lng <= 32767 Then param = lng ElseIf lng >= 32768 And lng <= 65535 Then param = CInt(lng – 2^16) Else MsgBox "Значение находится вне допустимого для unsigned short _ диапазона", vbCritical End If Call APIFunction(param) Пример 2 Передать число в диапазоне от 0 до 232 – 1, которое хранится в переменной VB cVar типа Currency в API­функцию с параметром типа unsigned int или unsigned long: . . . APIFunction(unsigned int param) . . . с объявлением в VB: Declare . . . APIFunction(param As Long) . . . Решение этой задачи следующее: Dim param As Long ' Тот же размер, что и у параметра APIфункции. IfcVar>=0AndcVar<=2^311Then param = cVar ElseIf cVar >= 2^31 And cVar <= 2^32  1 Then param =CLng(cVar – 2^32) Else MsgBox "Значение находится вне допустимого для unsigned int _ значения", vbCritical End If Call APIFunction(param) В следующем примере обратная ситуация – API­функция ожидает знаковую величину, а байт данных в VB является беззнаковым. Преобразование между представлениями
2 Знаковые и беззнаковые типы данных Пример 3 Передать число в диапазоне от –128 до 127, которое хранится в переменной VB iVar типа integer в API­функцию с параметром типа unsigned char (вспом­ ните, что тип данных VB Byte является беззнаковым): . . . APIFunction(unsigned char param) . . . с объявлением в VB: Declare . . . APIFunction(param As Byte) . . . Задача решается следующим образом: Dim param As Byte ' Тот же размер, что и параметра APIфункции. If iVar >= 0 And iVar <= 127 Then param = iVar ElseIf iVar >= 127 And iVar <= 1 Then param = CByte(iVar + 2^8) End If Call APIFunction(param) Пример 4 Переменной VB целого типа iVar присвоить число в диапазоне от 0 до 65535, возвращаемое API­функцией, у которой OUT­параметр имеет тип unsigned short: . . . APIFunction(unsigned short param) . . . с объявлением в VB: Declare . . . APIFunction(param As Integer) . . . Решение этой задачи таково: Dim lRealValue As Long ' Для хранения действительного значения, ' переданного в VB. If iVar >= 32768 And iVar <= 1 Then lRealValue = CLng(iVar) + 2^16 ElseIf iVar >= 0 And iVar <= 32767 Then lRealValue = CLng(iVar) End If Пример 5 Получить число в диапазоне от 0 до 232–1 в переменную VB lVar типа long из API­функции, у которой OUT­параметр имеет тип unsigned int: . . . APIFunction(unsigned int param) . . . с объявлением в VB: Declare . . . APIFunction(param As Long) . . . Решается эта задача таким образом: Dim cRealValue As Currency ' Для хранения действильного значения, ' переданного в VB. If lVar >= 2^31 And lVar <= 1 Then cRealValue = CCur(lVar) + 2^32 ElseIf lVar >= 0 And lVar <= 2^31  1 Then cRealValue = CCur(lVar) End If
3 Пример 6 Получить число в диапазоне от 0 до 255 в байт VB bVar из API­функции, у которой OUT­параметр имеет тип unsigned char: . . . APIFunction(unsigned char param) . . . с объявлением в VB: Declare . . . APIFunction(param As Byte) . . . Задача решается следующим образом: Dim iRealValue As Integer ' Для хранения действильного значения, ' переданного в VB. If bVar >= 128 And bVar <= 1 Then iRealValue = CInt(bVar) + 128 ElseIf bVar >= 0 And bVar <= 127 Then iRealValue = CInt(bVar) End If Преобразование длины слов Давайте завершим разговор о знаковых типах данных темой преобразования длины слов. Эта тема не относится к программированию API и вы, если хотите, можете ее пропустить. Допустим, имеется число, принадлежащее диапазону знаковых целых чисел от –32768 до 32767, и требуется присвоить его значение переменной типа Long. Как преобразует VB 16­разрядное знаковое представление числа в 32­разрядное знаковое представление? Если число положительное, то ответ будет таким, какой и следовало ожи­ дать – VB просто добавляет 16 нулей слева. Например, 0000 0000 0000 1010 ↔ 5 0000 0000 0000 0000 0000 0000 0000 1010 ↔ 5 Но как это происходит в случае с отрицательным числом? В качестве примера рассмотрим отрицательное число –32765 в знаковом пред­ ставлении: 1000 0000 0000 0011 ↔ 32765 Добавление 16 нулей слева привело бы к получению положительного числа, значит это неправильно. При этом изменение знакового бита тоже не помогает, так как слово 1000 0000 0000 0000 1000 0000 0000 0011 представляет  2^31 + 2^15 + 2 +1 = 2147450877 что, конечно, не равно –5 . Вместо этого предположим, что надо добавить слева 16 единиц, изменяя 1000 0000 0000 0011 ↔ 32765 на 1111 1111 1111 1111 1000 0000 0000 0011 ↔ x Преобразование длины слов
4 Знаковые и беззнаковые типы данных Для вычисления значения x оцените вклад вновь добавленных разрядов. Сначала знаковый бит добавлял –215 к исходному числу –5, а теперь добавляет 215 к числу x. Приращение от –5 до x за счет только одного этого бита: 2 * (2^15) = 2^16 Кроме того, добавленные единицы с разряда 16 по разряд 30 дополнительно вносят 2^16+2^17+...+2^30 в величину x. Наконец, 31­й бит, новый знаковый бит, вносит отрицательное значение –231. Суммирование всех добавлений дает абсолютное приращение x по сравнению с –5: 2*2^15+2^16+2^17+...+2^30 –2^31 Алгебраические преобразования, которые вы, вероятно, предпочтете опустить, показывают, что это абсолютное приращение на самом деле равняется 0: 2*2^15+2^16+2^17+...+2^30 –2^31=0 Другими словами, изменений нет. Итак, x = –5 . Это демонстрирует, что добавление 16 единиц слева не изменяет число. Иначе говоря, чтобы получить 32­разрядное знаковое представление отрицательного числа из 16­разрядного знакового представления этого числа, нужно просто до­ бавить 16 единиц слева. Можно объединить оба рассмотренных случая (положительные и отрицатель­ ные числа) следующим образом: чтобы получить 32­разрядное знаковое пред­ ставление числа из его 16­разрядного знакового представления, нужно просто 16 раз скопировать слева знаковый бит (нуль или единицу, неважно). Этот процесс называется расширением знака (sign extension).
Глава 6. Строки Тема строк может показаться довольно запутанной, однако эта путаница мгновен­ но исчезает, стоит только чуть внимательнее отнестись к деталям. Главная пробле­ ма заключается в том, что термин строка (string) используется в Visual Basic, по крайней мере, в двух разных смыслах. Что такое строка в Visual Basic? В соответствии с документацией, строка – это тип данных, состоящий из последовательности непрерывных символов, который является представлением самих символов, а не их числовых значений. Мне почему­то кажется, что здесь имелось в виду следующее: множество зна­ чений, которыми определен тип данных String – это множество последователь­ ностей символов конечной длины. В Visual Basic все символы представляются двухбайтовыми целыми числами, кодами Unicode. Иначе говоря, VB использует код Unicode для представления символов строки. Например, представление сим­ вола «h» в коде ASCII выглядит как &H68, следовательно, его представление в коде Unicode – &H0068 – в памяти будет храниться как 68 00. Таким образом, строка «help» будет представлена так: 00680065006С0070 Но поскольку слова записываются, начиная с младшего байта, строка «help» в памяти выглядит иначе: 680065006С007000 Это, конечно, хорошо, но абсолютно не то, что понимается под строками при программировании на VB. Чтобы избежать неясности, мы будем ссылаться на объект этого типа, как на массив символов Unicode, который, в конце концов, под­ ходит под данное выше определение. Это помогает также отличить его от массива символов ANSI, который также является символьным массивом, но с представле­ нием символов в виде однобайтового символьного кода ANSI. Объяснить, что же такое строки, можно следующим образом. При записи опе­ раторов Dim str As String str = "help" вы определяете не символьный массив Unicode. Здесь объявляется экземпляр с типом данных, называемым BSTR, что является сокращением от Basic String. BSTR и в действительности является указателем на некоторый участок памяти, состоящий из четырехбайтового поля длины и непосредственно массива символов Unicode с завершающим нулевым символом.
 Строки Тип BSTR Фактически строковый тип данных в VB, определяемый оператором Dim str As String подвергся радикальным изменениям между выпуском версий Visual Basic 3 и 4, что частично вызвано стремлением сделать строковый тип более совместимым с опера­ ционной системой Win32. Чтобы вы смогли понять, насколько сейчас ситуация улучшилась, на рис. 6.1 показан формат строкового типа данных VB в версии Visual Basic 3, называемый HLSTR (High­Level String – высокоуровневая строка). Длина Указатель Описатель строки Рис. 6 .1. Формат HLSTR в Visual Basic 3 Довольно сложный формат HLSTR начинается с указателя на дескриптор строки (string descriptor), который состоит из двухбайтового поля длины строки и указателя на символьный массив в формате ANSI (один байт на символ). При всем уважении к Win32 API надо сказать, что этот строковый формат – кошмарный сон. Начиная с версии Visual Basic 4, строковый тип данных VB из­ менен. Новый тип данных, BSTR, показан на рис. 6.2 . Длина Указатель Рис. 6 .2. Тип данных BSTR Этот тип данных фактически определен в спецификации OLE 2.0, то есть яв­ ляется частью спецификации ActiveX компании Microsoft. Есть несколько важных замечаний, которые следует сделать о типе данных BSTR:  тип BSTR на самом деле является указательным. Он имеет размер, равный 32 разрядам, как и все указатели, и ссылается на массив символов Unicode.
 Таким образом, массив символов Unicode и BSTR – это не одно и то же. В принципе правильно говорить о BSTR как о строке (или как о строке VB), но, к сожалению, массив символов Unicode также часто называют строкой. Поэтому не будем ссылаться на тип BSTR как на строку, лучше называть его собственным именем – BSTR;  массив символов Unicode, на который указывает BSTR, должен предварять­ ся четырехбайтовым полем длины строки и завершаться одним двухбайто­ вым нулевым символом (его код равен нулю);  массив символов Unicode может содержать произвольно расположенные дополнительные нулевые символы, поэтому на них нельзя полагаться как на символы завершения массива. Поэтому необходимо поле длины строки;  указатель ссылается на начало массива символов, а не на предваряющее массив четырехбайтовое поле длины. Как вы увидите, это критично для интерпретации BSTR в стиле строк VC++;  поле длины содержит количество байтов (а не количество символов) в сим­ вольном массиве, исключая завершающие нулевые байты. Так как массив является массивом символов Unicode, то количество символов в два раза меньше количества байтов. Следует подчеркнуть, что в Unicode символ «\0» 16­разрядный, а не 8­разряд­ ный. Не забывайте об этом при проверке массива Unicode на нулевые символы. Учтите, что, когда произносят «BSTR help» или «BSTR может содержать встро­ енные нулевые символы», на самом деле имеют в виду символьный массив, на который ссылается BSTR, а не указатель на него. Так как BSTR может включать встроенные нулевые символы, завершающий нулевой символ используется редко, по крайней мере, в части, касающейся VB. Однако его наличие чрезвычайно важно для Win32. Причина в том, что Unicode­ версия строки Win32 (обозначаемая LPWSTR) определяется как указатель на мас­ сив символов Unicode с завершающим нулевым символом (который, кстати, не может включать встроенные нулевые символы). Это объясняет, почему BSTR завершаются нулевым символом. BSTR, не вклю­ чающая встроенных нулевых символов, полностью совпадает с LPWSTR. Строки C++ будут рассматриваться немного позже. Давайте подчеркнем, что код, подобный следующему: Dim str As String str = "help" означает, что str – это имя BSTR, а не символьного массива Unicode. Иначе го­ воря, str – это имя переменной, которая содержит адрес xxxx, как показано на рис. 6.2 . Ниже приводится простой эксперимент, который можно проделать, чтобы убедиться, что строка VB является не символьным массивом, а только указателем на него. Рассмотрим следующую программу, в которой определяется структура со строками в качестве членов: Тип BSTR
 Строки Private Type utTest astring As String bstring As String End Type Dim uTest As utTest Dim s as String s = "testing" uTest.astring = "testing" uTest.bstring = "testing" Debug.Print Len(s) Debug.Print Len(uTest) Вывод данной программы таков: 7 8 Для строковой переменной s функция Len возвращает длину символьного массива; в данном случае в символьном массиве "testing" семь символов. Од­ нако для переменной uTest структурного типа функция Len фактически возвра­ щает размер структуры в байтах. То, что возвращаемое значение равно восьми, ясно показывает, что каждая из двух BSTR имеет длину четыре байта, поскольку BSTR является указателем. Строки в стиле C: LPSTR и LPWSTR В VC++ и Win32 используются строковые типы данных LPSTR и LPWSTR (они изображены на рис. 6.3). Строка LPSTR определяется как указатель на массив символов ANSI с за­ вершающим нулевым символом. Однако, поскольку единственный показатель Указатель LPSTR (Win32) Указатель LPWSTR (Win32 wide) Рис. 6 .3. Типы данных LPSTR и LPWSTR
 окончания строки LPSTR – это завершающий нулевой символ, LPSTR не может включать встроенные нулевые символы. Строка LPWSTR представляет собой ука­ затель на массив символов Unicode с завершающим нулевым символом. (Буква W в LPWSTR в терминах компании Microsoft означает «символ Unicode».) Кроме того, можно встретить типы данных LPCSTR и LPCWSTR. Добавленная буква «C» замещает слово «Constant» и означает только то, что экземпляр пере­ менной этого типа не может быть (и не будет) изменен ни одной API­функцией, использующей данный тип. В других отношениях LPCSTR идентична LPSTR, и, соответственно, LPCWSTR аналогична LPWSTR. И наконец, обобщенный (generic) тип данных LPTSTR используется при ус­ ловной компиляции точно так же, как тип данных TCHAR, с целью объединения кодировок ANSI и Unicode в одном исходном коде. Далее приводятся примеры таких объявлений: #ifdef UNICODE typedef LPWSTR LPTSTR; // LPTSTR – синоним LPWSTR в среде // Unicode. typedef LPCWSTR LPCTSTR; // LPCPSTR – синоним LPCWSTR в среде // Unicode. #else typedef LPSTR LPTSTR; // LPTSTR – синоним LPSTR в среде ANSI. typedef LPCSTR LPCTSTR; // LPTCSTR – синоним LPCSTR в среде ANSI. #endif Рис. 6.4 объединяет все варианты. LPCTSTR читается как «long pointer to a constant generic string» (дальний ука­ затель на константную обобщенную строку). строку символов в кодировке Дальний указатель на константную ANSI, Unicode или обобщенного типа Рис. 6 .4. Строительные блоки имен строковых типов LP...STR Терминология, связанная со строками Терминология, связанная со строками Чтобы избежать любой возможной неясности, будем пользоваться термина­ ми BSTR, массив символов Unicode и массив символов ANSI. Термин «строка» в книге немного видоизменен, все декларации записаны как «строка VB» (в зна­ чении BSTR) или «строка VC++» (в значении LP..STR). Данный термин без соответствующей модификации употребляться не будет.
100 Строки Однако, разбирая документацию VB, вы будете довольно часто встречать тер­ мин «строка» без подобного уточнения. Что имеется в виду, BSTR или массив символов, вам придется догадываться самостоятельно. Средства для исследования строк Для проведения исследования нужны соответствующие инструменты. Об API­функции CopyMemory уже рассказывалось ранее. Давайте познакомимся с некоторыми дополнительными средствами для работы со строками. Функция StrConv в Visual Basic Функция StrConv предназначена для преобразования массива символов из одного формата в другой. Ее синтаксис таков: StrConv(string, conversion, LCID) где string имеет тип BSTR, conversion – константа (описывается позже) и LCID – необязательный локальный идентификатор (который мы проигнорируем). Среди возможных значений констант нас интересуют только два:  vbUnicode (ее следовало бы назвать vbToUnicode);  vbFromUnicode. Данные константы указывают направление преобразования массива символов BSTR: из Unicode в ANSI и обратно. Но теперь возникает вопрос (который следовало бы адресовать к официаль­ ной документации). Дело в том, что ANSI BSTR не существует. По определению, символьный массив, на который ссылается BSTR, – это массив Unicode. Так как же следует понимать ANSI BSTR? Можно, однако, вообразить, чем могла бы быть ANSI BSTR. Для этого доста­ точно поменять массив символов Unicode с рис. 6.2 на массив ANSI. Далее ABSTR будет использоваться в качестве сокращения ANSI BSTR, но вам необходимо помнить, что это термин неофициальный, применяемый только в рамках данной книги. Теперь можно сказать, что существует два официальных варианта StrConv: StrConv(aBSTR, vbFromUnicode) ' Возвращает ABSTR. StrConv(anABSTR, vbUnicode) ' Возвращает BSTR. Ирония заключается в том, что, в первом варианте VB не сможет правильно воспринять возвращаемое значение своей собственной функции. Чтобы понять это, рассмотрим следующий код: s = "help" Debug.Print s Debug.Print StrConv(s, vbFromUnicode) Результат help ?? получается из­за того, что VB пытается проинтерпретировать ABSTR как BSTR.
101 Рассмотрим следующий код: s= "h" & vbNullChar & "e" & vbNullChar & "1" & vbNullChar & "p" & _ vbNullChar Debug.Print s Debug.Print StrConv(s, vbFromUnicode) Вывод программы: help help Здесь вы «обманули» VB, искусственно дополнив символьный массив до вида Unicode. Поэтому, когда StrConv осуществит свое преобразование, результатом будет являться ABSTR, что как раз и нужно для получения правильной интер­ претации BSTR. Этот пример показывает, что функции StrConv на самом деле ничего не извес­ тно ни о BSTR, ни о ABSTR. Она работает на основании предположения, что ей передается указатель на символьный массив, и слепо выполняет преобразование этого массива. Как вы увидите в дальнейшем, многие другие строковые функции ведут себя аналогично. То есть они могут принять и BSTR, и ABSTR – для них это всего лишь указатель на некоторый массив байтов с завершающим нулевым символом. Функции Len и LenB В Visual Basic есть две функции для определения длины строки – Len и LenB. Каждая принимает BSTR или ABSTR и возвращает значение типа long. Следую­ щий фрагмент кода поясняет это: s = "help" Debug.Print Len(s), LenB(s) Debug. Print Len(Strconv(s, vbFromUnicode)), LenB(Strconv(s, _ vbFromUnicode)) На экран будет выведено: 48 24 Отсюда следует, что Len возвращает количество символов, а LenB – количес­ тво байтов в BSTR. Функции Chr, ChrB и ChrW Эти три функции имеют различный диапазон (range) ввода и разный вывод. Данные отличия поначалу выглядят довольно запутанно – возможно, вам придет­ ся прочитать их определения несколько раз:  Chr принимает значение x типа long в диапазоне от 0 до 255 и возвращает BSTR с длиной, равной единице (один двухбайтовый символ). Этот единс­ твенный символ, на который ссылается BSTR, имеет код Unicode, равный x. (В этом случае коды Unicode и ANSI действительно совпадают.) Заметьте, что в соответствии с самой последней редакцией документации не сущест­ вует различий между Chr и Chr$; Средства для исследования строк
102 Строки  ChrB принимает значение x типа long в диапазоне от 0 до 255 и возвращает ABSTR с длиной, равной единице (байт). Этот единственный символ, на который ссылается ABSTR, имеет код ANSI, равный x;  ChrW принимает значение x типа long в диапазоне от 0 до 65535 и возвращает BSTR с длиной, равной единице (один двухбайтовый символ). Этот единс­ твенный символ, на который ссылается BSTR, имеет код Unicode, равный x. Функции Asc, AscB и AscW Эти функции выполняют действие, противоположное функциям Chr. Напри­ мер, функция AscB принимает односимвольную BSTR и возвращает байт, экви­ валентный коду ANSI данного символа. Чтобы убедиться, что тип возвращаемого значения – байт, попробуйте выполнить следующий код: Debug.Print VarType(AscB("h")) = vbByte Результат должен быть равным True. Может показаться, что AscB принимает в качестве входного значения BSTR, но на самом деле из BSTR она берет только первый байт. Функция Asc определяет в качестве входного значения BSTR (но не ABSTR) и возвращает целое число, равное коду Unicode исходного символа. Пустые строки и пустые символы В VB допустимы пустые (null) BSTR. Код Dim s As String s = vbNullString Debug.Print VarPtr(s) Debug.Print StrPtr(s) приводит к следующему результату (адрес на вашем компьютере, разумеется, может отличаться): 1243948 0 Это показывает, что пустая переменная типа BSTR – это просто указатель, зна­ чение которого равно нулю. О назначении StrPtr будет рассказано несколько поз­ же. В Win32 и VC++ переменная BSTR называется пустым (null) указателем. Воз­ можно, вам известно, чем отличаются в этом смысле vbNullString и vbNullChar. Константа vbNullChar является не указателем, а символом Unicode со значени­ ем, равным нулю. Таким образом, на уровне разрядов значения vbNullString и vbNullChar идентичны. Но они по­разному интерпретируются и, следователь­ но, имеют различия. Важно также не путать пустую (null) BSTR и пустую (empty) BSTR, которая обычно обозначается парой смежных кавычек: Dim s As String Dim t As String s = vbNullString t=""
103 В отличие от пустой (null) строки, пустая (empty) BSTR – это указатель с не­ которым отличным от нуля (nonzero) адресом памяти. По этому адресу находится завершающий нулевой символ пустой (empty) BSTR и предшествующее ему поле длины, содержащее нуль. Функции VarPtr и StrPtr Функция VarPtr уже упоминалась, но не в связи со строками. Хотя функции VarPtr и StrPtr не описаны в документации Microsoft, они могут быть очень по­ лезны, а потому мы будем часто ими пользоваться, особенно функцией VarPtr. Если var – некоторая переменная, то VarPtr(var) является адресом этой переменной, возвращаемым в виде значения типа long. Если str – переменная типа BSTR, то StrPtr(str) выводит содержимое BSTR. Это содержимое является адресом массива символов Unicode, на который указывает BSTR. Давайте рассмотрим это подробнее. На рис. 6.5 показана строка типа BSTR. Рис. 6 .5 . Строка типа BSTR Код к этому рисунку достаточно прост: Dim str As String str = "help" Обратите внимание, что переменная str размещена по адресу aaaa, а массив символов начинается с адреса xxxx, который и является содержимым указатель­ ной переменной str. Чтобы понять, что VarPtr = aaaa StrPtr = xxxx просто выполните следующую программу: Dim lng As Long Dim i As Integer Dim s As String Dim b(1 To 10) As Byte Dim sp As Long, vp As Long s = "help" Средства для исследования строк
104 Строки sp = StrPtr(s) Debug.Print "StrPtr:" & sp vp = VarPtr(s) Debug.Print "VarPtr:" & vp ' Проверим, что sp = xxxx и vp = aaaa, ' переместив long, на которую указывает vp (она равна xxxx), ' в переменную lng и затем сравнив ее с sp. CopyMemory lng, ByVal, vp, 4 Debug.Print lng = sp ' Чтобы убедиться, что sp содержит адрес символьного массива, ' копируем из этого адреса в массив байтов и выводим ' массив байтов. Мы должны получить "help". CopyMemory b(l), ByVal sp, 10 Fori=1To10 Debug.Print b(i); Next Получится следующий результат: StrPtr:1836612 VarPtr:1243988 True 104010101080112000 Это еще раз доказывает, что массив символов в BSTR действительно имеет формат Unicode. Кроме того, добавив операторы Dim ct As Long CopyMemory ct, ByVal sp  4, 4 Debug.Print "Length field: " & ct сразу после sp = StrPtr(s) DebugPrint "Strptr:" & sp получим следующий результат: Length field: 8 который показывает, что в поле длины действительно содержится количество байтов, а не количество символов. Как упоминалось ранее, если вам не нравится работать с недокументирован­ ными функциями, то можете использовать функцию rpiVarPtr из библиотеки rpiAPI.dll архива. Можно также имитировать StrPtr следующим образом: ' Имитируем StrPtr. Dim lng As Long CopyMemory lng, ByVal VarPtr(s), 4 ' lng = StrPtr(s). Как вы уже знаете, этот код копирует содержимое указателя BSTR, которое является возвращаемым значением StrPtr в переменную lng типа long.
105 Преобразование строк в VB Теперь вы подошли к странной истории о том, как VB управляет передачей BSTR во внешние DLL­функции. Загадка состоит в том, что он никак этого не делает. Вам уже известно, что VB для внутреннего представления строк (BSTR) ис­ пользует Unicode. Windows NT также применяет Unicode в качестве внутреннего символьного кода. Однако Windows 9x Unicode не поддерживает (с некоторыми исключениями). Давайте исследуем путь, которым аргумент BSTR доходит до внешней DLL­функции (будь то вызов Win32 API или любой другой функции). Пытаясь обеспечить совместимость с Windows 95, VB всегда (даже работая под Windows NT) создает ABSTR, преобразует массив символов Unicode (BSTR) к виду ANSI и размещает преобразованные символы в символьном массиве ABSTR. Затем VB передает ABSTR внешней функции. Как вы увидите позже, это верно даже в случае обращения к функции с точкой входа Unicode под Windows NT. Подготовка BSTR Перед тем как посылать BSTR внешней DLL­функции, VB создает новую строку ABSTR, хранящуюся в другом, отличном от исходной BSTR, месте. Затем передает эту строку DLL­функции. Процесс дублирования/преобразования изоб­ ражен на рис 6.6 . Рис. 6.6 . Преобразование BSTR в ABSTR Когда вы впервые встретились с функцией CopyMemory, она использова­ лась для иллюстрации процесса преобразования между Unicode и ANSI. Давайте повторим тоже самое, но уже с некоторыми изменениями. Библиотека rpiAPI. dll включает функцию, называемую rpiBSTRtoByteArray, которая возвращает значения VarPtr и StrPtr исходной строки, передаваемой этой DLL­функции. Соответствующая декларация в VB выглядит так: Public Declare Function rpiBSTRtoByteArray Lib "???\rpiAPI.dll" ( _ ByRef pBSTR As String, _ Преобразование строк в VB
10 Строки ByRef bArray As Byte, _ pVarPtr As Long, _ pStrPtr As Long _ ) As Long В качестве первого параметра эта функция принимает BSTR, передаваемую по ссылке. Следовательно, передается адрес BSTR, а не адрес символьного массива. То есть передается указатель на указатель, ссылающийся на символьный массив. Второй параметр должен быть установлен на первый байт байтового массива, под который вызывающая программа должна выделить достаточно памяти, чтобы принять все байты передаваемой BSTR. Если здесь произойдет какой­то сбой, то он обязательно приведет к аварийному завершению приложения. Последние два параметра являются выходными (OUT­parameters) в том смыс­ ле, что вызывающая программа просто объявляет пару переменных типа long, которым функция должна присвоить конкретные значения. Переменной pVarPtr будет задан адрес BSTR, а переменной pStrPtr – содержимое BSTR (которое, как вы знаете, представляет адрес символьного массива) в интерпретации DLL­ функции. Так вы сможете бегло ознакомиться с тем, что на самом деле передается из VB в DLL. Функция возвращает длину исходной строки в байтах. И, наконец, чтобы убе­ дить вас в правильности своей работы, функция изменит первый символ исходной строки на символ «X». Далее приводится тестовая программа (функция VBGetTarget обсуждалась в главе 3, в разделе «Реализация оператора раскрытия ссылки в Visual Basic»): Sub BSTRTest() Din i As Integer Dim sString As String Dim bBuf(1 To 10) As Byte Dim pVarPtr As Long Dim pStrPtr As Long Dim bTarget As Byte Dim lTarget As Long sString = "help" ' Выводим начальный адрес и содержимое BSTR. Debug.Print "VarPtr:" & VarPtr(sString) Debug.Print "StrPtr:" & StrPtr(sString) ' Вызываем внешнюю функцию. Debug.Print "Function called. Return value:" & _ rpiBSTRToByteArray(sString, bBuf(1), pVarPtr, pStrPtr) ' Выводим промежуточную ABSTR в представлении DLL. ' Ее адрес и содержимое: Debug.Print "Address of temp ABSTR as DLL sees it: " & pVarPtr Debug.Print "Contents of temp ABSTR as DLL sees it: " & pStrPtr
10 ' Выводим буфер, на который ссылается промежуточная ABSTR. Debug.Print "Temp character array: "; Fori=1To10 Debug.Print bBuf(i); Next Debug.Print ' Теперь то, что возвратила DLLфункция. ' Проверим статус переданного буфера строки – он освобожден. VBGetTarget lTarget, pVarPtr, 4 Debug.Print "Contents of temp ABSTR after DLL returns: " & lTarget ' Проверим измененный символ в строке. Debug.Print "BSTR is now: " & sString End Sub А далее показан результат: VarPtr:1242736 StrPtr:2307556 Function called. Return value:4 Address of temp ABSTR as DLL sees it: 1242688 Contents of temp ABSTR as DLL sees it: 1850860 Temp character array: 104 101 108 112 0 0 0 0 0 0 Contents of temp ABSTR after DLL returns: 0 BSTR is now: Xelp Сначала эта программа печатает адрес (VarPtr) и содержимое (StrPtr) ис­ ходной BSTR в представлении VB. Затем она вызывает функцию, которая запол­ няет буфер байтами и присваивает значения выходным параметрам. После этого выводятся содержимое буфера и значения выходных параметров. Важно отметить, что адрес и содержимое «строки», возвращенные DLL­функцией, отличаются от исходных значений. Это указывает на то, что VB передал DLL объект, отличаю­ щийся от исходного. На самом деле байты в буфере имеют формат ANSI, то есть объект представляет собой ABSTR. Далее выводится содержимое переданной ABSTR после выхода из DLL. Оно равно нулю. Это указывает на то, что память, выделенная временной ABSTR, освобождена. (Предположение, что ABSTR стала теперь пустой строкой, было бы неправильным, в действительности ABSTR больше не существует.) Обратите внимание, что, хотя тестовая программа выполнялась под Windows NT, преобразование строк все равно осуществлялось, несмотря на имеющуюся в NT поддержку Unicode. Возвращаемая BSTR Изменение BSTR в DLL и возвращение результата вызывающей програм­ ме – явление нередкое. Фактически это может быть единственной задачей DLL­ функции. Преобразование строк в VB
10 Строки Рис. 6.7 поясняет ситуацию. После того как ABSTR изменена DLL­функцией, процесс преобразования меняется на противоположный. Теперь исходная BSTR str будет ссылаться на массив символов Unicode с выходными данными API­фун­ кции. Заметьте, однако, что этот массив может не возвращаться на свое исходное место. Так, API­функция GetWindowText перемещает массив. Суть в том, что нельзя полагаться на неизменность содержимого BSTR. Постоянным остается только ее адрес. Рис. 6 .7 . Преобразование при возвращении параметра Вызываемые функции Известно, что в Windows 9x API входные точки Unicode не реализованы. Поэ­ тому, желая обеспечить совместимость своих приложений и с Windows 9x, и с Win­ dows NT, вы, возможно, захотите вызывать API­функции только через точки входа ANSI. Например, следует вызывать SendMessageA, не SendMessageW. (Ниже приводится пример и с использованием входной точки Unicode.) Полный цикл строки Давайте рассмотрим полный цикл, который проходит BSTR в процессе пере­ дачи внешней DLL. Предположим, что вызывается OLE­функция, которая принимает параметр­ строку и возвращает ее модификацию. Хороший пример представляет собой API­функция CharUpper. Она выполняет преобразование каждого символа стро­ ки в верхний регистр. Декларация VB для версии ANSI выглядит следующим образом:
10 Declare Function CharUpperA Lib "user32" ( _ ByVal lpsz As String _ ) As Long В Windows 9x В Windows 9x со строкой­аргументом происходит следующее (помните, что и туда, и обратно передаются указатели на символьный массив, а не сами сим­ вольные массивы): 1. BSTR lpsz дублируется VB в виде ABSTR, и дубль передается функции CharUpperA, которая интерпретирует его как LPSTR. 2. Эта функция обрабатывает LPSTR и передает результат VB. 3. VB преобразует LPSTR обратно в BSTR. Обратите внимание, что если большинство API­функций (в данном случае CharUpper) воспринимают параметры BSTR как LPSTR, то есть игнорируют поле длины, не может быть уверенности, что это поле всегда будет содержать правильное значение. В случае CharUpper длина не изменяется, и поэтому остается правиль­ ной, но другие API­функции, вероятно, могут изменять длину символьного массива. Функции, написанные специально для формата BSTR, будут просто вставлять завер­ шающий нулевой символ в новый символьный массив, не изменяя поле длины. Следо­ вательно, нельзя полагаться на достоверность значения, указанного в этом поле. В Windows NT В Windows NT строка­аргумент пройдет сквозь следующие манипуляции: 1. Строка в VB преобразуется из BSTR в ABSTR и передается функции CharUpperA, которая интерпретирует ее как LPSTR. 2. Данная функция переводит LPSTR в LPWSTR и передает LPWSTR в точку входа Unicode CharUpperW. 3. Unicode­функция CharUpperW обрабатывает LPWSTR и формирует резуль­ тирующую LPWSTR, возвращая ее CharUpperA. 4. Функция CharUpperA переводит LPWSTR обратно в LPSTR и передает ее VB, который воспринимает ее как ABSTR. 5. VB переводит ABSTR обратно в BSTR. Пример использования точки входа Unicode Казалось бы, в Windows NT можно вызывать функцию через точку входа Unicode, естественно, рассчитывая на правильный результат. Однако VB продол­ жает осуществлять преобразования между BSTR и ABSTR, и поэтому нужно его нейтрализовать. ANSI­версия вызова CharUpperA такова: s = "d:\temp" Debug.Print s CharUpperA s Debug.Print s И в Windows 9x, и в Windows NT результат такой, какой и следовало ожидать: d:\temp D:\TEMP Преобразование строк в VB
110 Строки В Windows NT можно попробовать реализовать сначала версию Unicode: s = "d:\temp" Debug.Print s CharUpperW s Debug.Print s но результат окажется таким: d:\temp d:\temp Ясно, что что­то не так. Кстати, в документации говорится, что при ошибках функции CharUpper отсутствуют признаки успешного завершения или ошибки. Ошибки встречаются редко. Для этой функции нет расширенной информации об ошибке. Не следует вызывать GetLastError. Однако вам известно, что проблема кроется в тех преобразованиях между BSTR и ABSTR, которые выполняет VB. Поэтому давайте попробуем выполнить следующую программу: s = "d:\temp" Debug.Print s s = StrConv(s, vbUnicode) Debug.Print s GharUpperW s Debug.Print s s = StrConv(s, vbFromUnicode) Debug.Print s Вы получите такой результат: d:\temp d:\temp D:\TEMP D:\TEMP Здесь делается попытка компенсировать сжатие исходной BSTR в ABSTR за счет ее предварительного расширения. Действительно, при первом вызове функ­ ция StrConv просто приводит каждый байт своего операнда к формату Unicode. Функция в принципе не пытается определить, что строка уже представлена в формате Unicode. Рассмотрим, например, первый символ Unicode «d». Его шестнадцатеричный Unicode­код – &H0064, в памяти он выглядит как 64 00. Каждый байт преобразу­ ется функцией StrConv в Unicode, что дает &H0064 0000 (в памяти 64 00 00 00). В результате между всеми символами Unicode исходной Unicode­строки вставля­ ются нулевые символы. Теперь, подготавливая строку к передаче функции CharUpperW, VB пре­ образует расширенную строку из Unicode в ANSI, возвращая ее, таким образом, к исходному формату Unicode. В этот момент CharUpperW может правильно интерпретировать строку и преобразовать ее в верхний регистр. Как только пре­ образованная строка возвращается из CharUpperW, VB переводит полученный
111 результат в Unicode, то есть расширяет строку, вставляя нулевые символы. Затем ее следует перевести в ANSI, чтобы избавиться от заполняющих строку лишних нулей. Передача строк в Win32 API Теперь настало время поговорить о некоторых практических аспектах пере­ дачи строк. ByVal в сравнении с ByRef Некоторые авторы утверждают, что оператор ByVal в случае со строками пе­ регружается, то есть примененный к строкам, а не к переменным другого типа, он принимает обратный (ByRef) смысл. Но я так не считаю. Запись ByVal str As String указывает VB на то, что требуется передать содержимое BSTR (на самом деле ABSTR), которое представляет собой указатель на символьный массив. Таким образом, ByVal работает как обычно, просто содержимое BSTR – это указатель на другой объект, что выглядит как передача по ссылке. В свою очередь ByRef str As String передает, как и следовало ожидать, адрес BSTR. Строковые параметры IN и OUT Многие API­функции принимают или возвращают строки. Почти все они опе­ рируют со строками в стиле C, то есть со строками LPSTR и LPWSTR. Некоторые функции, связанные с OLE, требуют строки BSTR. В качестве примера рассмот­ рим следующую функцию из Microsoft Web Publishing API (API­функции для публикации гипертекста). Обратите внимание, что она использует строки BSTR. Заметьте, что ее декларация сообщает, какие параметры являются входными (IN), а какие – выходными (OUT). Такое «дружелюбное» описание функции встреча­ ется крайне редко. HRESULT WpPostFile ( [in] LONG hwnd, [in] BSTR bstrLocalPath, [in, out] LONG * plSiteNameBufLen, [in, out] BSTR bstrSiteName, [in, out] LONG * plDestURLBufLen, [in, out] BSTR bstrDestURL, [in] LONG lFlags, [out, retval] LONG * plRetcode ); API­функции работают со строками тремя способами:  они могут принимать строку в качестве ввода во входном параметре;  они могут возвращать строку в качестве вывода в выходном параметре; Передача строк в Win32 API
112 Строки  они могут и принимать, и возвращать, как в одном и том же параметре, так и в отдельных (IN, OUT) параметрах. В листинге 6.1 представлены три варианта API­декларации. Листинг 6.1. Три варианта декларации // Пример со входным (IN) параметром. HWND FindWindow( LPCTSTR lpClassName, // Указатель на имя класса. LPCTSTR lpWindowName // Указатель на имя окна. ); // Пример с выходным (OUT) параметром. int GetWindowText( HWND hWnd, // Дескриптор окна или управляющего элемента // с текстом. LPTSTR lpString, // Адрес буфера текста. int nMaxCount // Максимальное количество копируемых символов. ); // Пример с IN/OUT параметром. LPTSTR CharUpper( LPTSTR lpsz // Одиночный символ или указатель на строку. ); Функция FindWindow возвращает дескриптор высокоуровневого окна, имя или имя класса которого соответствует заданной строке. Функция GetWindowText возвращает текст заголовка окна в выходном па­ раметре lpString. Она возвращает также количество символов заголовка в ка­ честве своего возвращаемого значения. Функция CharUpper приводит строку или единичный символ к верхнему ре­ гистру. Если аргументом является строка, функция преобразует символы символь­ ного массива на месте, то есть этот аргумент является IN­/OUT­параметром. Естественно, возникает вопрос о том, как правильно преобразовать приведен­ ные декларации функций к виду VB. Можно было бы просто заменить каждую строку в стиле C объявлением в стиле VB: ByVal str As String которое, как известно, определяет тип данных BSTR. Тем не менее такой перевод неоднозначен. Поэтому прежде всего надо уяснить различия между передачей BSTR по значению и по ссылке. Обсуждение входных параметров Первая декларация в листинге 6.1 HWND FindWindow( LPCTSTR lpClassName, // Указатель на имя класса. LPCTSTR lpWindowName // Указатель на имя окна. );
113 может быть преобразована следующим образом: Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _ ByVal lpClassName As String, _ ByVal lpWindowName As String _ ) As Long Это работает просто замечательно. Так как функция FindWindow не изменяет содержимого параметров (обратите внимание на букву «C» в LPCTSTR), строки BSTR будут истолкованы Win32 как строки LPSTR, каковыми они и являются. Вообще, имея дело с LPSTR­константой, можно смело использовать BSTR. Следует также обратить внимание на то, что функция FindWindow допускает присвоение значения одному из двух параметров (но не обоим одновременно), при этом другой параметр устанавливается в null. В Win32 такой параметр, которому по выбору программиста не присваивается значение, представляется пустым (null) указателем, то есть указателем, значение которого равно нулю. Естественно, нуль не является корректным адресом, поэтому пустой указатель имеет особый смысл, Win32 его по­особому и воспринимает. К счастью, в VB существует ключевое слово vbNullString, которое опре­ деляет пустую BSTR (и, следовательно, также пустую LPWSTR). Оно может ис­ пользоваться везде, где требуется пустая строка. В действительности это не так уж очевидно, как может показаться сначала. До введения vbNullString в Visual Basic (что произошло в VB 4) вам потребовалось бы нечто подобное FindWindow(0&,. . .) чтобы имитировать пустую строку в качестве первого параметра. Трудность состо­ ит в том, что VB выводил бы ошибку несовпадения типов (type mismatch error), так как нуль типа long не является строкой. Решение этой проблемы заключа­ лось в том, чтобы объявить три отдельных альтернативных имени для урегули­ рования двух исключительных случаев с пустыми параметрами. С введением vbNullString это неудобство устранено. В качестве примера используем запись, при помощи которой можно получить дескриптор окна с текстом заголовка «Microsoft Word – API.doc»: Dim sTitle As String Dim hnd As Long sTitle = "Microsoft Word  API .doc" hnd = FindWindow(vbNullString, sTitle) Или проще: Dim hnd As Long hnd = FindWindow(vbNullString, "Microsoft Word  API.doc") Обсуждение выходных параметров Теперь рассмотрим вторую декларацию в листинге 6.1: int GetWindowText( HWND hWnd, // Дескриптор окна или управляющего элемента // с текстом. Передача строк в Win32 API
114 Строки LPTSTR lpString, // Адрес буфера текста. int nMaxCount // Максимальное количество копируемых символов. ); Это можно перевести в VB следующим образом: Declare Function GetWindowText Lib "user32" Alias " GetWindowTextA" ( _ ByVal hwnd As Long, _ ByVal lpString As String, _ ByVal cch As Long _ ) As Long HWND – значение типа long, совпадает с int (integer) в среде C. В этом случае строка­параметр является выходным параметром, так как функция должна заполнить ее чем­нибудь полезным, в данном случае, заголовком окна, дескриптор которого находится в параметре hwnd. Далее приводится пример вызова этой функции: Sub GetWindowTitle() Dim sText As String Dim hnd As Long Dim cTitle As Integer Dim lngS As Long, lngV As Long ' Подготовим буфер для строки заголовка. sText = String$ (256, vbNullChar) ' Сохраним адреса BSTR и массива символов Unicode. lngV = VarPtr(sText) lngS = StrPtr(sText) ' Выполним поиск окна с заданным именем класса. hnd = Findwindow("ThunderRT5Form", vbNullString) ' Если окно найдено получим его заголовок. Ifhnd>0Then cTitle = GetWindowText(hnd, sText, 255) sText = Left$(sText, cTitle) Debug.Print sText ' Сравним текущие адреса BSTR и массива символов с сохраненными ' ранее и рассмотрим произошедшие изменения. Debug.Print VarPtr (sText), lngV Debug.Print StrPtr(sText), lngS Else Debug.Print "Окно с заданным именем класса отсутствует.", vbInformation End If End Sub Результат одного прогона:
115 RunHelp  Unregistered Copy  Monday, December 7, 1998 10:11:53 AM 1243480 1243480 2165764 2012076 Увидев первую строку, не волнуйтесь – это незарегистрированная копия моей собственной программы. Сначала в памяти размещается буфер строки заголовка окна. Об этом еще бу­ дет рассказываться позже. Затем функция FindWindow используется для поиска окна с именем класса ThunderRT5Form – форма VB5 времени выполнения. Если такое окно найдено, функция возвратит дескриптор (handle) этого окна в виде значения параметра hnd. Далее вызывайте функцию GetWindowText, передав ей hnd, буфер для текста sText и его размер. Так как функция GetWindowText возвращает количество символов в буфере без завершающего нулевого символа, то есть количество символов в заголовке окна, то можно использовать функцию Left для извлечения самого заголовка из буфера строки. Обратите внимание, что сохраняется как адрес BSTR (в lngV), так и адрес символьного массива (в lngS), поэтому вы можете их сравнивать после вызова GetWindowText. Видно, что BSTR осталась на месте, а ее содержимое изменилось, то есть переместился массив символов. Выше эта ситуация уже обсуждалась. Поскольку возвращаемая строка завершается нулевым символом и не содер­ жит внутри себя нулей, следующая функция извлекает ту часть буфера, которая содержит заголовок. Эта небольшая утилита довольно универсальна, а потому я с успехом использую ее не только в примерах этой книги, но и в других про­ граммах. Public Function Trim0(sName As String) As String ' Усекаем строку справа от первого нуля. Dim x As Integer x = InStr(sName, vbNullChar) If x > 0 Then Trim0 = Left$(sName, x  1) Else Trim0 = sName End Function Возвращаясь к обсуждаемой теме, важно усвоить, что когда идет речь о выход­ ных строковых параметрах, то почти всегда в обязанности программиста входит ус­ тановка буфера строки, то есть BSTR. Под нее должно быть выделено достаточно памяти, чтобы разместить все данные, которые будет заносить в нее вызываемая API­функция. Большинство API­функций не создают строки – они просто запол­ няют области, подготовленные для них вызывающей программой. Недостаточно только объявить Dim sText As String нужно еще выделить память: sText = String$ (256, vbNullChar) Таким образом, важно запомнить следующее: когда дело касается выходного строкового параметра, проверьте, что под буфер строки выделена память доста­ точного объема. Передача строк в Win32 API
11 Строки Следует обратить внимание, что в некоторых случаях, например в функции GetWindowText, предусмотрен входной параметр для задания размера буфера. Значит, функция заранее «соглашается» с тем, что она не будет посылать в бу­ фер больше символов, чем выделено вами под буфер строки. Обычно функция включает завершающий нулевой символ в его расчетное местоположение, но для подстраховки я часто увеличиваю буфер на дополнительный символ, о котором функции ничего не известно. Учтите, что есть и другие случаи, на которые указанная особенность не рас­ пространяется, и поэтому вы должны постоянно быть начеку. Рассмотрим, например, функцию SendMessage. В документации Win32 о сообщении LB_GETTEXT, которое может использоваться для получения текста одного из пунктов в окне списка, говорится следующее: приложение посылает сообщение LB_GETTEXT для получения строки из окна списка. wParam = (WPARAM) index; // Индекс пункта [от 0]. lParam = (LPARAM) (LPCTSTR) lpszBuffer; // Адрес буфера. Параметром lpszBuffer является указатель на буфер­приемник строки. Буфер должен иметь достаточный объем для самой строки и завершающего ну­ левого символа. Сообщение LB_GETTEXTLEN может быть послано до сообщения LB_GETTEXT для получения длины строки в символах. Таким образом, страховка в виде входного параметра отсутствует. Если вы ошибетесь в выделении достаточного объема памяти под буфер, функция будет обращаться за верхнюю границу буфера, в неопределенную (unknown) область памяти. В самом лучшем случае следствием этого будет аварийное завершение программы. Если не повезет, запись повредит какие­то другие данные, что, воз­ можно, приведет к логическим ошибкам в программе или к аварийному заверше­ нию используемого приложения. Правда, Windows не совсем «безучастна» к такого рода проблемам. В ней предусмотрено сообщение LB_GETTEXTLEN, которое вы можете использовать для предварительного получения длины текста искомого пункта. Имея это значение, реально выделить под буфер достаточный объем памяти. В листинге 6.2 приведена простая программа. Данная программа читает пункты другого окна списка (кото­ рое может принадлежать другому приложению) и помещает их в ваше окно списка lstMain. Этот пример существенно расширен в главе 16. Обратите внимание на использование двух различных форм функции SendMessage. Листинг 6.2 . Использование LB_GETTEXT Public Sub ExtractFromListBox(hControl As Long) Dim cItems As Integer Dim i As Integer Dim sBuf As String Dim cBuf As Long Dim lResp As Long ' Получим количество пунктов в управляющем элементе. cItems = SendMessageByLong(hControl, LB_GETCOUNT, 0&, 0&)
11 If cItems <= 0 Then Exit Sub ' Заносим пункты в окно списка. Fori=CTocItems1 ' Получим длину пункта. cBuf = SendMessageByString(hControl, LB_GETTEXTLEN, CLng(i), _ vbNullString) ' Выделим буфер для хранения пункта. sBuf = String$(cBuf + 1, " ") ' Посылаем сообщение для получения пункта. lResp = SendMessageByString(hControl, LB_GETTEXT, CLng(i), sBuf) ' Добавляем пункт к списку в нашем окне списка. If lResp > 0 Then Form1.lstMain.AddItem Left$(sBuf, lResp) End If Next i Form1.lstMain.Refresh End Sub Пример работы параметра IN/OUT Рассмотрим теперь третью, и последнюю, функцию листинга 6.1: PTSTR CharUpper( LPTSTR lpsz // Одиночный символ или указатель на строку. ); Одна из возникающих здесь трудностей заключается в том, что несмотря на объявление lpsz как имеющей тип LPSTR данная функция позволяет присваи­ вать этому параметру значение, отличное от LPSTR. То есть в документации ут­ верждается, что параметр lpsz является указателем на строку, завершающуюся нулем, или определяет одиночный символ. Если старшее слово данного параметра равно нулю, то младшее слово содержит одиночный символ, который должен быть преобразован. Если в функцию передается строка, можно записать VB­декларацию так: Declare Function CharUpperForString Lib "user32" Alias "CharUpperA" ( _ ByVal lpsz As String _ ) As Long Этот код будет работать в общем случае, как, например, в программе ' Преобразуем строку. str = "help" Debug.Print StrPtr(str) Debug.Print CharUpperForString(str) Debug.Print str Передача строк в Win32 API
11 Строки результат которой: 1896580 1980916 HELP Давайте на минуточку прервемся, чтобы проанализировать этот результат. В документации к функции CharUpper утверждается следующее: если операндом яв­ ляется строка символов, функция возвращает указатель на преобразованную строку. Так как строка преобразуется на месте, возвращаемое значение совпадает с lpsz. С другой стороны, полученные два адреса – StrPtr(s) (который является адресом символьного массива) и CharUpper(s) – явно отличаются друг от друга. Но вспомните правила преобразования между BSTR и ABSTR: ваша строка str подвергается преобразованию во временную строку ABSTR по другому адресу. Она передается функции CharUpper, которая изменяет ее (переводит в верх­ ний регистр) и возвращает адрес строки ABSTR. Теперь VB преобразует ABSTR обратно в BSTR, но поскольку VB ничего не известно о том, что возвращаемое значение указывает на временную строку ABSTR, он и возвращает адрес именно этой строки. Можно подтвердить это экспериментально, осуществляя вызов через точку входа Unicode так же, как было сделано в более раннем примере. Соответствующие декларация и программа приводятся ниже: Declare Function CharUpperWide Lib "user32" Alias "CharUpperW" ( _ ByVal lpsz As Long _ ) As Long ' Создаем LPSTR. s = "help" lng = StrPtr(s) Debug.Print lng Debug.Print CharUpperWide(lng) Debug.Print s Получаем следующий результат: 1980916 1980916 HELP Теперь оба адреса совпадают, так как не было преобразования. Для операций с символами можно записать следующее объявление: Declare Function CharUpperForChar Lib "user32" Alias "CharUpperA" ( _ ByVal lpsz As Long _ ) As Long Например, такой вызов вернет «A» в верхнем регистре: Debug.Print Chr(CharUpperForChar(CLng(Asc("a")))) На первый взгляд кажется, что можно объединить обе декларации, используя оператор As Any.
11 Declare Function CharUpperAsAny Lib "user32" Alias "CharUpperA" ( _ ByVal lpsz As Any _ ) As Long Кроме того, программа s = "help" Debug.Print StrPtr(s) Debug.Print CharUpperAsAny(s) Debug.Print s работает в таком варианте: Debug.Print Chr(CharUpperAsAny(CLng(Asc("a")))) Она выполняется также и в таком виде: Debug.Print Chr(CharUpperAsAny(97&)) Программа возвращает «A» верхнего регистра. Однако следующий пример на моем компьютере завершается аварийно: Debug.Print CharUpperAsAny(&H11000) Проблема в том, что функция CharUpper распознает старший байт слова &H11000 как ненулевой и воспринимает все значение как адрес. Это роковая ошибка. Ведь не известно, что находится по адресу &H11000. Например, в моем случае – защищенная область памяти. Что случилось с указателем Есть и другая, гораздо более коварная проблема, которая может возникать в связи с передачей строк API­функциям. Как вы видели в случае функции CharUpper, API­функция иногда использует один и тот же параметр для хране­ ния данных разного типа (в разные моменты времени, конечно). Давайте предста­ вим себе описанную ниже ситуацию. Некоторая API­функция имеет следующую декларацию: PTSTR WatchOut( int nFlags , // Флаги. LPTSTR lpsz // Указатель на строку или длина типа long. ); В документации говорится, что если значение nFlags – WO_TEXT (где­то определенная символьная константа), то lpsz принимает строку LPTSTR (ука­ затель на символьный массив), но если значение nFlags – WO_LENGTH, то lpsz принимает длину строки типа long. Если теперь использовать VB объявление Declare Function WatchOut Lib "whatever" ( _ ByVal nFlags As Integer _ ByVal lpsz As String _ ) As Long то можно попасть в настоящую неприятность. В частности, если установить nFlags в значение WO_LENGTH, то Windows 9x отреагирует на это следующим образом. Допустим, вы формируете первоначальный буфер строки BSTR для lpsz: Передача строк в Win32 API
120 Строки Dim str As String str = String$(256, vbNullChar) VB создает временную строку ABSTR для передачи в WatchOut, как показано на рис. 6.8 . Рис. 6 .8 . Создание временной строки ABSTR Как изображено на рис 6.9, в силу того, что nFlags = WO_LENGTH, WatchOut изменит указатель, а не символьный массив. Рис. 6 .9 . Изменение указателя, а не символьного массива VB пытается преобразовать то, что он считает массивом символов ANSI с ад­ ресом zzzz и неопределенной длиной (см. рис. 6.10). Это катастрофа. Рис. 6 .10. Результирующий ошибочный указатель Потерянная строка Ошибочный указатель В Windows NT функция WatchOut изменяет исходный указатель BSTR (вмес­ то ANSI­копии), но это приводит к таким же разрушительным последствиям. Заметьте, что даже если каким­то образом вам настолько не повезет, что не про­ изойдет аварийного завершения программы в процессе попыток VB преобразовать ложную ABSTR, то результатом ее дальнейшей работы будет «мусор». Кроме того, остается вопрос с потерянной строкой, то есть с областью памяти, которая не бу­ дет освобождена после завершения программы. Это называется утечкой памяти (memory leak).
121 Подобная проблема может быть подытожена довольно просто: иногда API­ функция может изменять указатель на строку (а не саму строку) на произволь­ ное числовое значение. Но для VB данная переменная по­прежнему останется указателем. Это приводит к фатальному сбою приложения. Кроме того, проверка содержимого указательной переменной BSTR на изменение не решает проблему, потому что, как вы видели (см. рис. 6.8), Visual Basic иногда переводит указатель на настоящий (legitimate) массив символов. Ситуация, описанная выше, может встретиться на самом деле. Далее приво­ дится очень важный пример, который будет обсуждаться до конца этой главы. Функция GetMenultemlnfo извлекает данные о пункте меню Windows. Ее объявление выглядит так: BOOL GetMenuItemInfo( HMENU hMenu, // Дескриптор меню. UINT uItem, // Указывает на конкретный пункт. BOOL fByPosition, // Используется вместе с uItem. MENUITEMINFO *lpmii // Указатель на структуру (см. // комментарий в тексте) ); где параметр lpmii является указателем на структуру MENUITEMINFO, которую функция GetMenultemlnfo заполняет данными. Эта структура записывается следующим образом: typedef struct tagMENUITEMINFO { UINT cbSize; UINT fMask; UINT fType; UINT fState; UINT wID; HMENU hSubMenu; HBITMAP hbmpChecked; HBITMAP bmpUnchecked; DWORD dwItemData; LPTSTR dwTypeData; UINT cch; } Заметьте, что предпоследний член структуры – строка LPTSTR. Теперь приложение rpiAPIData из архива автоматически преобразует эту структуру в пользовательский тип VB, замещая в данном случае все типы данных C на тип VB long. Public Type MENUITEMINFO cbsize As Long '//UINT. fMask As Long '//UINT. fType As Long '//UINT. fState AS Long '//UINT. wTD As Long '//UINT. hsubMenu As Long '//HMENU. hbmpChecked As Long '//HBITMAP. Передача строк в Win32 API
122 Строки hbmpUnchecked As Long '//HBITMAP. dwItemData As Long '//DWORD. dwTypeData As Long '//LPTSTR. cch As Long '//UINT. End Type Вместо этого предположим, что строка LPTSTR была преобразована в строку VB: dwTypeData As String '//LPTSTR. В соответствии с документацией на структуру MENUITEMINFO, если пара­ метру fMask присваивается значение MIIM_TYPE, в dwTypeData выделяет­ ся подходящий буфер строки и размер буфера помещается в cch, то функция GetMenultemlnfo извлечет тип пункта меню в fType (уточняя значение cch). Если тип – MFT_TEXT, то буфер строки будет заполнен текстом данного пункта меню. Однако проблема состоит в том, что если тип является MFT_BITMAP, то младшие два байта параметра dwTypeData принимают значение дескриптора растровой картинки (и cch игнорируется). Таким образом, функция GetMenultemlnfo может изменить значение пара­ метра dwTypeData со строки LPTSTR на дескриптор растровой картинки. Подоб­ ная проблема аналогична описанной ранее. Несколько позже в этой главе будет рассматриваться интересный пример на данную тему. Необходимо запомнить, что если даже тип – MFT_TEXT, то указатель dwDataType может быть переустановлен на другой буфер символов. Итак, если не использовать строковую переменную для dwDataType, то что тогда делать? Ответ заключается в том, чтобы создать собственный символьный массив путем объявления массива байтов и передать указатель на этот массив. Другими словами, создать свою строку LPSTR. VB ничего не известно о строках LPTSTR, поэтому он будет пытаться интерпретировать ее как строку VB. Такой подход решает даже проблему потери массива, поскольку если API­фун­ кция изменит вашу LPSTR на числовое значение (подобно дескриптору растровой картинки), вы по­прежнему будете располагать ссылкой на байтовый массив (ко­ торый каким­то образом был создан), а значит, сможете очистить память само­ стоятельно или память будет освобождена, когда переменная байтового массива выйдет за область своего действия. До начала работы с байтовым массивом и обсуждения примера давайте подве­ дем итог. Иногда API­функция изменяет строку LPSTR на число. Но VB по­пре­ жнему воспринимает ее как строку. Это приводит к критической ошибке. Более того, проверка, изменилось ли содержимое указательной переменной BSTR, не помогает, так как VB иногда переустанавливает исходную BSTR на первоначальный символьный массив. Поэтому если существует возможность того, что это может произойти, вам следует создать свою собственную строку LPSTR с помощью мас­ сива байтов и использовать ее вместо BSTR. Для большей безопасности требуется делать это регулярно в тех случаях, когда строка является членом структуры. И последнее замечание. Тщательнее прорабатывайте детали! Очень часто па­ раметр API­функции ссылается на структуру, которая может иметь в качестве сво­
123 их членов другие структуры, члены которых, в свою очередь, также могут быть структурами. Такое вложение структур бывает довольно распространенным. Вы познакомитесь с примером, в котором создадите приложение, связанное с таблицей экспорта DLL. Вложенность усложняет отслеживание взаимодействия API­фун­ кции со всеми членами структур. Надежнее всегда использовать указатели на символьные массивы (то есть строки LPSTR) и полностью избегать строк BSTR при операциях со строками, встроенными в структуры. Строки и массивы байтов Массив байтов – это просто массив, члены которого имеют тип Byte, например: Dim b(1 to 100) As Byte Для получения указателя на него можно использовать VarPtr: Dim lpsz As Long lpsz = VarPtr(b(1)) ' Или rpiVarPtr(b(1)). Имя переменной lpsz является сокращением от long pointer to nullterminated string (дальний указатель на строку, завершающуюся нулем). Заметьте, что адрес первого члена массива является адресом всего массива. Помня, что LPSTR – это указатель на символьный массив с завершающим нулевым символом, будем инициализировать массив нулевыми значениями: Fori=1To100 b(i)=0 Next В действительности VB сам выполняет инициализацию переменных, однако полагаться на это – плохой стиль программирования. Преобразование между массивами байтов и строками BSTR Для копирования BSTR Dim s As String в массив байтов можно действовать двумя разными способами. Средствами только самого VB задача решается следующим образом: s = "help" Dim b(1 To 8) As Byte Fori=1To8 b(i) = AscB(MidB(s, i)) Next Другой способ решения выглядит так: s = "help" Dim b(1 To 8) As Byte CopyMemory b(1), ByVal StrPtr(s), LenB(s) Заметьте, что в обоих случаях получится одинаковый результат, указывающий на то, что байты, представляющие каждый символ в коде Unicode, перевернуты: 1040101010801120 Строки и массивы байтов
124 Строки При копировании массива байтов в строку BSTR VB предоставляет кое­ка ­ кую поддержку. Если b – байтовый массив Unicode, то можно написать просто: Dim t As String t=b Для b, как байтового массива ANSI, введите такой код: Dim t As String t = StrConv(b, vbUnicode) Заметьте, однако, что функция StrConv не распознает завершающего нуля в байтовом массиве – она преобразует весь массив. Все нулевые символы, которые встретятся в массиве, станут встроенными нулевыми символами BSTR. Преобразование между строками BSTR и LPTSTR Давайте рассмотрим преобразования строки BSTR в строку LPTSTR и обрат­ ное преобразование. Из BSTR в LPWSTR Преобразовать BSTR в байтовый массив Unicode теоретически просто, так как символьный массив BSTR является байтовым массивом Unicode, поэтому все, что нужно делать – копировать байты один за другим. Ниже представлена функция, переводящая строки BSTR в строки LPWSTR: Function BSTRtoLPWSTR(sBSTR As String, b() As Byte, lpwsz As Long) As Long ' Ввод: непустая (nonempty) строка BSTR. ' Ввод: **безразмерный** массив байтов b(). ' Вывод: заполняет массив байтов b() строкой символов Unicode из sBSTR. ' Вывод: присваивает lpwsz указатель на массив b(). ' Возвращает количество байтов, не считая завершающего двухбайтового ' нулевого символа Unicode. ' Исходная BSTR остается неизменной. Dim cBytes As Long cBytes = LenB(sBSTR) ' Распределяем память под массив, с учетом завершающего двухбайтового ' нуля. ReDim b(l To cBytes + 2) As Byte ' Устанавливаем указатель на массив символов BSTR. lpwsz = StrPtr(sBSTR) ' Копируем массив. CopyMemory b(l), ByVal lpwsz, cBytes + 2 ' Перемещаем указатель lpwsz на новый массив. lpwsz = VarPtr(b(l))
125 ' Возвращаем количество байтов. BSTRtoLPWSTR = cBytes End Function Эта функция принимает BSTR, «безразмерный» массив байтов, переменную lng типа long, приводит long к LPWSTR и отсылает количество байтов в возвра­ щаемом значении. Например: Dim b() As Byte Dim lpsz As Long, lng As Long 1ng = BSTRToLPWSTR("here", b, lpsz) Может случиться так, что вы просто скопируете содержимое BSTR в содер­ жимое lpsz: lpsz = StrPtr(sBSTR) Проблема в том, что теперь у вас два указателя на один и тот же массив – это опасная ситуация, потому что VB об этом ничего не известно и он может освобо­ дить память, выделенную под массив. Из BSTR в LPSTR Функция, переводящая BSTR в LPSTR, похожа на предыдущую, но требует предварительного преобразования из Unicode в ANSI. Function BSTRtoLPSTR(sBSTR As String, b() As Byte, lpsz As Long) As Long ' Ввод: непустая (nonempty) строка BSTR. ' Ввод: **безразмерный** массив байтов b(). ' Вывод: заполняет массив байтов b() строкой символов ANSI. ' Вывод: присваивает lpwsz указатель на массив b(). ' Возвращает количество байтов, не считая завершающего нулевого ' символа. ' Исходная BSTR остается неизменной. Dim cBytes As Long Dim sABSTR As String cBytes = LenB(sBSTR) ' Распределяем память под массив, с учетом завершающего нуля. ReDim b(l To cBytes + 2) As Byte ' Преобразуем в ANSI. sABSTR = StrConv(sBSTR, vbFromUnicode) ' Устанавливаем указатель на массив символов BSTR. lpsz = StrPtr(sABSTR) ' Копируем массив. CopyMemory b(l), ByVal lpsz, cBytes + 2 Строки и массивы байтов
12 Строки ' Перемещаем lpsz на новый массив. lpsz = VarPtr(b(l)) ' Возвращаем количество байтов. BSTRtoLPSTR = cBytes End Function Из LPWSTR в BSTR После вызова API­функции вы можете получить в качестве возвращаемого значения LPWSTR, то есть указатель на массив символов Unicode с завершающим нулевым символом. VB упрощает получение BSTR из массива байтов – надо просто выполнить присваивание, используя знак равенства. Однако VB не умеет обращаться с указателем на массив байтов. Подобное преобразование может быть выполнено приведенной ниже ути­ литой: Function LPWSTRtoBSTR(ByVal lpwsz As Long) As String ' Ввод: корректный указатель LPWSTR lpwsz. ' Возвращаемое значение: BSTR с тем же массивом символов. Dim cChars As Long ' Получим количество символов в lpwsz. cChars = lstrlenW(lpwsz) ' Инициализируем строку. LPWSTRtoBSTR = String$(cChars, 0) ' Копируем строку. CopyMemory ByVal StrPtr(LPWSTRtoBSTR), ByVal lpwsz, cChars * 2 End Function Из LPSTR в BSTR Чтобы возвращать BSTR из LPSTR, достаточно модифицировать предыду­ щую утилиту следующим образом: Function LPSTRtoBSTR(ByVal lpsz As Long) As String ' Ввод: корректный указатель LPSTR lpsz. ' Возвращаемое значение: BSTR с тем же массивом символов. Dim cChars As Long ' Получим количество символов в lpsz. cChars = lstrlenW(lpsz) ' Инициализируем строку. LPSTRt0BSTR = String$(cChars, 0)
12 ' Копируем строку. CopyMemory ByVal StrPtr(LPSTRtoBSTR), ByVal lpsz, cChars ' Преобразуем в Unicode. LPSTRtoBSTR = Trim0(StrConv(LPSTRtoBSTR, vbUnicode)) End Function Пример использования массивов байтов Давайте продемонстрируем использование массивов байтов на простом при­ мере функции CharUpper. Вам известно, что ее объявление выглядит так: LPTSTR CharUpper( LPTSTR lpsz // Одиночный символ или указатель на строку. ); Такая декларация имеет два возможных варианта в VB: Declare Function CharUpperByBSTR Lib "user32" Alias "CharUpperA" ( _ ByVal s As String _ ) As Long или: Declare Function CharUpperByLPSTR Lib "user32" Alias "CharUpperA" ( _ ByVal lpsz As Long _ ) As Long Первый вариант вы уже видели в действии, давайте попробуем поработать со вторым. Следующая далее программа сначала преобразует BSTR в LPSTR. Заметьте, что надо было бы переводить в LPWSTR, так как LPWSTR передается в CharUpperA без преобразования VB. Если бы вы передали LPWSTR, то как только CharUpperA обнаружила нулевой байт, являющийся частью (старшим байтом) первого симво­ ла Unicode в LPWSTR, она определила бы, что строка закончена. Таким образом, к верхнему регистру был бы преобразован только первый символ строки. Затем LPSTR передается в CharUpperA, которая переводит ее в верхний ре­ гистр. Имея сохраненный указатель LPSTR, можно проверить, был ли он изменен. Если нет, то следует преобразовать LPSTR обратно в BSTR и вывести ее. Если ука­ затель изменен, то вы должны освободить память, выделенную под массив байтов (или позволить переменной массива самой выйти за область ее действия). В этом простом примере указатель, конечно, не может быть модифицирован функцией CharUpper. Тем не менее та же самая процедура будет работать и с API­функцией, которая может изменить указатель. Public Sub CharUpperText Dim lpsz As Long Dim lpszOrg As Long Dim sBSTR As String Dim b() As Byte sBSTR = "help" Строки и массивы байтов
12 Строки ' Преобразуем BSTR в LPSTR. BSTRtoLPSTR sBSTR, b, lpsz ' Сохраняем LPSTR для проверки на изменение APIфункцией. lpszOrg = lpsz ' Переводим в верхний регистр. CharUpperAsLPWSTR lpsz ' Если указатель не изменен, преобразуем обратно в BSTR ' и выводим. If lpszOrg = lpsz Then Debug.Print LPSTRtoBSTR(lpsz) Else Erase b ' Используем новое значение lpsz, если требуется... End If End Sub Пример изменения меню Windows Давайте вернемся к тому примеру, который касается выполнения функции GetMenuItemInfo. Надо напомнить, что функция GetMenultemlnfo полу­ чает информацию о пункте меню Windows. Ее декларация в VB выглядит таким образом: Declare Function GetMenuItemInfo Lib "user32" Alias "GetMenuItemInfoA" ( _ ByVal hMenu As Long, _ ByVal uItem As Long, _ ByVal lByPos As Long, _ ByRef lpMenuItemInfo As MENUITEMINFO _ ) As Long где параметр lpmii является указателем на структуру MENUITEMINFO, кото­ рую функция GetMenultemlnfo заполняет данными. Ниже показана эта струк­ тура: Public Type MENUITEMINFO cbSize As Long fMask As Long fType As Long fState AS Long wTD As Long hsubMenu As Long hbmpChecked As Long hbmpUnchecked As Long dwItemData As Long dwTypeData As Long cch As Long End Type
12 В соответствии с документацией, если вы присвоите параметру fMask значе­ ние MIIM_TYPE, выделите подходящий буфер строки в dwTypeData и поместите размер буфера в cch, то функция GetMenultemlnfo извлечет тип пункта меню в fType. Если этот тип – MFT_TEXT, то буфер строки будет заполнен текстом дан­ ного пункта меню. Однако проблема заключается в том, что если типом является MFT_BITMAP, то младшие два байта параметра dwTypeData принимают значе­ ние дескриптора растровой картинки. Таким образом, функция GetMenultemlnfo изменит значение параметра dwTypeData со строки LPTSTR на дескриптор растровой картинки. На рис. 6.11 показано меню с растровой картинкой. О том, как создать такое меню в VB с использованием Win32 API, бу­ дет рассказываться в главе 21. В листинге 6.3 демонстрируется программа, используемая для получения текста каждого пункта этого меню. Листинг 6.3. Получение текста меню Public Sub GetMenuInfoExample Const MIIM_TYPE = &H10 ' Из WINUSER.H . Dim uMenuItemInfo As MENUITEMINFO Dim bBuf(l To 5O) As Byte Dim sText As String ' Инициализируем структуру. uMenuItemInfo.cbSize = LenB(uMenuItemInfo) uMenuItemInfo.fMask = MIIM_TYPE uMenuItemInfo.dwTypeData = VarPtr(bBuf(1)) uMenuItemInfo.cch = 49 ' Получаем текст меню. Fori=0To2 ' Счетчик нужно устанавливать каждый раз. uMenuItemInfo.cch = 49 ' Получим значение TypeData до вызова API. Debug.Print "Before:" & uMenuItemInfo.dwTypeData ' Вызываем API. lng = GetMenuItemInfo(hSubmenu, CLng(i), 1, uMenuItemInfo) ' Получим значение TypeData после вызова API. Debug.Print "After:" & uMenuIternInfo.dwTypeData ' Выводим текст – ТУТ ВНИМАТЕЛЬНЕЕ. ' sText = StrConv(bBuf, vbUnicode) ' Debug.Print sText Next End Sub Рис. 6 .11. Меню с растровой картинкой Строки и массивы байтов
130 Строки Вот что происходит в процессе выполнения этой программы. На первом шаге цикла (i = 0) проблем не возникает, вывод записывается следующим образом: Before:1479560 After:1479560 Заметьте, что указатель на буфер не был изменен. Таким образом, закомменти­ рованные операторы, которые печатают текст меню, выполнились бы без ошибок. Второй шаг цикла также выполняется без ошибок (до тех пор, пока оператор, включающий sText, остается закомментированным). Вывод тем не менее отли­ чается от предыдущего: Before:1479560 After:3137668829 Как и предполагалось в документации, GetMenuItemInfo возвращает де­ скриптор растровой картинки в uMenuItemInfo.dwTypeData. Значит, вы по­ теряли указатель на буфер sBuf. На третьем шаге цикла программа завершилась бы аварийно, так как третий вызов функции GetMenuItemInfo привел бы к попытке записать текст меню третьего пункта в несуществующий буфер по адресу 3137668829 = &HBB0506DD. Если этот адрес относится к защищенной области памяти (возможно, так оно и есть), вы получите сообщение, похожее на то, которое получил я (см. рис. 6.12). Рис. 6 .12. VB «недоволен» Заметьте, что если убрать комментарий с операторов, которые выводят текст меню, то программа, возможно, завершится аварийно на втором шаге цикла, когда дойдет до них. Чтобы исправить программу, нужно выяснить, когда изменяется указатель, и устранить данную проблему, как это сделано в листинге 6.4 . Листинг 6.4 . Исправленная версия программы из листинга 6.3 Public Sub GetMenuInfoExample Dim uMenuItemInfo As utMENUITEMINFO Dim bBuf(1 To 50) As Byte Dim sText As String Dim lPointer As Long ' Инициализируем структуру. uMenuItemInfo.cbSize = LenB(uMenuItemInfo)
131 uMenuItemInfo.fMask = MIIM_TYPE uMenuItemInfo.dwTypeData = VarPtr(bBuf(1)) uMenuItemInfo.cch = 49 ' Получаем текст меню. Fori=0To2 ' Счетчик нужно устанавливать каждый раз. uMenuItemInfo.cch = 49 ' Сохраним указатель на буфер. lPointer = uMenuItemInfo.dwTypeData Debug.Print "Before:" & uMenuItemInfo.dwTypeData ' Вызываем API lng = GetMenuItemInfo(hSubmenu, CLng(i), 1, uMenuItemInfo) Debug.Print "After:" & uMenuIternInfo.dwTypeData ' Проверяем, не изменился ли указатель. If lPointer <> uMenuItemInfo.dwTypeData Then Debug.Print "Bitmap!" ' Восстанавливаем указатель. uMenuItemInfo.dwTypeData = lPointer Else ' Выводим текст. sText = StrConv(bBuf, vbUnicode) Debug.Print sText End If Next End Sub Результат: Before:1760168 After:1760168 Test1 Before:1760168 After:1443168935 Bitmap! Before:1760168 After:1760168 Test3 Заметьте, что если бы вы объявили uMenuItemInfo.dwTypeData типом String, то как только функция GetMenuItemInfo изменила указатель на де­ скриптор растровой картинки, VB считал бы, что этот дескриптор – адрес сим­ вольного массива. Невозможно даже отследить такую ситуацию и восстановить указатель, так как изменение было бы вполне законным (legitimate). Строки и массивы байтов
132 Строки Предыдущие примеры и их обсуждение показали, что со строками BSTR нуж­ но обращаться очень осторожно. Иначе говоря, есть две проблемы, на которых следует акцентировать внимание:  строка BSTR подвергается преобразованию BSTR–ABSTR при передаче внешней функции;  внешняя функция может изменить значение BSTR на значение другого типа (например, на дескриптор или длину). Учтите, что на эти проблемы следует обращать внимание и тогда, когда BSTR является членом структуры. В любом случае само преобразование обычно происходит успешно, так как VB осуществляет обратное преобразование возвращаемого значения. Однако вторая проблема может стать роковой. Единственный способ избежать этих трудностей – «вручную» заменять все строки BSTR на строки LPSTR, используя массив байтов. Получение адреса переменной пользовательского типа API­программисту часто требуется получить адрес переменной пользователь­ ского типа. Рассмотрим, например, такую структуру: Type utExample sString As String iInteger As Integer End Type Dim uEx As utExample Предположим, требуется определить адрес переменной uEx. Сразу обратите внимание, что адрес структуры совпадает с адресом ее первого члена. Теперь рассмотрим следующую программу: Debug.Print VarPtr(uEx) Debug.Print VarPtr(uEx.sString) Debug.Print VarPtr(uEx.iInteger) Debug.Print Debug.Print rpiVarPtr(uEx) Debug.Print rpiVarPtr(uEx.sString) Debug.Print rpiVarPtr(uEx.iInteger) Вывод выглядит таким образом: 1243836 1243836 1243840 1243824 1243820 1243840
133 Вы видите, что VarPtr выводит адреса такими, какими и следовало их ожи­ дать: адрес uEx совпадает с адресом uEx.sString, а адрес uEx.iInteger сдви­ нут на четыре байта, за счет того, что четыре байта содержится в BSTR. С другой стороны, rpiVarPtr оказалась восприимчивой к преобразованию BSTR–ABSTR, которое произошло с членом структуры, то есть с BSTR. Зависимость между первым и вторым адресом во второй группе может выгля­ деть странной, однако вспомните, что каждый вызов rpiVarPtr сопровождается преобразованием и невозможно сравнивать адреса двух разных вызовов, каждый из которых связан с преобразованием. Третий адрес совпадает с первоначальным адресом целочисленного члена структура. В этом вызове преобразования не было: Debug.Print rpiVarPtr(uEx.iInteger) так как параметр не являлся BSTR. Таким образом, можно использовать внешнюю функцию, такую как rpiVarPtr, для вычисления адреса структуры при условии, что данная структура имеет, по крайней мере, один параметр, не относящийся к BSTR. В этом случае вы получаете адрес одного из таких параметров и считаете в обратном направлении до начала структуры. Адрес переменной пользовательского типа
Глава 7. Функции получения системной информации Давайте попробуем применить кое­что из того, что уже узнали, непосредственно на практике. Один из разделов Win32 API, который доступен и тем, кто не слиш­ ком много знает о принципах функционирования Windows, – это раздел, касаю­ щийся системной информации. Существует несколько API­функций, которыми можно воспользоваться для получения информации о компьютере. Win32 API­функции получения системной информации перечислены в табл. 7.1 . Таблица 7.1. Функции получения системной информации APIфункции GetComputerName GetSystemMetrics GetWindowsDirectoty GetKeyboardType GetTempPath SetComputerName GetSysColor Get UserName SetSysColors CetSystemDirectory GetVersion SystemParametersInfo GetSystemlnfo GetVersionEx Рассмотрим некоторые из этих функций. Имя компьютера Функция GetComputerName используется для получения текущего имени компьютера. Связанная с ней SetComputerName используется для присвоения имени компьютеру. Объявление в VC++ записывается таким образом: BOOL GetComputerName( LPTSTR lpBuffer, // Адрес буфера имени. LPDWORD nSize // Адрес размера буфера имени. ); Здесь используются следующие параметры: LPSTR, указатель на DWORD, тип возвращаемого значения – BOOL. Ниже приведен код соответствующей VB­де­ кларации: Declare Function GetComputerName Lib "kernel32" Alias "GetComputerNameA" (_ ByVal lpBuffer As String, _ nSize As Long _ ) As Long
135 В соответствии с документацией, выполнение функции GetComputerName в Windows 9x завершится неудачей, если размер буфера входных данных меньше, чем величина константы MAX_COMPUTERNAME_LENGTH + 1. Эта константа опре­ делена в WINBASE.H как #define MAX_COMPUTERNAME_LENGTH 15 что в VB записывается таким образом: Public Const MAX_COMPUTERNAME_LENGTH = 15 Теперь у вас есть все, чтобы написать небольшую функцию, возвращающую имя компьютера: Public Function GetTheComputerName As String Dim s As String s = String(MAX_COMPUTERNAME_LENGTH + 1, 0) lng = GetComputerName(s, MAX_COMPUTERNAME_LENGTH) GetComputerName = Trim0(s) End Function Следует напомнить, что Trim0 – это простая функция, усекающая строку после первого нулевого символа. На моем компьютере возвращаемое значение равно SRCOMPUTER Пути к системным каталогам Windows Функции GetWindowsDirectory, GetSystemDirectory и GetTempPath находят путь к каталогу, к системному каталогу и к каталогу временных файлов Windows. Например, функция GetSystemDirectory определена как: UINT GetSystemDirectory ( LPTSTR lpBuffer, // Адрес буфера системного каталога. UINT uSize // Размер буфера каталога. ); UINT GetWindowsDirectory( LPTSTR lpBuffer, // Адрес буфера каталога Windows. UINT uSize // Размер буфера каталога. ); DWORD GetTempPath( DWORD nBufferLength, // Размер буфера в символах. LPTSTR lpBuffer // Указатель на буфер пути к каталогу // временных файлов. ); Пути к системным каталогам Windows
13 Функции получения системной информации Соответствующие VB декларации записываются так: Declare Function GetSystemDirectory Lib "kernel32" Alias _ "GetSystemDirectoryA" ( _ ByVal lpBuffer As String, _ ByVal nSize As Long _ ) As Long Declare Function GetWindowsDirectory Lib "kernel32" _ Alias "GetWindowsDirectoryA" ( _ ByVal lpBuffer As String, _ ByVal nSize As Long _ ) As Long Declare Function GetTempPath Lib "kernel32" Alias "GetTempPathA" ( _ ByVal nBufferLength As Long, _ ByVal lpBuffer As String _ ) As Long Каждая из этих функций возвращает количество символов, помещенных в буфер строки. Размер буферов должен быть на единицу больше длины соответс­ твующей символьной константы: Public Const MAX_PATH = 260 Далее представлена примерная программа: Dim s As String, lng As Long s = String(MAX_PATH + 1, 0) lng = GetWindowsDirectory(s, MAX_PATH) Debug.Print Left$(s, lng) lng = GetSystemDirectory (s, MAX_PATH) Debug.Print Left$(s, lng) lng = GetTempPath(MAX_PATH, s} Debug.Print Left$(s, lng) На моем компьютере вывод такой: C:\WINNT C:\WINNT\System32 C:\TEMP\ Для каждого случая в документации говорится, что возвращаемое значение – количество символов, скопированных в буфер, не считая завершающего нуля. Поэтому можно использовать функцию Left$ для безошибочного выделения возвращаемой строки. С другой стороны, о функции GetComputerName в доку­ ментации сказано только то, что в случае успеха возвращаемое значение не равно нулю. Поэтому вместо функции Left$ использована функция Trim0.
13 Версия операционной системы Функция GetVersionEx возвращает информацию о версии операционной системы Windows и может использоваться для определения рабочей системы – Windows 95, Windows 98 или Windows NT. Она объявляется как BOOL GetVersionEx( LPOSVERSIONINFO lpVersionInformation // Указатель на структуру // с информацией о версии. ); где lpVersionInformation – указатель на структуру OSVERSIONINFO, которая определена следующим образом: typedef struct _OSVERSIONINFO { DWORD dwOSVersionInfoSize; DWORD dwMajorVersion; DWORD dwMinorVersion; DWORD dwBui1dNumber; DWORD dwPlatformId; TCHAR szCSDVersion[ 128 ]; } OSVERSIONINFO; Давайте посмотрим, что же говорится об этой структуре в документации. dwOSVersionInfoSize Задает размер структуры OSVERSIONINFO в байтах. Для структур это являет­ ся общим требованием. Так как DWORD – четырехбайтовое беззнаковое типа long и поскольку VB преобразует строку из 128 символов в массив символов ANSI из 128 байт, общий размер структуры составляет: 4 × 5 + 128 = 148 байт. Это значение возвращает функция Len, но не LenB, которая не принимает в расчет преобразо­ вание из Unicode в ANSI. dwMajorVersion Указывает номер основной версии операционной системы. Например, для Windows NT версии 3.51 номер основной версии – 3. Для Windows NT 4.0 и Windows 9x номер основной версии – 4. dwMinorVersion Указывает дополнительный номер версии операционной системы. Например, для Windows NT версии 3.51 дополнительный номер версии – 51. Для Windows NT 4.0 и Windows 95 дополнительный номер версии – 0. Для Windows 98 допол­ нительный номер версии – 10. dwBuildNumber Указывает номер сборки операционной системы для Windows NT. Для Windows 9x два младших байта содержат номер сборки операционной системы, а два стар­ ших байта – номер основной версии и дополнительный номер версии. Версия операционной системы
13 Функции получения системной информации dwPlatformId Идентифицирует платформу операционной системы, может иметь одно из следующих значений:  VER_PLATFORM_WIN32s (= 0). Win32s, работающая на Windows 3.1;  VER_PLATFORM_WIN32_WINDOWS (= 1). Win32, работающая на Windows 95 или Windows 98;  VER_PLATFORM_WIN32_NT (= 2). Win32, работающая на Windows NT. szCSDVersion В Windows NT содержит строку, завершающуюся нулевым символом, напри­ мер «Service Pack 3», которая указывает самую последнюю версию установленного в системе служебного пакета программ (service pack). Строка будет пустой, если служебный пакет не установлен. В Windows 95 включает строку с завершающим нулевым символом, в которой может быть произвольная дополнительная инфор­ мация об операционной системе. Преобразование декларации функции GetVersionEx к виду VB дает следу­ ющий результат: Declare Function GetversionEx Lib "kernel32" Alias "GetVersionExA" ( _ lpVersionInformation As OSVERSIONINFO _ ) As Long Такой подход реализует передачу адреса структуры OSVERSIONINFO по ссылке. Чтобы перевести в VB определение структуры, нужно заменить типы DWORD на Long, а массив фиксированной длины типа TCHAR на строку фиксированной длины или на массив байтов. В упомянутом выше случае VB будет осуществлять обычное преобразование из Unicode в ANSI: Type OSVERSIONINFO dwOSVersionInfoSize As Long dwMajorVersion As Long dwMinorVersion As Long dwBuildNumber As Long dwPlatformId As Long szCSDVersion As String * 128 ' 128 символов. End Type Давайте это опробуем: Public Sub PrintVersionInfo Dim lret As Long Dim osverinfo As OSVERSIONINFO osverinfo.dwOSVersionInfoSize = Len(osverinfo) lret = GetVersionEx(osverinfo) Iflret=0Then RaiseApiError lret Else
13 Debug.Print "Version: " & osverinfo.dwMajorVersion & "." _ & osverinfo.dwMinorVersion Debug.Print "Build: " & osverinfo.dwBuildNumber Debug.Print "Platform ID: " & osverinfo.dwPlatformId Debug.Print "String: " & osverinfo.szCSDversion End If End Sub Вы найдете определение функции RaiseAPIError в конце главы 3. На моем компьютере получился такой вывод: Version: 4.0 Build: 1381 Platform ID: 2 String: Service Pack 3 Небольшая функция возвращает наименование операционной системы: Public Function GetOsVersion() As String ' Возвращает наименование операционной системы. Dim lret As Long Dim osverinfo As OSVERSIONINFO osverinfo.dwOSVersionInfoSize = Len(osverinfo) lret = GetVersionEx(osverinfo) Iflret=0Then GetОSVersion = "unknown" Else Select Case osverinfo.dwPlatformId & "/" & osverinfo.dwMajorVersion _ & "/" & osverinfo.dwMinorVersion Case "1/4/0" GetOSVersion = "Win95" Case "1/4/10" GetOSVersion = "Win98" Case "2/3/51" GetOSVersion = "WinNT351" Case "2/4/0" GetOSVersion = "WinNT4" End Select End If End Function Системные метрики Функция GetSystemMetrics получает информацию о метриках (системе еди­ ниц измерения) объектов операционной системы. Декларация в VC++ проста: Системные метрики
140 Функции получения системной информации int GetSystemMetrics( int nIndex // Системная метрика или установки конфигурации. ); Это переводится в VB так: Declare Function GetSystemMetrics Lib "user32" ( _ ByVal nIndex As Long _ ) As Long Параметр nIndex принимает значение одной из 49 возможных констант. Фун­ кция возвращает запрошенные единицы измерения (в общем случае в пикселах или в безразмерных единицах). Чтобы дать общее представление о типе возвращаемой информации, здесь приведены образцы констант для этой функции. Единицы измерения высоты и ши­ рины приведены в пикселах : Const SM_CMOUSEBUTTONS = 43 ' Количество клавиш мыши. Const SM_MOUSEWHEELPRESENT = 75 ' Истина (True), если мышь имеет ' колесо прокрутки. ' (Только Win NT 4 или Win 98.) Const SM_SWAPBUTTON = 23 ' Истина (True), если клавиши мыши ' можно поменять местами (мышь для ' левши). Const SM_CXBORDER = 5 ' Ширина и высота рамки окна. Const SM_CYBORDER = 6 Const SM_CXSCREEN = 0 ' Ширина и высота экрана. Const SM_CYSCREEN = 1 Const SM_CXFULLSCREEN = 16 ' Ширина и высота области ' приложения в полноэкранном режиме. Const SM_CYFULLSCREEN = 17 Const SM_CXHTHUMB = 10 ' Ширина прямоугольного курсора ' в горизонтальной полосе прокрутки. Const SM_CXICONSPACING = 38 ' Размеры ячейки сетки для ' значка в режиме просмотра с крупными ' значками. Const SM_CYICONSPACING = 39 Const SM_CYCAPTION = 4 ' Высота стандартной области заголовка. Системные параметры Функция SystemParamtersInfo – это мощная функция, предназначен­ ная для получения или установки всех системных параметров. Она может также в процессе установки параметра обновлять пользовательские профили. Ниже приведена ее декларация: BOOL SystemParametersInfo ( UINT uiAction, // Запрашиваемый или устанавливаемый системный // параметр.
141 UINT uiParam, // Зависит от принятого системного параметра. PVOID pvParam, // Зависит от принятого системного параметра. UINT fWinIni // Флаг обновления пользовательского профиля. ); Она может быть переведена в VB так: Declare Function SystemParametersInfo Lib "user32" _ Alias "SystemParanetersInfoA" ( _ ByVal uiAction As Long, _ ByVal uiParam As Long, _ pvParam As Any, _ ByVal fWinIni As Long _ ) As Long Заметьте, что для корректного преобразования типов (type safety) можно изме­ нять тип данных pvParam в зависимости от принятого системного параметра. Эта мощная функция может принимать, по меньшей мере, 90 различных зна­ чений uiAction. Но мы рассмотрим всего один из примеров. Обратите внимание, что функция возвращает в случае успеха ненулевое значение и нуль в случае не­ удачи. Она устанавливает GetLastError, так что можно использовать значение VB Err.LastDLLError. Характеристики системных иконок Получать или устанавливать характеристики системных значков (icons), по­ добных тем, которые появляются на рабочем столе, можно с помощью констант: Public Const SPI_GETICONMETRICS = 45 Public Const SPI_SETICONMETRICS = 46 Согласно документации о SPI_GETICONMETRICS, параметр pvParam должен указывать на структуру ICONMETRICS, в которую записываются запрашивае­ мые сведения. В SPI_SETICONMETRICS параметр pvParam должен ссылаться на структуру ICONMETRICS, которая содержит новые параметры. В обоих случаях нужно также присвоить члену cbSize структуры ICONMETRICS и параметру uiParam в SystemParametersInfo значение размера структуры в байтах. Объявление структуры ICONMETRICS выглядит так: typedef struct tagICONMETRICS { UINT cbSize; int iHorzSpacing; int iVertSpacing; int iTitleWrap; LOGFONT lfFont; } ICONMETRICS, FAR *LPICONMETRICS; А структура LOGFONT объявляется следующим образом: typedef struct tagLOGFONT { // lf. LONG lfHeight; LONG lfWidth; Системные параметры
142 Функции получения системной информации LONG lfEscapement; LONG lfOrientation; LONG lfWeight; BYTE lfItalic; BYTE lfUnderline; BYTE lfStrikeOut; BYTE lfCharSet; BYTE lfOutPrecision; BYTE lfClipPrecision, BYTE lfQuality; BYTE lfPitchAndFamily; TCHAR lfFaceName[LF_FACESIZE]; } LOGFONT; где Public Const LF_FACESIZE = 32 Здесь есть важный момент, который следует отметить. Очень часто одна струк­ тура содержит указатели на другие структуры. Однако в данном случае переменная lfFont является на самом деле структурой, а не указателем на нее. Следователь­ но, необходимо объединить обе структуры при создании версии VB: Public Type ICONMETRICS cbSize As Long iHorzSpacing As Long iVertSpacing As Long iTitleWrap As Long lfHeight As Long lfWidth As Long lfEscapement As Long lfOrientation As Long lfWeight As Long lfItalic As Byte lfUnderline As Byte lfStrikeout As Byte lfCharset As Byte lfOutPrecision As Byte lfClipPrecision As Byte lfQuality As Byte lfPitchAndFamily As Byte lfFaceName As String * LF_FACESIZE End Type Заметьте, что эта структура построена грамотно с точки зрения выравнивания членов. Под lfFaceName можно было бы использовать и массив байтов, и строку фиксированной длины. Но при выводе возвращаемого значения проще использо­ вать строку фиксированной длины. Далее приведена программа, которая использует эту функцию: Public Sub PrintIconMetrics
143 Dim im As ICONMETRICS im.cbSize = Len(im) lret = SystemParametersInfo(SPI_GETICONMETRICS, Len(im), im, 0&) Iflret=0Then RaiseApiError Err.LastDllError Else Debug.Print "Hor Spacing:" & im.iHorzSpacing Debug.Print "Vert Spacing:" & im.iVertSpacing Debug.Print im.lfFaceName & "/" End If End Sub Вывод на моем компьютере был такой: Hor Spacing:101 Vert Spacing:109 MS Sans Serif Системные цвета Функции GetSysColor и SetSysColors используются для получения и установки цветов различных элементов системы, таких как кнопки, строки заго­ ловков и т.д . Цветовой палитрой может также управлять пользователь с помощью апплета Display (Экран) на панели Control Panel (Панель управления). Декларация GetSysColor проста: DWORD GetSysColor ( int nIndex // Элемент экрана. ); где nIndex может принимать значение одной из множества символьных констант, например #define COLOR_ACTIVECAPTION 2 В VB это выглядит так: Declare Function GetSysColor Lib "user32" (ByVal nIndex As Long) As Long Public Const COLOR_ACTIVECAPTION = 2 Возвращаемое значение – это цвет в формате RGB. В частности, каждый цвет занимает один байт в возвращаемом значении типа unsigned long: красный цвет – младший байт, зеленый – следующий байт, далее – синий цвет. Самый старший байт равен нулю. Байты цветов представлены в переменной типа long в обратном порядке, поскольку при записи переменной в память байты распола­ гаются от младших к старшим. Объявление функции SetS ysColors: BOOL WINAPI SetSysColors ( int cElements, // Количество изменяемых // элементов. CONST INT *lpaElements, // Адрес массива элементов. CONST COLORREF *lpaRgbValues // Адрес массива значений RGB. ); Системные цвета
144 Функции получения системной информации Здесь cElements определяет количество системных элементов, цвет которых требуется изменить; lpaElements – указатель на целочисленный массив VC++, который содержит индексы изменяемых элементов; lpaRgbValues ссылается на целочисленный массив VC++ новых значений цвета в формате RGB. Версия VB записывается так: Declare Function SetSysColors Lib "user32" ( _ ByVal nChanges As Long, _ lpSysColor As Long, _ lpColorValues As Long _ ) As Long Здесь, перед тем как вызывать функцию, необходимо задать первые элементы для каждого массива­параметра. И наконец, заметим, что обе функции возвращают нуль в случае успешного завершения и устанавливают значение GetLastError. В следующем примере исходный цвет строки заголовка активного окна будет изменяться на красный каждые полсекунды. Public Function FlashTitleBarColor() Dim i As Integer Dim lret As Long Dim SaveColor As Long Dim lIndices(0 To 0) As Long Dim lNewColors(0 To 0) As Long ' Получим и сохраним текущий цвет. SaveColor = GetSyscolor (COLOR_ACTIVECAPTION) Debug.Print "Current color:" & Hex(SaveColor) Fori=1To5 ' Изменяем цвет на красный. lIndices (0) = COLOR_ACTIVECAPTION lNewColors(0) = &HFF lret = SetSysColors(1&, lIndices(0), lNewColors(0)) Iflret=0Then RaiseApiError Err.LastDllError End If Delay 0.5 ' Восстанавливаем исходный цвет. lIndices(0) = COLOR_ACTIVECAPTION lNewColors(0) = SaveColor lret = SetSysColors(1&, lIndices(0), lNewColors(0)) If lret 0 Then RaiseApiError Err.LastDllError End If Delay 0.5
145 Next End Function Подпрограмма Delay имеет следующий код: Sub Delay(rTime As Single) ' Задержка на rTime с (min=.01, max=300). Dim OldTime As Variant ' Береженого бог бережет. If rTime < 0.01 Or rTime > 300 Then rTime = 1 OldTime = Timer Do DoEvents Loop Until Timer  OldTime >= rTime End Sub Системные цвета
Глава 8. Обработка исключений По мере продвижения по книге, выполнения примеров программ и проведения собственных экспериментов вы, возможно, столкнетесь со множеством так на­ зываемых общих ошибок защиты (General Protection Faults – GPF). Это вполне нормально. Вероятно, вы даже подумаете, что вам нечего делать с этими довольно бесполезными окнами с сообщением об ошибке, которые сопровождают каждую GPF, возникающую в процессе вызова вашей программой Win32 API. Однако это не так. В данной главе рассказывается о том, как Windows обрабатывает ошибки системы и приложений, которые возникают при обращении к Win32 API, и как можно заменить установленную по умолчанию обработку ошибок (или компен­ сировать ее отсутствие) в программах на Visual Basic. Отслеживание GPF Прежде чем обсуждать обработку ошибок GPF, рассмотрим следующую про­ грамму: Dim lpDest As Long Dim lng As Long lng=5 CopyMemory ByVal lpDest, ByVal VarPtr(lng), 1 Программа приведет к ошибке GPF, так не заданы адреса назначения lpDest и данный адрес просто проинициализирован нулем. Но за попыткой записи в об­ ласть памяти с нулевым адресом последует GPF, причем на экран будет выведено сообщение, показанное на рис. 8.1. Если нажать кнопку OK, то Windows, конечно, сразу завершит работу прило­ жения. Можно ли восстановиться после такого рода ошибки, как при выполнении только программы VB, без API­вызовов? Рис. 8.1. Попытка записи в область памяти с адресом 0
14 В этом случае происходит следующее. Когда при выполнении процесса воз­ никает исключительная ситуация, или исключение (еще одно название ошибки), Windows ищет обработчик данного исключения (exception handler). При создании процесса операционная система по умолчанию устанавливает для него обработ­ чик ошибок, называемый UnhandledExceptionFilter. Если программист не задействовал другой обработчик исключения, то вызывается эта функция (пос­ тоянная настройка). UnhandledExceptionFilter сначала проверяет, не выполняется ли програм­ ма в режиме отладки и не подключена ли она к отладчику. Если результат отрица­ тельный, а для VB­программы это так и есть, функция выведет малоприятное сооб­ щение об ошибке, подобное тому, что показано на рис. 8.1. Затем, если пользователь выберет кнопку Cancel (Отмена), Windows запустит отладчик для этого потока. (По умолчанию это отладчик Visual C++, если он установлен на данном компьютере.) А если пользователь выберет кнопку OK, функция UnhandledExceptionFilter просто вызовет функцию ExitProcess для завершения процесса, в котором вы­ полняется данный поток. Замена обработчика исключения по умолчанию Теперь давайте посмотрим, существует ли функция Win32 API с названием SetUnhandledExceptionFilter. Согласно документации, данная функция позволяет приложению заменять высокоуровневый обработчик исключения, ко­ торый Win32 помещает в вершине каждого потока и процесса. VB­синтаксис у функции SetUnhandledExceptionFilter такой: Declare Function SetUnhandledExceptionFilter Lib "kernel32" ( _ ByVal lpTopLevelExceptionFilter As Long _ ) As Long где параметр lpTopLevelExceptionFilter является адресом нового обработ­ чика исключения (заменяющего обработчик, установленный по умолчанию). Итак, нужно только сделать вызов: SetUnhandledExceptionFi1ter AddressOf NewExceptionHandler где NewExceptionHandler – новый обработчик исключения в виде стандартно­ го программного модуля VB. Функция SetUnhandledExceptionFilter возвращает адрес предыдущего обработчика исключения, но вам этот адрес не понадобится. Для восстановления исходного обработчика, устанавливаемого по умолчанию, можно просто вызвать эту функцию с параметром lpTopLevelExceptionFilter, равным нулю. Обработчик исключения, заменяющий устанавливаемый по умолчанию Обработчик исключения, заменяющий устанавливаемый по умолчанию, дол­ жен иметь следующую сигнатуру: Function NewExceptionHand1er(ByRef lpExceptionPointers As _ EXCEPTION_POINTERS) As Long Замена обработчика исключения
14 Обработка исключений В параметре lpExceptionPointers функция принимает адрес структуры EXCEPTION_POINTERS, которая позволяет заменяющему обработчику ошибки получить информацию о данной исключительной ситуации. Сама структура оп­ ределена следующим образом: Type EXCEPTION_POINTERS pExceptionRecord As Long ' Указатель на структуру EXCEPTION_RECORD. pContextRecord As Long ' Указатель на структуру CONTEXT. End Type Второй член структуры – указатель на структуру CONTEXT, которая содержит информацию о состоянии компьютера на момент вызова обработчика исключе­ ния (то есть на момент возникновения исключительной ситуации). Эта структура сообщает такую информацию, как, например, состояние регистров центрального процессора. Но не будем углубляться в детали, поскольку они не имеют отноше­ ния к нашей теме. Первый член указывает на следующую структуру: Type ECXEPTION_RECORD ExceptionCode As Long ExceptionFlags As Long pExceptionRecord As Long' Указатель на структуру EXCEPTION_RECORD. ExceptionAddress As Long NumberParameters As Long ExceptionInformation (EXCEPTION_MAXIMUM_PARAMETERS) As Long End Type Давайте кратко опишем составляющие этой структуры:  ExceptionCode. Задает код исключения. Варианты возможных значений кода приведены в табл. 8.1;  ExceptionFlags. Значение 0 указывает на некритическую исключитель­ ную ситуацию, а значение EXCEPTION_NONCONTINUABLE_EXCEPTION (&HC0000025&) – на критическую. Любая попытка продолжить выполнение программы после возникновения критического исключения приведет к возбужде­ нию нового исключения с кодом EXCEPTION_NONCONTINUABLE_EXCEPTION;  pExceptionRecord. Согласно документации, исключения могут быть вло­ женными (хотя в документации и не говорится о том, как это может про­ изойти). Если член pExceptionRecord не равен нулю, то он указывает на структуру EXCEPTION_RECORD, в которой содержится информация о вложенном исключении;  ExceptionAddress. Содержит адрес инструкции, вызвавшей исключи­ тельную ситуацию;  NumberParameters. Определяет количество значимых (valid) элементов массива ExceptionInformation (см. следующий пункт);  ExceptionInformation. Используется только в одном случае, когда ис­ ключительная ситуация связана с нарушением доступа (access violation) (код исключения EXCEPTION_ACCESS_VIOLATION), NumberParameters равня­ ется двум, а ExceptionInformation(0) содержит нуль, если была попытка
14 неразрешенного (illegal) чтения, и единицу, если была попытка неразрешенной записи. И в том, и в другом случае ExceptionInformation(1) хранит адрес памяти, по которому пытались читать или записывать данные. Таблица 8.1. Коды исключительных ситуаций Exception Codes Value EXCEPTION_ACCESS_VIOLATION &HC0000005& EXCEPTION_ARRAY_BOUNDS_EXCEEDED &HC000008C& EXCEPTION_BPEAKPOINT &H80000003& EXCEPTION_DATATYPE_MISALIGNMENT &H80000002& EXCEPTION_FLT_DENORMAL_OPERAND &HC000008D& EXCEPTION_FLT_DIVIDE_BY_ZERO &HC000008E& EXCEPTION_FLT_INEXACT_RESULT &HC000008F& EXCEPTION_FLT_INVALID_OPERATION &HC0000090& EXCEPTION_FLT_OVERFLOW &HC0000091& EXCEPTION_FLT_STACK_CHECK &HC0000092& EXCEPTION_FLT_UNDERFLOW &HC0000093& EXCEPTION_GUARD_PAGE &H80000001& EXCEPTION_ILLEGAL_INSTRUCTION &HC000001D& EXCEPTION_IN_PACE_ERROR &HC0000006& EXCEPTION_INT_DIVIDE_BY_ZERO &HC0000094& EXCEPTION_INT_OVERFLOW &HC0000095& EXCEPTION_INVALID_DISPOSITION &HC0000026& EXCEPTION_INVALID_HANDLE &HC0000008& EXCEPTION_NONCONTINUABLE_EXCEPTION &HC0000025& EXCEPTION_PRIV_INSTRUCTION &HC0000096& EXCEPTION_SINGLE_STEP &HC0000004& EXCEPTION_STACK_OVERFLOW &HC00000FD& Новый обработчик исключения NewExceptionHandler, согласно докумен­ тации, должен возвращать одно из следующих значений, которые определяют дальнейший ход событий:  EXCEPTION_EXECUTE_HANDLER приводит к завершению процесса;  EXCEPTION_CONTINUE_EXECUTION продолжает выполнение с момента возникновения исключения. Здесь это будет означать повторение данного исключения;  EXCEPTION_CONTINUE_SEARCH вызывает обработчик исключения, уста­ новленный Windows по умолчанию. Но документация адресована программистам VC++. У программистов VB, как всегда, есть свои уловки. Чтобы понять, в чем эти хитрости заключаются, давайте вспомним, как VB поступает с ошибками в собственных программах. Если возникает ошибка, он Замена обработчика исключения
150 Обработка исключений проверяет процедуру, вызвавшую появление ошибки, на наличие обработчика исключения (On Error...) . Если он не задан, VB поднимается по стеку вызовов в поисках установленной программы обработки ошибок. Если такой не находится, VB выводит сообщение об ошибке и завершает выполнение программы. Однако, просматривая стек вызовов, VB будет пропускать все внешние проце­ дуры, то есть любой код, написанный не на VB. На рис. 8.2 показан стек вызовов в тот момент, когда контрольная точка находится на новом обработчике исключения NewExceptionHandler. Рис. 8 .2 . Стек вызовов Первой на рис. 8.2 является процедура обработки события Click (см. листинг 8.1), которая вызывает исключительную ситуацию в динамической библиотеке kernel32.dll, экспортирующей функцию CopyMemory. Таким образом, ссылка на [<Non­Basic Code>] (программа, написанная не на Basic) на рис. 8.2 – это ссылка на внешнюю функцию CopyMemory. Листинг 8.1. Процедура обработки события Click, приводящая к исключительной ситуации Private Sub cmdRaiseException_Click() Dim lpDest As Long Dim lng As Long On Error GoTo ERR_RaiseException lng=5 CopyMemory ByVal lpDest, ByVal varPtr(lng), 1 MsgBox "Recovered from GPF" Exit Sub ERR_RaiseException: MsgBox Err.Description Resume Next End Sub Затем в новом обработчике исключения NewExceptionHandler делаем сле­ дующее:  собираем информацию об исключительной ситуации в строковой перемен­ ной sError;
151  сознательно избегаем включения обработчика ошибок VB в процедуру NewExceptionHandler. Другими словами, не используем в ней оператор On Error;  намеренно помещаем в NewExceptionHandler код для возбуждения ошибки VB, используя значение sError в качестве описания ошибки, как в операторе Err.Raise 1000, "NewExceptionHandler", sError Если теперь возникнет исключительная ситуация, вызов Err.Raise заставит VB искать обработчик ошибки VB. Так как в процедуре NewExceptionHandler, вызывающей исключительную ситуацию, обработчика нет, VB начнет поиск в стеке вызовов, пропуская внешний код, вызвавший исключение. Следовательно, в ситуации, проиллюстрированной на рис. 8.2, выполнится обработчик исключе­ ния процедуры cmdRaiseException_Click, выводя на экран описание ошибки в окне сообщения VB (см. рис. 8.3). Рис. 8 .3. Окно сообщения VB с описанием GPF Теперь вы полностью контролируете ситуацию. Например, в коде обработки ошибки в процедуре cmdRaiseException_Click вы вызываете Resume Next для продолжения выполнения программы, но, безусловно, здесь имеются и другие возможности. Таким образом, приложение VB не завершит работу аварийно. Прежде чем обратиться к примеру, следует упомянуть, что, хотя этот подход к обработке исключений может быть очень полезен, в целях экономии места и ради ясности изложения основного замысла рассмотренный метод в примерах этой книги не используется. Пример программы обработчика исключения В листинге 8.2 показана программа для нового обработчика исключения NewExceptionHandler. Полностью приложение содержится в архиве на сайте издательства «ДМК Пресс» www.dmkpress.ru . Листинг 8.2 . Новый обработчик исключения NewExceptionHandler Function NewExceptionHandler(ByRef lpExceptionPointers As _ EXCEPTION_POINTERS) As Long ' Нет необходимости задавать возвращаемое значение, так как Err.Raise ' изменит поток управления. Dim er As EXCEPTION_RECORD Dim sError As String Пример программы обработчика исключения
152 Обработка исключений ' Делаем копию той части записи об исключении, ' которая передается в структуре EXCEPTION_POINTERS. CopyMemory er, ByVal lpExceptionPointers.pExceptionRecord, Len(er) ' Задаем строку с описанием ошибки. Do sError = GetException(er.ExceptionCode) ' Специальная обработка нарушения доступа – получаем адрес. If sError = "EXCEPTION_ACCESS_VIOLATION" Then sError = sError & "  Instr @ &H" & Hex(er.ExceptionAddress) _ & " tried illegally to " _ & IIf(er.ExceptionInformation(O) = 0, _ "read from address", "write to address") _ & " &H" & Hex(er.ExceptionInformation(1)) End If ' Проверка на вложенную ошибку. If er.pExceptionRecord = 0 Then Exit Do ' Вложенная ошибка существует. ' Заменяем эту ошибку вложенной. CopyMemory er, Byval er.pExceptionRecord, Len(er) ' Новая строка для следующей ошибки. sError = sError & vlrrtf Loop ' Генерируем ошибку, чтобы подняться по стеку вызовов, обходя внешний ' источник ошибки. Err.Raise 1000, "NewExceptionHand1er", sError End Function
Глава 9. Архитектура Windows Глава 10. Объекты и их дескрипторы Глава 11. Процессы Глава 12. Потоки Глава 13. Архитектура памяти Windows Глава 14. РЕ-файлы Часть II Операционная система Windows
Глава 9. Архитектура Windows В этой главе дан обзор архитектуры операционной системы Windows NT. Для обсуж­ дения выбрана Windows NT, так как, по мнению Microsoft, это будущее Windows. Ли­ ния Windows 9x закончится на Windows 981, и все мы рано или поздно будем рабо­ тать с одной из преемниц Windows NT (первая из них называется Windows 2000).2 В конце главы кратко обсуждаются отличия Windows 9x и Windows NT. Задача, поставленная в этой главе, – дать самое общее представление об ар­ хитектуре Windows. Не будем углубляться в детали больше, чем это необходимо для достижения цели. Учтите, что устаревшая 16­разрядная Windows описываться не будет, так как ее сопоставление с 32­разрядной Windows уже не актуально (за очень редкими исключениями). Таким образом, упоминание Windows всегда будет относиться к одной из 32­разрядных операционных систем. Процессы и потоки Приложение (application) Windows – это совокупность исполняемых программ и вспомогательных файлов. Например, Microsoft Word представляет собой одно из популярных приложений Windows. Процессом называется исполняемый экземп­ ляр приложения. Заметим, что в большинстве случаев пользователь может запус­ кать несколько экземпляров (копий) одного и того же приложения одновременно. Каждый исполняемый экземпляр – это отдельный процесс со своей собственной областью памяти. Если быть более точным, процессом (process) называется исполняемый экзем­ пляр (running instance) приложения и комплект ресурсов, отводящийся данному исполняемому приложению. Поток (thread) – это внутренняя составляющая процесса, которой операцион­ ная система выделяет процессорное время для выполнения кода. Именно потоки исполняют программный код, а не процессы. Каждый процесс должен иметь как минимум один поток. Конечно, основное назначение потоков – дать процессу возможность поддерживать несколько ветвей управления, то есть выполнять боль­ 1 Вопреки прогнозам линия Windows 9x не закончилась. Следующая после Windows 98 версия уже вышла и называется Windows Millennium Edition (Windows ME). – Прим. перев. 2 Видимо, автор не совсем точен или текст несколько устарел. Как известно, стратегическое на­ правление развития операционных систем (ОС) компании Microsoft – это слияние линий Windows 9x и Windows NT в одну мощную и удобную платформу для целого семейства ОС Windows, от мобильных до корпоративных. Выпущена бета­версия первого варианта такой ОС с кодовым названием Windows XP. XP станет также первой ОС компании Microsoft, имеющей 64­разрядную версию. – Прим. перев.
155 ше действий одновременно. В многопроцессорной конфигурации (компьютер с несколькими процессорами) Windows NT (но не Windows 9x) может распре­ делять потоки по процессорам, реально обеспечивая параллельную обработку. В однопроцессорной конфигурации процессор должен выделять кванты времени (time slices) каждому исполняемому в данный момент потоку. Кванты времени более подробно будут обсуждаться в главе 12. Архитектура Windows На рис. 9.1 в обобщенном виде представлена архитектура Windows NT. Давай­ те рассмотрим некоторые из изображенных пунктов. Пользовательский режим Пользовательские приложения NTDLL.DLL Подсистема Win32 Win32 API (Kernel32.dll, User32.dll, GDI32.dll) Функции среды Системные процессы пользовательского режима Диспетчер сеансов WinLogon  процесс регистрации Режим ядра Системный процесс режима ядра Сервисы исполнительной системы Системные сервисы Ф у н к ц и и б и б л и о т е к и в р е м е н и в ы п о л н е н и я Д и с п е т ч е р п р о ц е с с о в и п о т о к о в Д и с п е т ч е р о б ъ е к т о в Д и с п е т ч е р в и р т у а л ь н о й п а м я т и Win32K.SYS Диспетчер окон Цифровой графический интерфейс Драйверы графических устройств Ядро Слой абстрагирования от аппаратуры (HAL) Диспетчер ввода/вывода Диспетчер кэша Диспетчер файловой системы Драйверы устройств Аппаратура Рис. 9 .1. Архитектура Windows NT в упрощенном виде Архитектура Windows
15 Архитектура Windows Режим ядра и пользовательский режим Микропроцессор Pentium имеет четыре уровня привилегий (privilege levels),1 известных также как кольца (rings), которые управляют, например, доступом к памяти, возможностью использовать некоторые критичные команды процессора (такие как команды, связанные с защитой) и т.д . Каждый поток выполняется на одном из этих уровней привилегий. Кольцо 0 – наиболее привилегированный уро­ вень, с полным доступом ко всей памяти и ко всем командам процессора. Кольцо 3 – наименее привилегированный уровень. Для обеспечения совместимости с системами на базе процессоров, отличных от тех, что выпускает компания Intel, Windows поддерживает только два уров­ ня привилегий – кольца 0 и 3. Если поток работает в кольце 0, говорят, что он выполняется в режиме ядра (kernel mode). Если поток выполняется в кольце 3, говорят, что он работает в пользовательском режиме (user mode). Низкоуровневый код операционной системы действует в режиме ядра, тогда как пользовательские приложения выполняются в основном в пользовательском режиме. Заметим, что прикладной поток может переключаться из пользовательского режима в режим ядра при вызове некоторых API­функций, которые требуют более высокого уровня привилегий, например, связанных с доступом к файлам или с выполнением функций, ориентированных на графические операции. В действи­ тельности некоторые пользовательские потоки могут работать в режиме ядра даже больше времени, чем в пользовательском режиме. Но как только завершается выполнение той части кода, которая относится к режиму ядра, пользовательский поток автоматически переключается обратно в пользовательский режим. Такой подход лишает возможности писать код, пред­ назначенный для работы в режиме ядра, программист может только вызывать выполняющиеся в режиме ядра системные функции (system functions). При работе с Windows NT можно определить, когда поток выполняется в поль­ зовательском режиме, а когда – в режиме ядра. Запустите утилиту Performance Monitor (Системный монитор) из пункта Administrative Tools (Администрирова­ ние) меню Start. (Пуск). Выберите пункт Add To Chart (Добавить на диаграмму) из меню Edit (Правка). Затем добавьте % User Time (Процент работы в пользо­ вательском режиме) и % Privileged Time (Процент работы в привилегированном режиме) из списка Counters (Счетчики). Далее выполните что­нибудь связанное с интенсивными графическими операциями, например, откройте Windows Paint. На рис. 9.2 показаны выведенные на экран результаты этих действий в конкретный момент времени. Белая линия на рисунке – время выполнения в режиме ядра в процентах. Интересно, что драйверы устройств работают в режиме ядра. Это обстоятель­ ство имеет два следствия. Во­первых, в отличие от неправильно выполняющего­ ся приложения неправильно работающий драйвер устройства может нарушить работу всей системы, так как он имеет доступ и ко всему системному коду, и ко 1 Четыре уровня привилегий – это общее свойство процессоров Intel, начиная с i80386, а не только процессора Pentium. – Прим. перев.
15 всей памяти. Во­вторых, прикладной программист может получить доступ к за­ щищенным ресурсам, написав драйвер псевдоустройства (fake device), хоть это и нелегкая задача. Рис. 9.2 . Пользовательский режим в сравнении с режимом ядра Сервисы Термин сервис (service) имеет в среде Windows множество значений. Ниже представлены некоторые из них, имеющие отношение к рассматриваемой теме:  сервис API – функция или подпрограмма API, которая реализует некоторое действие (сервис) операционной системы, такое как создание файла или работа с графикой (рисование линий или окружностей). Например, фун­ кция API CreateProcess используется в Windows для создания нового процесса;  системный сервис – недокументированная (undocumented) функция, кото­ рая может вызываться из пользовательского режима. Эти функции часто используются функциями Win32 API для предоставления низкоуровневых сервисов. Например, функция API CreateProcess для реального создания процесса вызывает системный сервис NTCreateProcess;  внутренний (internal) сервис – функция или подпрограмма, которая может вызываться только из кода, выполняемого в режиме ядра. Эти функции от­ носятся к низкоуровневой части кода Windows: к исполнительной системе Windows NT, к ядру или к слою абстрагирования от аппаратуры (HAL). Системные процессы Системные процессы (system processes) – это особые процессы, обслуживаю­ щие операционную систему. В системе Windows постоянно задействованы следу­ ющие системные процессы (учтите, что все они, кроме процесса system, выпол­ няются в пользовательском режиме): Архитектура Windows
15 Архитектура Windows  процесс idle, который состоит из одного потока, управляющего временем простоя процессора;  процесс system (название выбрано не очень удачно) – специальный про­ цесс, выполняющийся только в режиме ядра. Его потоки называются сис- темными потоками (system threads);  процесс Session Manager (диспетчер сеансов) – SMSS.EXE;  подсистема Win32 – CSRSS.EXE;  процесс регистрации в системе – WinLogon (WINLOGON.EXE). Вы можете убедиться в том, что эти системные процессы действительно вы­ полняются в системе, посмотрев на вкладку Processes (Процессы) программы Task Manager (Диспетчер задач) Windows NT. Кроме того, на рис. 9.3 показано приложение rpiEnumProcsNT, создание которого описывается в главе 11. (Бу­ дет выполнена также и версия для Windows 9x). Процессы idle, system, smss, csrss и WinLogon можно видеть в окне списка процессов вкладки Processes. Давайте рассмотрим вкратце некоторые из этих системных процессов. Рис. 9 .3. Окно приложения rpiEnumProcsNT
15 Процесс Session Manager Процесс Session Manager (SMSS.EXE) – один из первых процессов, со­ здаваемых операционной системой в процессе загрузки. Он выполняет важные функции инициализации, такие как создание переменных окружения системы; задание имен устройств MS DOS, например, LPT1 и COM1; загрузка той части подсистемы Win32, которая относится к режиму ядра; запуск процесса регистра­ ции в системе WinLogon. Процесс WinLogon Этот системный процесс управляет входом пользователей в систему и выходом из нее. Вызывается специальной комбинацией клавиш Windows Ctrl+Alt+Delete. WinLogon отвечает за загрузку оболочки Windows (обычно это Windows Explorer). Процесс system Процесс system состоит из системных потоков (system threads), являющихся потоками режима ядра. Windows и многие драйверы устройств создают потоки процесса system для различных целей. Например, диспетчер памяти формирует системные потоки для решения задач управления виртуальной памятью, диспет­ чер кэша использует системные потоки для управления кэш­памятью, а драйвер гибкого диска – для контроля над гибкими дисками. Подсистема Win32 Подсистема Win32 – основной предмет рассмотрения данной книги. Она является разновидностью подсистемы среды (environment subsystem). Другие подсистемы среды Windows (не показаны на рис. 9.1) включают POSIX и OS/2. POSIX является сокращением термина «переносимая операционная система на базе UNIX» (portable operating system based on UNIX) и реализует ограниченную поддержку операционной системы UNIX. Назначение подсистемы среды – служить интерфейсом между пользовательски­ ми приложениями и соответствующей частью исполнительной системы Windows. Каждая подсистема имеет свои функциональные возможности на базе единой исполнительной системы Windows. Любой выполняемый файл неразрывно связан с одной из этих подсистем. В самом деле, если заглянуть внутрь исполняемого файла с помощью встро­ енной в Windows программы быстрого просмотра, то можно обнаружить такую строку: Subsystem: Image runs in the Windows GUI subsystem. Это означает, что исполняемый файл связан с подсистемой Windows GUI, то есть с Win32. Если с помощью программы быстрого просмотра заглянуть в испол­ няемый файл режима командной строки (например, ftp.exe, который находится в системном каталоге Windows), то можно увидеть следующую строку: Subsystem: Image runs in the Windows character subsystem. Это приложение Win32, ориентированное на режим командной строки. Архитектура Windows
10 Архитектура Windows Как видно из рис. 9.1, подсистема Win32 содержит Win32 API в виде набора DLL, таких как KERNEL32.DLL, GDI32.DLL и USER32.DLL. Интересно отметить, что в Windows NT Microsoft перенесла часть подсистемы Win32 из пользовательского режима в режим ядра. В частности, драйвер устройс­ тва режима ядра WIN32K.SYS, который управляет отображением окон, выводом на экран, вводом данных с клавиатуры или при помощи мыши и передачей со­ общений. Он включает также библиотеку интерфейсов графических устройств (Graphical Device Interface library – GDI.DLL), используемую для создания гра­ фических объектов и текста. Вызов Win32 API-функций Когда приложение вызывает API­функцию из подсистемы Win32, может про­ изойти одно из нескольких событий:  если DLL подсистемы (например, USER32.DLL), экспортирующая данную API­функцию, содержит весь код, необходимый для выполнения функции, то функция выполняется и возвращает результат;  API­функции, вызываемой приложением, может потребоваться вызвать для выполнения вспомогательных действий дополнительный модуль, принадле­ жащий подсистеме Win32 (но не той DLL, которая экспортирует данную функцию);  API­функции, вызываемой приложением, могут понадобиться услуги не­ документированного системного сервиса. Например, чтобы создать новый процесс, API­функция CreateProcess вызывает недокументированный системный сервис NTCreateProcess для реального создания данного про­ цесса. Это делается с помощью функций библиотеки NTDLL.DLL, которая помогает осуществлять переход из пользовательского режима в режим ядра. Исполнительная система Windows Сервисы исполнительной системы Windows составляют низкоуровневую часть Windows NT режима ядра, включенную в файл NTOSKRNL.EXE. Иногда сервисы исполнительной системы делят на две группы: исполнитель- ную систему (executive), относящуюся к верхнему уровню, и ядро (kernel). Ядро – это самый нижний уровень операционной системы, реализующий наиболее фун­ даментальные сервисы, такие как:  планирование потоков;  обработку исключений;  обработку прерываний;  синхронизацию процессоров в многопроцессорной системе;  создание объектов ядра. Ниже приведены некоторые наиболее важные составляющие исполнительной системы:  диспетчер процессов и потоков создает и завершает и процессы, и потоки, используя сервисы низкоуровневого ядра;
11  диспетчер виртуальной памяти реализует механизм виртуальной памяти, который будет обсуждаться более подробно в главе 13;  диспетчер ввода/вывода реализует аппаратно­независимый ввод/вывод и взаимодействует с драйверами устройств;  диспетчер кэша управляет кэшированием диска;  диспетчер объектов создает объекты исполнительной системы Windows и управляет ими. Windows использует объекты для представления разнооб­ разных ресурсов, таких как процессы и потоки;  библиотеки времени выполнения содержат такие функции, как обработки строк и арифметические функции. Уровень абстрагирования от аппаратуры (HAL) Уровень абстрагирования от аппаратуры (HAL) – это библиотека режима ядра (HAL.DLL), которая реализует низкоуровневый интерфейс с аппаратурой. Компо­ ненты Windows и драйверы устройств от других компаний взаимодействуют с аппа­ ратурой посредством HAL. Существует много версий HAL под различные аппарат­ ные платформы. Подходящий уровень выбирается в процессе установки Windows. Отличие Windows x и Windows NT Давайте кратко рассмотрим некоторые из наиболее важных отличий между операционными системами Windows 9x и Windows NT:  Windows NT поддерживает симметричную многопроцессорную обработку (SMP), то есть Windows NT может использовать одновременно несколь­ ко процессоров. Термин «симметричная» объясняется тем, что Windows NT интерпретирует все процессоры одинаково и распределяет как потоки операционной системы, так и прикладные пользовательские программы по всем процессорам (в противоположность асимметричной схеме, когда один процессор закрепляется исключительно за операционной системой);  Windows NT способна работать на платформах, построенных не на базе про­ цессоров Intel. Например, Windows NT работает на системах с процессором PowerPC;  Windows NT является истинной 32­разрядной операционной системой, тог­ да как Windows 9x содержит значительное количество 16­разрядного кода, который был перенесен из Windows 3.1 . Как следствие, в Windows 9x облас­ ти памяти, закрепленные за операционной системой, доступны из пользо­ вательского режима, что делает Windows 9x гораздо менее стабильной, чем Windows NT;  Windows NT реализует надежную защиту файловой системы, которая от­ сутствует в Windows 9x;  и в той, и в другой операционных системах приложения могут совместно использовать память. Однако в Windows NT к этой памяти получают до­ ступ только те приложения, которые специально запросили данный ресурс, тогда как в Windows 9x разделяемая память доступна всем исполняемым программам. Отличие Windows x и Windows NT
Глава 10. Объекты и их дескрипторы Архитектура Windows базируется на использовании множества различных объек­ тов. Объект ядра (kernel object) – это структура данных, доступ к членам которой имеет только ядро Windows. Далее приведены примеры объектов ядра:  объект Process представляет процесс;  объект Thread определяет поток;  объект File представляет открытый файл;  объект Filemapping представляет отображаемый в память файл (memory­ mapped file), то есть файл, содержимое которого отображено непосредствен­ но на виртуальное адресное пространство и используется как физическая память;  объект Pipe используется для обмена данными между процессами;  объект Event является объектом синхронизации потоков, сигнализирую­ щим о завершении операции;  объект Mutex представляет собой объект синхронизации потоков, который может использоваться несколькими процессами;  объект Semaphore используется для того, чтобы учитывать ресурсы и сиг­ нализировать потоку о доступности ресурса на данный момент. Кроме объектов ядра, существуют также пользовательские объекты и объекты GDI, такие как меню, окна, шрифты, кисти и курсоры мыши. Дескрипторы Одной из характеристик любого объекта является дескриптор, который ис­ пользуется для идентификации этого объекта. Хотя к объектам ядра нельзя получить непосредственный доступ из поль­ зовательского режима, в Windows API есть функции, которые можно вызывать из данного режима для управления этими объектами. Это своего рода инкапсуляция (encapsulation), защищающая объекты от непредусмотренных или неразрешенных действий. Когда создается объект ядра посредством вызова соответствующей API­фун­ кции (CreateProcess, CreateThread, CreateFile и CreateFileMapping), функция возвращает дескриптор вновь созданного объекта. Такой дескриптор может быть передан другой API­функции для того, чтобы она могла управлять данным объектом. В общем, дескриптор объекта является зависимым от процесса (process­specific). Это означает, что он действует только в пределах данного процесса. Некоторые
13 идентификаторы, такие как ID процесса, наоборот, являются идентификаторами системного уровня. Другими словами, область их действия – все процессы систе­ мы. В примерах данной книги используются и дескрипторы, и идентификаторы процессов. Подсчет используемости Объект ядра принадлежит ядру Windows, а не процессу, создавшему этот объект (или любому другому процессу). Как вы позже узнаете, объекты могут использо­ ваться совместно многими процессами и применяться разными способами. У каж­ дого процесса, который работает с объектом, есть свой собственный, действующий в пределах данного процесса, дескриптор этого объекта. С учетом этого ядро должно поддерживать подсчет используемости (usage count) каждого объекта. Ядро уничтожает объект тогда, когда его используемость становится равной нулю, но не раньше. Таким образом, процесс, создавший дан­ ный объект, может закрыть (close) его дескриптор (посредством вызова API­фун­ кции CloseHandle), но объект не будет уничтожен, если какой­то другой процесс продолжает его использовать (имеет его дескриптор). Отметим также, что у объектов ядра есть атрибуты защиты, которые можно использовать для ограничения доступа к данным объектам. Фактически это одно из основных свойств, отличающих объекты ядра от пользовательских объектов и объектов GDI. Совместное использование объектов несколькими процессами Существует несколько способов совместного использования объекта несколь­ кими процессами. Наследование Когда процесс (а точнее поток этого процесса) создает объект ядра, он может указать, что дескриптор этого объекта наследуется (inheritable) порожденными (child) процессами, которые данный родительский процесс создает впоследс­ твии. В этом случае дескрипторы родительского и порожденного процессов одинаковы. Дублирование дескриптора Функция DuplicateHandle определяется следующим образом: BOOL DuplicateHandle( HANDLE hSourceProcessHandle, // Дескриптор процессаисточника. HANDLE hSourceHandle, // Копируемый дескриптор. HANDLE hTargetProcessHandle, // Дескриптор процессаприемника. LPHANDLE lpTargetHandle, // Указатель на дескрипторкопию. DWORD dwDesiredAccess, // Доступ для дескрипторакопии. BOOL bInheritHandle, // Флаг наследуемости дескриптора. DWORD dwOptions // Необязательные опции. ); Эта функция позволяет скопировать дескриптор объекта одного процесса в другой процесс. Новый дескриптор­копия, действующий в пределах своего про­ Дескрипторы
14 Объекты и их дескрипторы цесса, может иметь значение, отличное от значения дескриптора­источника, но это не существенно, так как все дескрипторы действуют только в пределах своих процессов. Именованные объекты Многим объектам ядра при их создании может быть присвоено имя. Областью действия имени является вся система. Это означает, что любой другой процесс мо­ жет получить доступ к объекту по его имени (если считать, конечно, что другому процессу это имя известно). Например, последний параметр функции HANDLE CreateFileMapping( HANDLE hFile, // Дескриптор отображаемого файла. LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // Необязательные атрибуты защиты. DWORD flProtect, // Защита отображаемого объекта. DWORD dwMaximumSizeHigh, // Старшие 32 бита размера объекта. DWORD dwMaximumSizeLow, // Младшие 32 бита размера объекта. LPCTSTR lpName // Имя объекта отображения файла. ); может использоваться для задания имени отображения файла. Предположим, создан объект Filemapping с именем MyFMO. Другой процесс может вызвать функцию OpenFileMapping с этим именем в качестве последне­ го аргумента. Функция вернет зависимый от процесса дескриптор объекта для использования во втором процессе. В виде альтернативы второй процесс может вызвать функцию CreateFileMapping, используя имя объекта в качестве ее последнего аргумента. Система определит, что объект Filemapping с таким именем уже существует и просто вернет его дескриптор. Здесь могут возникнуть проблемы, поскольку процесс считает, что он создает новый объект, тогда как в дейс­ твительности он получает дескриптор существующего объекта. Программист должен сразу проверить возвращенное функцией CreateFileMapping значение, чтобы правильно сориентироваться в ситуации. Пример отображения файла Данную главу завершает пример создания, совместного использования и уничто­ жения объекта ядра. Для этого примера воспользуемся объектом File­mapping. Windows способна интерпретировать файл на диске так, как если бы он был частью памяти. Для этого часть файла отображается на область адресного про­ странства виртуальной памяти. Связанные с памятью API­функции, подобные CopyMemory, могут затем использоваться для просмотра и изменения содержи­ мого файла на диске. Такой файл называется отображаемым в память (memory­ mapped file). Создаваемое в данном примере приложение будет выполнять следующие дейс­ твия: 1. Создаст объект File на базе существующего файла, используя API­функ­ цию CreateFile. Эта функция вернет дескриптор объекта File.
15 2. Использует дескриптор файла и API­функцию CreateFileMapping для создания объекта Filemapping. Функция вернет дескриптор объекта File­mapping. 3. Использует дескриптор объекта Filemapping, а также API­функцию MapViewOfFile для отображения части файла в память. Данная функция назначает область виртуальной памяти, выделяемой этому файлу. Базовый адрес выделенной области памяти является дескриптором представления этой области в виде отображения файла. 4. Использует базовый адрес и функцию CopyMemory для чтения из отоб­ ражаемого файла и записи в этот же файл. Программа просто изменяет регистр текста в тестовом файле Mapped.txt. 5. Закрывает все дескрипторы. Так как код из этого примера задействован в двух приложениях VB (второе иллюстрирует совместное использование объектов) и так как вы, возможно, за­ хотите применять его в своих собственных приложениях, то лучшим местом для него будет модуль класса. В сущности, не так уж трудно создать класс с именем CfileMapping со всеми необходимыми свойствами и методами для создания объектов File, File­mapping и представлений отображений в память. В лис­ тинге 10.1 представлен весь код класса CfileMapping. Листинг 10.1 . Класс CfileMapping Option Explicit Private mFileMappingName As String Private mFileViewBase As Long Private mFileMappingHandle As Long Private mFileHandle As Long Private mFileName As String '  ' Отображение файла. '  ' Создаем дескриптор консольного вывода. Private Declare Function CreateFile Lib "kernel32" _ Alias "CreateFileA" ( _ ByVal lpFileName As String, _ ByVal dwDesiredAccess As Long, _ ByVal dwShareMode As Long, _ ByVal lpSecurityAttributes As Long, _ ByVal dwCreationDisposition As Long, _ ByVal dwFlagsAndAttributes As Long, _ ByVal hTemplateFile As Long _ ) As Long Const GENERIC_READ = &H80000000 Const GENERIC_WRITE = &H40000000 Const FILE_SHARE_READ = &H1 Пример отображения файла
1 Объекты и их дескрипторы Const FILE_SHARE_WRITE = &H2 Const OPEN_EXISTING = 3 Private Declare Function CreateFileMapping Lib "kernel32" _ Alias "CreateFileMappingA" ( _ ByVal hFile As Long, _ ByVal lpSecurityAttributes As Long, _ ByVal flProtect As Long, _ ByVal dwMaximumSizeHigh As Long, _ ByVal dwMaximumSizeLow As Long, _ ByVal lpName As String _ ) As Long Const PAGE_NOACCESS = &H1 Const PAGE_READONLY = &H2 Const PAGE_READWRITE = &H4 Const PAGE_WRITECOPY = &H8 Const PAGE_EXECUTE = &H10 Const PAGE_EXECUTE_READ = &H20 Const PAGE_EXECUTE_READWRITE = &H40 Const PAGE_EXECUTE_WRITECOPY = &H80 Const PAGE_GUARD = &H100 Const PAGE_NOCACHE = &H200 Private Declare Function MapViewOfFile Lib "kernel32" ( _ ByVal hFileMappingObject As Long, _ ByVal dwDesiredAccess As Long, _ ByVal dwFileOffsetHigh As Long, _ ByVal dwFileOffsetLow As Long, _ ByVal dwNumberOfBytesToMap As Long _ ) As Long Const SECTION_EXTEND _SIZE = &H10 Const SECTION_MAP _EXECUTE = &H8 Const SECTION_MAP _READ = &H4 Const SECTION_MAP _WRITE = &H2 Const SECTION_QUERY = &H1 Const FILE_MAP _COPY = SECTION_QUERY Const FILE_MAP _READ = SECTION_MAP _READ Const FILE_MAP _WRITE = SECTION_MAP _WRITE Private Declare Function CloseHandle Lib "kernel32" ( _ ByVal hObject As Long) As Long Private Declare Function UnMapViewOfFile Lib "kernel32" _ Alias "UnmapViewOfFile" (ByVal lpBaseAddress As Long) As Long Public Property Get FileMappingName() As String FileMappingName = mFileMappingName End Property
1 Public Property Let FileMappingName(pFileMappingName As String) mFileMappingName = pFileMappingName End Property Public Property Get FileName() As String FileName = mFileName End Property Public Property Let FileName(pFileName As String) If Dir$(pFileName, vbNormal) <> "" Then mFileName = pFileName Else mFileName = "" End If End Property Public Property Get FileHandle() As Long FileHandle = mFileHandle End Property Public Property Let FileHandle(pFileHandle As Long) mFileHandle = pFileHandle End Property Public Property Get FileMappingHandle() As Long FileMappingHandle = mFileMappingHandle End Property Public Property Let FileMappingHandle(pFileMappingHandle As Long) mFileMappingHandle = pFileMappingHandle End Property Public Property Get FileViewBase() As Long FileViewBase = mFileViewBase End Property Public Property Let FileViewBase(pFileViewBase As Long) mFileViewBase = pFileViewBase End Property Public Function OpenFile() As Long ' Открываем файл и получаем его дескриптор. mFileHandle = CreateFile(mFileName, _ GENERIC_READ Or GENERIC_WRITE, _ FILE_SHARE_READ Or FILE_SHARE_WRITE, _ 0&, _ OPEN_EXISTING, 0&, 0&) OpenFile = mFileHandle Пример отображения файла
1 Объекты и их дескрипторы End Function Public Function OpenFileMapping() As Long ' Создаем отображение файла. mFileMappingHandle = CreateFileMapping( _ mFileHandle, 0&, PAGE_READWRITE, 0&, _ FileLen(mFileName), mFileMappingName) OpenFileMapping = mFileMappingHandle End Function Public Function MapFileView() As Long mFileViewBase = MapViewOfFile( _ mFileMappingHandle, FILE_MAP _WRITE, 0&, 0&, 0&) MapFileView = mFileViewBase End Function Public Function ReadFromFile(cBytes As Long) As String ' Читаем из отображаемого файла. ReDim bFile(1 To cBytes) As Byte CopyMemory ByVal VarPtr(bFile(1)), ByVal mFileViewBase, cBytes ReadFromFile = StrConv(bFile, vbUnicode) End Function Public Function WriteToFile(sWrite As String) As Long ' Возвращаем количество прочитанных байтов. Dim b() As Byte Dim lpsz As Long BSTRtoLPSTR sWrite, b, lpsz CopyMemory ByVal mFileViewBase, ByVal lpsz, Len(sWrite) WriteToFile = Len(sWrite) End Function Public Function UnMapFileView() As Long UnMapFileView = UnMapViewOfFile(mFileViewBase) End Function Public Function CloseFileMapping() As Long CloseFileMapping = CloseHandle(mFileMappingHandle) End Function Public Function CloseFile() As Long CloseFile = CloseHandle(mFileHandle) End Function Обратите внимание, что некоторые из методов являются всего лишь контей­ нерами для API­функций.
1 Если открыть проект FileMapping в каталоге Code_FileMapping и нажать ко­ мандную кнопку, то выполнится следующая процедура. (Предполагается, что файл Mapped.txt находится в соответствующем каталоге приложения.) Sub DoFileMapping() Dim sFileName As String Dim s As String ' Создаем файл. Dim oFile As New CFileMapping ' Устанавливаем свойства. oFile.FileName = App.Path & "\Mapped.txt" If oFile.FileName = "" Then MsgBox App.Path & "Файл \Mapped.txt не существует", vbExclamation Exit Sub End If oFile.FileMappingName = "TestFileMapping" List1.AddItem "Open file: " & oFile.OpenFile List1.AddItem "Open file mapping: " & oFile.OpenFileMapping List1.AddItem "Map file: " & oFile.MapFileView ' Читаем файл целиком. s = oFile.ReadFromFile(FileLen(oFile.FileName)) List1.AddItem "Read file: " & s ' Переводим в другой регистр. If s = UCase$(s) Then s = LCase$(s) Else s = UCase$(s) End If List1.AddItem "Write file: " & oFile.WriteToFile(s) & " bytes" List1.AddItem "Read file: " & oFile.ReadFromFile(FileLen(oFile. FileName)) List1.AddItem "UnMap file: " & oFile.UnMapFileView List1.AddItem "Close file mapping: " & oFile.CloseFileMapping List1.AddItem "Close file: " & oFile.CloseFile List1.AddItem "*****" Set oFile = Nothing End Sub Пример отображения файла
10 Объекты и их дескрипторы Вывод показан на рис. 10.1 . Чтобы убедиться, что два приложе­ ния могут совместно использовать один и тот же объект – в данном случае объект File – поместите класс CfileMapping в оба проекта VB. Добавьте к этим проектам процедуру DoFileMapping и запустите их на выполнение. Когерентность Следует сказать еще несколько слов в продолжение темы отображаемых в па­ мять файлов. На основе единственного та­ кого файла можно выполнять следующие действия:  создавать несколько действующих представлений, основанных на одном и том же объекте Filemapping;  работать с несколькими объектами Filemapping одного и того же файла, возможно, в разных процессах и с различным представлением этого файла у каждого объекта;  создавать процесс, ассоциированный с представлением отображаемого фай­ ла, с целью записи данных и процесс, использующий для работы с файлом обычные функции ввода/вывода. Не стоит и говорить, что при перечислении подобных возможностей возни­ кает вопрос о том, что может случиться, если одно из представлений изменит данные файла. Ответы на подобного рода вопросы можно получить из следую­ щих фактов:  если речь идет об одном объекте Filemapping, то система гарантирует ко­ герентность (coherence) всех представлений, основанных на данном объекте, даже если он совместно используется несколькими процессами. То есть все представления отображают одно и то же текущее состояние данных, вклю­ чая изменения, внесенные любым из представлений. Это объясняется тем, что все эти представления отображают одни и те же данные, хранящиеся в одной и той же области физической памяти;  когерентность представлений файла, созданных на базе разных объектов Filemapping, не гарантирована;  нет гарантии, что изменения, внесенные в файл обычными операциями чтения/ записи (например, ReadFile и WriteFile) будут отражены в представле­ ниях файла. И наконец, традиционные методы файлового ввода/вывода за­ действуют буферы памяти, которые отличаются от буферов, используемых при отображении файла. Поэтому не следует смешивать в одном процессе методы работы с файлами и памятью. Рис. 10.1. Приложение с отображением файла в память
Глава 11. Процессы Как вам уже известно, процесс – это исполняемый экземпляр приложения и набор ресурсов, которые выделяются данному исполняемому приложению. Ресурсы включают в себя следующее:  виртуальное адресное пространство;  системные ресурсы, такие как растровые изображения, файлы, области па­ мяти и т.д .;  модули процесса, то есть исполняемые модули, которые отображены (за­ гружены) в его адресное пространство. Это могут быть динамические библи­ отеки (DLL), драйверы (DRV) и управляющие элементы (OCX), основной загрузочный модуль (EXE) процесса, который иногда и называют собствен­ но модулем. Модуль данных (или программный модуль) может или нахо­ диться на диске, или быть загруженным в физическую память (RAM). Правда, термин «загружен» (loaded) имеет иное значение, относящееся к виртуаль­ ному адресному пространству процесса. Здесь больше подходит термин «отображен» (mapped), так как само отображение – это просто назначение виртуальным адресам физических адресов. После того как модуль загружен в физическую память, его физические адреса могут отображаться в различные виртуальные адресные пространства, при этом возможно использование в каждом процессе разных виртуальных адресов. Отображение не обязательно требует физического перемещения реальных данных или программ (хотя, как вы увидите позже, бывает и так);  уникальный идентификационный номер, называемый идентификатором процесса;  один или несколько потоков управления. Поток – это внутренняя составляющая процесса, которой операционная сис­ тема выделяет процессорное время. Каждый процесс должен иметь, по крайней мере, один поток. Поток включает:  текущее состояние регистров процессора;  два стека, один из которых используется при выполнении в режиме ядра, второй – при выполнении в пользовательском режиме;  участок памяти для работы подсистем, библиотеки времени выполнения, динамические библиотеки;  уникальный идентификатор, называемый идентификатором потока. Состояние регистров, содержимое стека и области памяти называют контекс­ том потока (thread’s context). Как уже упоминалось, основное назначение потоков – дать процессу возмож­ ность поддерживать несколько ветвей управления, то есть выполнять больше «дел»
12 Процессы одновременно. В многопроцессорной конфигурации (компьютер с двумя и более процессорами) Windows NT (но не Windows 9x) может назначать разные потоки разным процессорам в различные моменты времени, обеспечивая действительно параллельную обработку. В однопроцессорной конфигурации процессор должен выделять кванты времени каждому исполняемому в данный момент потоку. Эта глава посвящена обсуждению процессов, а тема потоков отложена до сле­ дующей главы. Дескрипторы и идентификаторы процессов Разницу между дескрипторами и идентификаторами процессов можно уста­ новить, анализируя API­функцию CreateProcess. Одна из возможных VB­де­ клараций этой довольно сложной функции представлена ниже: Declare Function CreateProcess Lib "kernel32" Alias "CreateProcessA" ( _ ByVal lpApplicationName As String, _ ByVal lpCommandLine As String, _ lpProcessAttributes As SECURITY_ATTRIBUTES, _ lpThreadAttributes As SECURITY_ATTRIBUTES, _ ByVal bInheritHandles As Long, _ ByVal dwCreationFlags As Long, _ lpEnvironment As Any, _ ByVal lpCurrentDirectory As String, _ lpStartupInfo As STARTUPINFO, _ lpProcessInformation As PROCESS_INFORMATION _ ) As Long Последний параметр – это указатель на структуру PROCESS_INFORMATION: typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessID; DWORD dwThreadID; } PROCESS_INFORMATION; Ее члены в соответствии с документацией имеют следующее значение:  HProcess возвращает дескриптор вновь созданного процесса;  Hthread возвращает дескриптор первичного потока вновь созданного про­ цесса. Дескриптор используется для спецификации данного потока во всех функциях, которые выполняют операции с объектом Thread;  DwProcessId возвращает глобальный идентификатор процесса. Время жизни идентификатора – с момента создания процесса до момента его за­ вершения;  DwThreadId возвращает глобальный идентификатор потока. Время жизни идентификатора – с момента создания потока до момента его завершения. Таким образом, между дескриптором и идентификатором процесса (или пото­ ка) существуют следующие основные различия:
13  дескриптор действует в пределах своего процесса, в то время как идентифи­ катор работает на системном уровне. Таким образом, дескриптор действует только в пределах того процесса, в котором он был создан;  у каждого процесса только один идентификатор, но может быть несколько дескрипторов;  некоторым API­функциям требуется идентификатор, в то время как дру­ гим – дескриптор процесса. Следует подчеркнуть, что, хотя дескриптор является зависимым от процесса, один процесс может иметь дескриптор другого процесса. Иными словами, если процесс A имеет дескриптор процесса B, то этот дескриптор идентифицирует процесс B, но действует только в процессе A. Он может использоваться в процессе A для вызова некоторых API­функций, которые имеют отношение к процессу B. (Однако память процесса B остается недоступной для процесса A.) Дескрипторы модулей Каждый модуль (DLL, OCX, DRV и т.д .), загруженный в пространство процес­ са, имеет свой дескриптор (module handle), называемый также логическим номе­ ром экземпляра (instance handle). В 16­разрядной Windows данными терминами обозначались разные объекты, в 32­разрядной системе это один и тот же объект. Дескриптором исполняемого модуля является начальный или базовый адрес данного исполняемого модуля в адресном пространстве процесса. Это объясняет, почему такой дескриптор имеет смысл только в рамках процесса, включающего данный модуль. Базовый адрес загрузочного (EXE) модуля, создаваемого VC++, по умолчанию имеет значение &H400000. Однако программист в VC++ может изменить этот адрес по умолчанию, и есть серьезные основания поступать именно так для того, чтобы избежать конфликта с другими модулями. (Позже вы увидите, что перерас­ пределение памяти, связанное с конфликтом базовых адресов, требует слишком большого объема физической памяти и временных затрат.) Кроме того, модули, создаваемые другими программными средами, могут иметь другие базовые адреса, устанавливаемые по умолчанию. Дескрипторы модулей часто используются для вызова API­функций, которые выделяют ресурсы данному процессу. Например, функция LoadBitmap загружает ресурс растровой картинки из загрузочного файла данного процесса. Ее деклара­ ция записывается так: HBITMAP LoadBitmap( HINSTANCE hInstance, // Дескриптор экземпляра приложения. LPCTSTR lpBitmapeName // Адрес имени ресурса растровой картинки. ); Первый параметр – это дескриптор экземпляра процесса. Обратите внимание на то, что дескриптор модуля загрузочного файла процесса часто ошибочно называют дескриптором модуля процесса, а об имени загрузочно­ го файла говорят как об имени модуля процесса. Дескрипторы модулей
14 Процессы Идентификация процесса Следующие четыре объекта часто встречаются в программировании API, свя­ занном с процессами:  идентификатор процесса;  дескриптор процесса;  полное имя загрузочного файла;  дескриптор модуля загрузочного файла процесса. Наиболее значимым здесь является вопрос о том, можно ли, имея один из этих объектов, получить остальные. Ответ на этот вопрос схематично проил­ люстрирован на рис. 11.1 . Стрелки на рисунке указывают на существующие возможности. Рис. 11.1. Идентификаторы и дескрипторы Дескриптор окна в процессе A Идентификатор процесса Дескриптор процесса Имя файла модуля Дескриптор модуля E B B B E C D D
15 Получение дескриптора процесса по его идентификатору Получить дескриптор процесса по его идентификатору относительно легко, но обратная операция вряд ли возможна (по крайней мере, прямо). По идентификатору можно определить дескриптор любого процесса с помо­ щью функции OpenProcess: HANDLE OpenProcess( DWORD dwDesiredAccess, // Флаг доступа. BOOL bInheritHandle, // Флаг наследования дескриптора. DWORD dwProcessID // Идентификатор процесса. ); Параметр dwDesiredAccess имеет отношение к правам доступа и может принимать различные значения:  PROCESS_ALL_ACCESS эквивалентно установке флагов полного доступа;  PROCESS_DUP_HANDLE использует дескриптор как исходного процесса, так и принимающего в функции DuplicateHandle для копирования (дубли­ рования) дескриптора;  PROCESS_QUERY_INFORMATION задействует дескриптор процесса для чте­ ния информации из объекта Process;  PROCESS_VM_OPERATION использует дескриптор процесса для модифика­ ции виртуальной памяти процесса;  PROCESS_TERMINATE работает для завершения процесса с его дескрипто­ ром в функции TerminateProcess;  PROCESS_VM_READ применяет для чтения из виртуальной памяти процесса его дескриптор в функции ReadProcessMemory;  PROCESS_VM_WRITE использует для записи в виртуальную память процесса его дескриптор в функции WriteProcessMemory;  SYNCHRONIZE (Windows NT) работает с дескриптором процесса в любой из функций ожидания, таких как WaitForSingleObject, для ожидания завершения процесса. Параметр bInheritHandle установлен в значение True, для того чтобы позволить порожденным процессам наследовать дескриптор. Иначе говоря, по­ рожденный процесс получает дескриптор родительского процесса. Отметим, что значение дескриптора может изменяться. (В книге тема наследования процессов не описывается.) Параметр dwProcessID должен иметь значение идентификатора того про­ цесса, дескриптор которого нужно узнать. Функция OpenProcess возвращает дескриптор указанного процесса. Декларация VB функции OpenProcess приведена ниже: Declare Function OpenProcess Lib "kernel32" ( _ ByVal dwDesiredAccess As Long, _ ByVal bInheritHandle As Long, _ ByVal dwProcessId As Long _ ) As Long Идентификация процесса
1 Процессы Так как иногда требуется получить дескриптор процесса по его идентификато­ ру, создадим для этих целей небольшую функцию. Обратите внимание, что доступ к процессу используется только для чтения информации: Public Function ProcHndFromProcID(ByVal lProcID As Long) As Long ' НЕ ЗАБУДЬТЕ ЗАКРЫТЬ ДЕСКРИПТОР! ProcHndFromProcID = OpenProcess( _ PROCESS_QUERY_INFORMATION Or PROCESS_VM _READ, 0&, lProcID) End Function Для Windows NT нужно добавить дополнительный флаг, который использу­ ется при синхронизации потоков (см. главу 12): Public Function ProcHndFromProcIDSync(ByVal lProcID As Long) As Long ' НЕ ЗАБУДЬТЕ ЗАКРЫТЬ ДЕСКРИПТОР! ProcHndFromProcIDSync = OpenProcess( _ PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ Or SYNCHRONIZE, 0&, lProcID) End Function Помните, что полученный дескриптор обязательно должен быть закрыт, если он больше не нужен. Это делается путем вызова функции CloseHandle: BOOL CloseHandle( HANDLE hObject // Дескриптор закрываемого объекта. ); или в VB: Declare Function CloseHandle Lib "kernel32" ( _ ByVal hObject As Long _ ) As Long Примеры использования функции OpenProcess будут обсуждаться в этой главе несколько позже. Имена файлов и дескрипторы модулей Перейти от имени файла модуля к дескриптору модуля и наоборот не со­ ставляет особого труда, по крайней мере, в пределах одного процесса. Функция GetModuleFileName принимает дескриптор модуля, чтобы вернуть полное имя (имя и путь) исполняемого файла: DWORD GetModuleFileName( HMODULE hModule, // Дескриптор модуля. LPTSTR lpFilename, // Указатель на буферприемник пути к модулю. DWORD nSize // Размер буфера в символах. ); Учтите, однако, что эта функция работает только из того процесса, дескриптор модуля которого задается. Ее нельзя использовать из другого процесса. Функция GetModuleHandle, которая так же применяется внутри одного про­ цесса, выполняет обратное действие – возвращает дескриптор модуля по имени файла (без пути):
1 HMODULE GetModuleHandle( LPCTSTR lpModuleName // Имя модуля. ); Следует еще раз повторить, что эта функция не может использоваться для получения дескрипторов модулей других процессов. Приведенные выше объявления двух функций могут быть переведены в VB следующим образом: Declare Function GetModuleFileName Lib "kernel32" _ Alias "GetModuleFileNameA" ( _ ByVal hModule As Long, _ ByVal lpFileName As String, _ ByVal nSize As Long _ ) As Long Declare Function GetModuleHandle Lib "kernel32" _ Alias "GetModuleHandleA" ( _ ByVal lpModuleName As String _ ) As Long Приведенная в листинге 11.1 функция, принимая дескриптор, имя или полное имя модуля, возвращает другие два элемента в своих выходных параметрах. Листинг 11.1. Получение дескриптора, имени или полного имени модуля Public Function Proc_ModuleInfo(ByRef lHnd As Long, _ ByRef sName As String, _ ByRef sFQName As String) As Long ' Алгоритм: ' If lHnd = 1 then получить дескриптор, имя и полное имя EXE. ' ElseIf lHnd > 0 then получить имя и полное имя модуля. ' ElseIf sName <> "" then получить дескриптор и полное имя. ' ElseIf sFQName <> "" then получить дескриптор и имя. ' Требуется: Public Const MAX_PATH = 260 Dim lret As Long, X As Integer Dim hModule As Long, s As String Proc_ModuleInfo = 0 IflHnd= 1 OrlHnd>0Then hModule = lHnd ' Получаем дескриптор. If lHnd = 1 Then ' Получаем дескриптор EXE. hModule = GetModuleHandle(vbNullString) If hModule = 0 Then Proc_ModuleInfo = 1 Exit Function End If End If Идентификация процесса
1 Процессы ' По дескриптору получаем имена. s = String(MAX_PATH + 1, 0) lret = GetModuleFileName(hModule, s, MAX_PATH) sFQName = Left(s, lret) X = InStrRev(sFQName, "\") If X > 0 Then sName = Mid$(sFQName, X + 1) ElseIf sName <> "" Then ' По имени получаем дескриптор и полное имя. hModule = GetModuleHandle(sName) If hModule = 0 Then Proc_ModuleInfo = 2 Exit Function Else lHnd = hModule s = String(MAX_PATH + 1, 0) lret = GetModuleFileName(hModule, s, MAX_PATH) sFQName = Left(s, lret) End If ElseIf sFQName <> "" Then ' По полному имени получаем дескриптор и имя. hModule = GetModuleHandle(sFQName) If hModule = 0 Then Proc_ModuleInfo = 3 Exit Function Else lHnd = hModule X = InStrRev(sFQName, "\") If X > 0 Then sName = Mid$(sFQName, X + 1) End If End If End Function В листинге 11.2 представлена программа, использующая эту функцию. Вы­ вод, получившийся на моем компьютере, показан в листинге 11.3 . Листинг 11.2 . Вызов функции Proc_ModuleInfo Public Sub Proc_ModuleInfoExample() Dim sModName As String, sFQModName As String, hMod As Long Dim lret As Long hMod = 1: sModName = "": sFQModName = "" lret = Proc_ModuleInfo(hMod, sModName, sFQModName) Debug.Print "Handle: &H" & Hex(hMod) Debug.Print "Name: " & sModName
1 Debug.Print "FQName: " & sFQModName Debug.Print hMod = 0: sModName = "User32.dll": sFQModName = "" lret = Proc_ModuleInfo(hMod, sModName, sFQModName) Debug.Print "Handle: &H" & Hex(hMod) Debug.Print "Name: " & sModName Debug.Print "FQName: " & sFQModName Debug.Print hMod = 0: sModName = "": sFQModName = "C:\WINNT\system32\USER32.dll" lret = Proc_ModuleInfo(hMod, sModName, sFQModName) Debug.Print "Handle: &H" & Hex(hMod) Debug.Print "Name: " & sModName Debug.Print "FQName: " & sFQModName Debug.Print hMod = 2011627520: sModName = "": sFQModName = "" lret = Proc_ModuleInfo(hMod, sModName, sFQModName) Debug.Print "Handle: &H" & Hex(hMod) Debug.Print "Name: " & sModName Debug.Print "FQName: " & sFQModName Debug.Print End Sub Листинг 11.3. Вывод для программы, представленной в листинге 11.2 . Handle: &HFFFFFFFF Name: VB6.EXE FQName: G:\Visual Studio\VB98\VB6.EXE Handle: &H77E70000 Name: User32.dll FQName: G:\WINNT\system32\USER32.DLL Handle: &H77E70000 Name: USER32.dll FQName: G:\WINNT\system32\USER32.DLL Handle: &H77E70000 Name: USER32.dll FQName: G:\WINNT\system32\USER32.DLL Получение идентификатора текущего процесса Чтобы получить идентификатор текущего процесса, можно использовать фун­ кцию GetCurrentProcessId, объявление которой выглядит так: Идентификация процесса
10 Процессы DWORD GetCurrentProcessID(VOID) или в VB: Declare Function GetCurrentProcessID Lib "kernel32" () As Long Эта функция возвращает идентификатор процесса. Заметьте, что значение идентификатора процесса может быть в верхнем диапазоне unsigned long, по­ этому может потребоваться преобразование возвращаемого значения. Следует также учесть, что данная функция работает только в текущем процессе. Не су­ ществует способа, во всяком случае, известного мне, определить идентификатор другого процесса, кроме как получить список всех процессов и выбрать из него тот процесс, характеристики которого требуются. Получение идентификатора процесса от окна В главе 6 рассказывалось о функции FindWindow. Она объявляется следую­ щим образом: HWND FindWindow( LPCTSTR lpClassName, // Указатель на имя класса. LPCTSTR lpWindowName // Указатель на имя окна. ); или в VB: Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _ ByVal lpClassName As String, _ ByVal lpWindowName As String _ ) As Long Функция использует имя класса или заголовок окна для получения дескриптора окна. Имея дескриптор, можно вызвать функцию GetWindowThreadProcessId, возвращающую идентификатор потока, который создал данное окно, и идентифика­ тор процесса, которому принадлежит данный поток. Синтаксис выглядит так: DWORD GetWindowThreadProcessId( HWND hWnd, // Дескриптор окна. LPDWORD lpdwProcessId // Адрес переменной для идентификатора // процесса. ); Данная функция возвращает идентификатор потока. Кроме того, если ей пе­ редается указатель на DWORD в lpdwProcessId(), в целевой переменной возвра­ щается идентификатор процесса. Можно перевести это объявление в VB следующим образом: Declare Function GetWindowThreadProcessId Lib "user32" ( _ ByVal hwnd As Long, _ lpdwProcessId As Long _ ) As Long Далее представлена небольшая функция, которая возвращает идентификатор процесса по дескриптору окна:
11 Public Function ProcIDFromhWnd(ByVal hwnd As Long) As Long Dim lret As Long, hProcessID As Long lret = GetWindowThreadProcessId(hwnd, hProcessID) ProcIDFromhWnd = hProcessID End Function О дескрипторах окон более подробно рассказывается в главе 15. Получение имен и дескрипторов модулей Обычно одному процессу принадлежит много модулей, загруженных в его адресное пространство, и это, естественно, усложняет задачу получения дескрип­ торов и имен модулей. Но это еще не самое трудное. К сожалению, придется ис­ пользовать совершенно разные методы для Windows 9x и Windows NT. Windows NT 4.0 требует использования динамической библиотеки, называе­ мой PSAPI.DLL, что означает API состояния процесса (Process Status API). Эта библиотека, несовместимая с Windows 9x, экспортирует функции перечисления всех процессов в системе и всех драйверов устройств. Она предоставляет возмож­ ность получения информации обо все модулях, исполняемых данным процессом. Позже в этой главе будет приведен пример использования этой библиотеки. Для перечисления потоков в операционной системе Windows NT нужно использовать динамическую библиотеку PDH.DLL, что означает «вспомогательная система для оценки характеристик производительности» (Performance Data Helper), которая поставляется в комплекте инструментальных средств NT Resource Toolkit. Впро­ чем, в перечислении потоков нет особой необходимости. С другой стороны, Windows 9x поддерживает функции Toolhelp (вспомо­ гательные средства) в своей версии динамической библиотеки KERNEL32.DLL. Они используются для фиксации состояния области памяти любого процесса. Используя этот «снимок» памяти, можно получить любую информацию о текущих процессах, а также о модулях и потоках каждого процесса. (Впрочем, в отличие от PSAPI.DLL здесь отсутствует информация о драйверах устройств.) Это не очень удобно, так как для Windows NT и Windows 9x придется писать разные программы. Но есть и положительный момент: Windows 2000 будет подде­ рживать Toolhelp. (Надеюсь только, что она по­прежнему будет поддерживать и библиотеку PSAPI.DLL, чтобы мне не пришлось переписывать мои программы!) Прежде чем заняться примером, давайте закончим рассмотрение темы о де­ скрипторе процесса. Псевдодескрипторы процессов Обратимся еще к одному вопросу, касающемуся темы о дескрипторах и иден­ тификаторах процессов. Функция GetCurrentProcess возвращает псевдо­ дескриптор текущего процесса: HANDLE GetCurrentProcess(VOID) Псевдодескрипторы процессов
12 Процессы Псевдодескриптор (pseudohandle) представляет собой упрощенный вариант дескриптора. По определению, псевдодескриптор – это зависимое от процесса чис­ ло, которое служит идентификатором процесса и может использоваться в вызовах тех API­функций, которым требуется дескриптор процесса. Хотя назначение псевдодескрипторов и обычных дескрипторов почти одно и то же, у них все же есть некоторые существенные различия. Псевдодескрипторы не могут наследоваться порожденными процессами, как настоящие дескрипторы (real handles). К тому же псевдодескрипторы ссылаются только на текущий про­ цесс, а настоящие дескрипторы могут ссылаться и на внешний (foreign). Windows предоставляет возможность получения настоящего дескриптора по псевдодескриптору при помощи API­функции DuplicateHandle, о которой говорилось в главе 10. Она определяется как BOOL DuplicateHandle( HANDLE hSourceProcessHandle, // Дескриптор процессаисточника. HANDLE hSourceHandle, // Копируемый дескриптор. HANDLE hTargetProcessHandle, // Дескриптор процессаприемника. LPHANDLE lpTargetHandle, // Указатель на копию дескриптора. DWORD dwDesiredAccess, // Доступ к копии дескриптора. BOOL bInheritHandle, // Флаг наследования дескриптора. DWORD dwOptions // Необязательные опции. ); Далее приводится небольшая программа, показывающая, как получать де­ скрипторы и псевдодескрипторы процесса: Public Const PROCESS_VM _READ = &H10 Public Const PROCESS_QUERY_INFORMATION = &H400 Public Const DUPLICATE_SAME_ACCESS = &H2 Public Sub DuplicateHandleExample() Dim lret As Long Dim hPseudoHandle As Long Dim hProcessID As Long Dim hProcess As Long Dim hDupHandle As Long ' Идентификатор процесса (ID). hProcessID = GetCurrentProcessId Debug.Print "Идентификатор текущего процесса: " & hProcessID ' Псевдодескриптор. hPseudoHandle = GetCurrentProcess Debug.Print "Псевдодескриптор: " & hPseudoHandle ' Копируем дескриптор. lret = DuplicateHandle(hPseudoHandle, _ hPseudoHandle, _ hPseudoHandle, _
13 hDupHandle, _ 0&, 0&, DUPLICATE_SAME_ACCESS) Debug.Print "Дескриптор, полученный функцией DuplicateHandle: " & hDupHandle ' Дескриптор от функции OpenProcess. hProcess = OpenProcess( _ PROCESS_QUERY_INFORMATION Or PROCESS_VM _READ, _ 0&, _ hProcessID) Debug.Print "Дескриптор, полученный функцией OpenProcess: " & hProcess ' Закрываем настоящие дескрипторы. Debug.Print lret = CloseHandle(hProcess) Debug.Print "Закрываем дескриптор, полученный функцией OpenProcess: " _ & hProcess lret = CloseHandle(hDupHandle) Debug.Print "Закрываем дескриптор, полученный функцией _ DuplicateHandle: " & hDupHandle End Sub Эта программа копирует исходный псевдодескриптор в переменную hDupHandle и получает настоящий (но другой) дескриптор, используя функцию OpenProcess. Обратите внимание, что настоящий дескриптор должен быть закрыт, а псевдо­ дескриптор закрывать не нужно. Вывод на моем компьютере был таким: Идентификатор текущего процесса: 183 Псевдодескриптор: 1 Дескриптор, полученный функцией DuplicateHandle: 412 Дескриптор, полученный функцией OpenProcess: 392 Закрываем дескриптор, полученный функцией OpenProcess: 392 Закрываем дескриптор, полученный функцией DuplicateHandle: 412 Перечисление процессов Теперь рассмотрим проблему перечисления процессов в системе. Как уже го­ ворилось, способы ее решения в Windows 9x и Windows NT различны. Перечисление процессов в Windows NT Из PSAPI.DLL будут использованы следующие функции:  EnumProcesses перечисляет идентификаторы процессов, для каждого про­ цесса в системе;  EnumProcessesModules перечисляет дескрипторы каждого модуля дан­ ного процесса;  GetModuleBaseName получает имя модуля по его дескриптору; Перечисление процессов
14 Процессы  GetModuleBaseNameEx получает полный путь к модулю по его дескриптору;  GetModuleInformation получает информацию о модуле;  GetProcessMemoryInfo получает данные об использовании процессом памяти. В архиве вы найдете приложение rpiEnumProcsNT, которое отображает каж­ дый процесс вместе с именем его загрузочного файла. Кроме того, эта утилита показывает, какие модули загружены в адресное пространство выбранного про­ цесса, а также некоторую информацию, относящуюся к использованию процессом памяти. На рис. 11.2 показано главное окно этого приложения. Полный исходный текст программы содержится в архиве примеров. Рис. 11.2 . Перечисление процессов в Windows NT При нажатии кнопки MemMap (Карта памяти) постранично выводится карта памяти адресного пространства выбранного процесса (см. рис. 11.3). Карта памяти будет подробно рассматриваться в главе 13. При щелчке по кнопке Refresh (Перечитать) начинается перечисление всех процессов в системе. Функция EnumProcess объявляется таким образом:
15 BOOL EnumProcesses( DWORD *lpidProcess, // Массив, принимающий идентификаторы процесса. DWORD cb, // Размер массива DWORD в байтах. DWORD *cbNeeded // Содержит количество возвращенных байтов. ); Так как в качестве первого параметра выступает указатель на первый элемент массива DWORD, данный элемент можно передать в VB по ссылке. DWORD в дейс­ твительности является unsigned long, следовательно, нужно использовать тип VB Long и выполнить все необходимые преобразования из знакового в беззнако­ вое. Однако идентификаторы процессов – очень небольшие числа (они начина­ ются с единицы и затем последовательно увеличиваются), а cbNeeded – просто счетчик байтов, следовательно, ни для каких параметров DWORD* преобразова­ ний не потребуется. Итак, одна из возможных в VB деклараций записывается таким образом: Public Declare Function EnumProcesses Lib "PSAPI.DLL" ( _ idProcess As Long, _ ByVal cBytes As Long, _ cbNeeded As Long _ ) As Long Заметьте, что Win32 не предоставляет непосредственной возможности опреде­ лить, какого размера должен быть массив idProcess. Единственный способ – по­ пытаться угадать, подставив выбранное значение в функцию, а затем сравнить возвращенное значение cbNeeded, которое содержит количество возвращенных байтов (то есть количество возвращенных элементов массива типа long, умно­ женное на четыре), с выбранным вами размером массива. Если количество воз­ Рис. 11.3. Карта памяти процесса Перечисление процессов
1 Процессы вращенных элементов массива равно выбранному размеру массива (количество возвращенных элементов никогда не превысит заданный размер), то потребуется увеличить размер массива и повторить попытку. Процедура GetProcess, приведенная в листинге 11.4, начинается с подобных замысловатых операций. После этого программа сортирует полученный список процессов и применяет функцию OpenProcess в цикле For для нахождения дескриптора каждого процесса, с помощью которого можно вызывать такие фун­ кции, как GetModuleNameEx. Заметьте, что функция EnumProcessModules использована для получения доступа к первому модулю каждого процесса, так как первый модуль всегда является загрузочным файлом. Определение функции RaiseAPIError дано в конце главы 3. Листинг 11.4 . Перечисление процессов Public Const PROCESS_VM _READ = &H10 Public Const PROCESS_QUERY_INFORMATION = &H400 Sub GetProcesses() ' Заполняет массивы lProcessIDs, hProcesses, sEXENames, sFQEXENames. ' Устанавливает cProcesses. Dim i As Integer, j As Integer, l As Long Dim cbNeeded As Long Dim hEXE As Long Dim hProcess As Long Dim lPriority As Long ' Первая попытка. cProcesses = 25 Do ' Размер массива. ReDim lProcessIDs(1 To cProcesses) ' Перечисляем. lret = EnumProcesses(lProcessIDs(1), cProcesses * 4, cbNeeded) Iflret=0Then RaiseApiError Err.LastDllError Exit Sub End If ' Сравниваем количество возвращенных байтов с размером массива ' в байтах. ' Если меньше, тогда мы получили все, что требовалось. If cbNeeded < cProcesses * 4 Then Exit Do Else cProcesses = cProcesses * 2 End If Loop
1 cProcesses = cbNeeded / 4 ' Сортируем по идентификатору процесса. For i = 1 To cProcesses Forj=i+1TocProcesses If lProcessIDs(i) > lProcessIDs(j) Then ' Обмен. l = lProcessIDs(i) lProcessIDs(i) = lProcessIDs(j) lProcessIDs(j) = l End If Next Next ReDim Preserve lProcessIDs(1 To cProcesses) ReDim sEXENames(1 To cProcesses) ReDim sFQEXENames(1 To cProcesses) ReDim sPriorityClass(1 To cProcesses) ' Теперь у нас есть идентификаторы процессов. ' Используем OpenProcess для получения дескрипторов всех процессов. For i = 1 To cProcesses hProcess = OpenProcess( _ PROCESS_QUERY_INFORMATION Or PROCESS_VM _READ, _ 0&, _ lProcessIDs(i)) ' Осторожнее с системными процессами. Select Case lProcessIDs(i) Case 0 ' Системный процесс Idle. sEXENames(i) = "Idle Process" sFQEXENames(i) = "Idle Process" Case 2 sEXENames(i) = "System" sFQEXENames(i) = "System" Case 28 sEXENames(i) = "csrss.exe (Win32)" sFQEXENames(i) = "csrss.exe (Win32)" End Select ' Если ошибка, пропускаем этот процесс. If hProcess <> 0 Then ' Теперь получаем дескриптор первого модуля ' в этом процессе, так как первый модуль  это EXE. hEXE=0 lret = EnumProcessModules(hProcess, hEXE, 4&, cbNeeded) If hEXE <> 0 Then Перечисление процессов
1 Процессы ' Получаем имя модуля. sEXENames(i) = String$(MAX_PATH, 0) lret = GetModuleBaseName(hProcess, hEXE, sEXENames(i), _ Len(sEXENames(i))) sEXENames(i) = Trim0(sEXENames(i)) ' Получаем полный путь. sFQEXENames(i) = String$(MAX_PATH, 0) lret = GetModuleFileNameEx(hProcess, hEXE, sFQEXENames(i), _ Len(sFQEXENames(i))) sFQEXENames(i) = Trim0(sFQEXENames(i)) ' Получаем приоритет. lPriority = GetPriorityClass(hProcess) Select Case lPriority Case IDLE_PRIORITY_CLASS sPriorityClass(i) = "idle" Case NORMAL_PRIORITY_CLASS sPriorityClass(i) = "normal" Case HIGH_PRIORITY_CLASS sPriorityClass(i) = "high" Case REALTIME_PRIORITY_CLASS sPriorityClass(i) = "real" Case Else sPriorityClass(i) = "???" End Select EndIf 'EXE<>0. End If ' hProcess <> 0. ' Закрываем дескриптор. lret = CloseHandle(hProcess) Next End Sub Когда пользователь выбирает какой­либо процесс, программа выполняет похо­ жую процедуру перечисления модулей выбранного процесса, используя функ­ ции EnumProcessModules, GetModuleBaseName и GetModuleFileNameEx. Кроме того, вызывается функция GetModuleInformation, которая заполняет структуру MODULEINFO: Type MODULEINFO lpBaseOfDll As Long ' Указатель на базовый адрес модуля. SizeOfImage As Long ' Размер модуля в байтах. EntryPoint As Long ' Указатель на точку входа в модуль. End Type Эта структура дает информацию о размере модуля и его базовом адресе.
1 Можно также получить сведения о памяти посредством вызова функции GetProcessMemoryInfo. Она заполняет структуру всякого рода данными, но в настоящий момент требуется только размер рабочего набора, информация об ис­ пользовании файла страниц (page file) и количество неудачных обращений к стра­ ницам памяти (page fault count). Более подробно память рассматривается в главе 13, здесь же приводятся только основные сведения, которыми нужно владеть:  размер рабочего набора представляет собой объем реально присутствующей физической памяти (RAM), задействованной в текущий момент данным процессом;  использование файла страниц является информацией об объеме файла под­ качки (page file, swap file), который использует данный процесс;  количество неудачных обращений к страницам памяти представляет собой число попыток процесса получить доступ к адресу памяти, при которых выяснялось, что данный адрес в настоящий момент не представлен реаль­ ным физическим адресом. Такая ситуация требует подкачки в оперативную память страницы с затребованным адресом и, соответственно, перемещения какой­либо страницы из памяти в файл подкачки на жестком диске. Драйверы устройств также являются загрузочными файлами, но они не при­ надлежат конкретному процессу, действуя скорее на системном уровне. Функции EnumDeviceDriver, GetDeviceDriverBaseName и GetDeviceDriverFileName похожи на соответствующие функции для процессов и модулей. Владея информацией о модулях и драйверах устройств, можно создать карту памяти (в нижней части окна приложения). При выборе модуля или драйвера в списке цвет точки входа этого модуля на карте памяти изменяется на белый. Понаблюдайте за изменениями карты памяти, перемещаясь по списку процессов. Подробнее карты памяти будут обсуждаться в главе 13. Перечисление процессов в Windows 9x Версия рассмотренной утилиты для Windows 9x также содержится в архиве примеров. Ее главное окно показано на рис. 11.4 . Программа использует динамическую библиотеку Toolhelp, о которой ранее шла речь в этой главе, в разделе «Получение имен и дескрипторов модулей». Хотя в версии для Win95 нельзя составить список драйверов, зато можно с выгодой использовать способность программ, работающих в пользовательском режиме, исследовать верхнюю область адресного пространства процесса. Интересно сравнить карты памяти для VB6 с рис. 11.2 и 11.4 . В частности, можно увидеть, что Windows 9x помещает Win32 и прочие системные DLL в другие, нежели Windows NT, области памяти. Например, в Windows NT KERNEL32.DLL располагается сразу под отметкой 2 Гб (по адресу &H77F00000, как показано на рис. 11.2), что соответствует области памяти, зарезервированной для приложений. А в Windows 9x эта DLL находится на отметке 3 Гб, в области памяти, принадле­ жащей операционной системе. На рис. 11.5 показана верхняя область карты памяти процесса в Win95. Об­ ратите внимание на расположение KERNEL32.DLL и других системных DLL. В Windows NT эта область памяти является защищенной. Перечисление процессов
10 Процессы Рис. 11.4 . Составление списка процессов в Windows 9x Рис. 11.5 . Верхняя область адресного пространства процесса в Win95
11 Как определить, выполняется ли приложение Один из наиболее часто задаваемых программистами вопросов звучит так: «Существует ли оптимальный способ узнать, выполняется данное приложение или нет?» Можно назвать несколько способов определения этого, но, неудивительно, если кто­нибудь предложит альтернативные. К сожалению только один из этих способов работает для приложений, созданных в отличных от VB средах. Использование FindWindow Первый способ самый простой, но работает лишь с вашими собственными приложениями и только в том случае, если приложение имеет однозначно иден­ тифицируемый и неизменяемый заголовок окна верхнего уровня. Тогда можно использовать функцию FindWindow, чтобы определить, существует ли окно с таким заголовком. Однако здесь есть одна тонкость. В качестве примера можно использовать фрагмент кода обработки события Load из приложения просмотра буфера обмена, которое будет рассматриваться несколько позже: ' Проверяем, работает ли наша программа и если да, то переключаемся. hnd = FindWindow("ThunderRT6FormDC", "rpiClipViewer") If hnd <> 0 And hnd <> Me.hwnd Then SetForegroundWindow hnd End End If Проблема заключается в следующем. Как только программа начнет выполнять­ ся, создастся форма с заголовком «rpiClipViewer», и функция FindWindow всегда будет сообщать, что такая форма (окно) существует. Однако с помощью небольшо­ го маневра VB можно обмануть. В частности, можно изменить во время проекти­ рования заголовок главной формы, скажем, на «rpiClipView». Затем, в событии Activate главной формы заменить этот заголовок окончательным вариантом: Private Sub Form_Activate() Me.Caption = "rpiClipViewer" End Sub Теперь на время действия события Form_Load главное окно будет иметь за­ головок «rpiClipView» и, следовательно, функция FindWindow не даст утверди­ тельного ответа. В действительности данная функция будет сообщать о том, что такое окно существует, но только тогда, когда появится еще один исполняемый экземпляр этого приложения, а именно это и требовалось получить. Проблема использования функции SetForegroundWindow В Windows 95 и в Windows NT 4 функция SetForegroundWindow Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As Long) _ As Long Как определить, выполняется ли приложение
12 Процессы выведет приложение, которому принадлежит окно с дескриптором hwnd, на передний план. В данном случае документация по Windows 98/2000 может совсем сбить с толку. В ней утверждается, что для Windows NT 5.0 и более поздних версий при­ ложение не может выводить окно на передний план, если пользователь в то же са­ мое время работает с другим окном. Вместо этого функция SetForegroundWindow активизирует указанное окно (см. SetActiveWindow) и вызовет функцию FlashWindowEx, чтобы уведомить пользователя. К сожалению, Microsoft в данном случае лишает программиста возможности самому определять, какое приложение должно находиться на переднем плане. (Безусловно, злоупотребление этой возможностью приводит к тому, что окна могут вести себя довольно надоедливо. Но в данном случае подобный шаг – объ­ ективная необходимость, а не злонамеренное использование.) К счастью, функция SetForegroundWindow выполняет все необходимые действия, если вызвать ее из того приложения, которое нужно вывести на пере­ дний план. Иными словами, она выводит на передний план то приложение, из которого вызывается. Динамическая библиотека rpiAccessProcess, о которой будет говориться в гла­ ве 20, экспортирует функцию rpiSetForegroundWindow. Ее VB­декларация точно такая же, как у функции Win32 SetForegroundWindow: Declare Function rpiSetForegroundWindow Lib "rpiAccessProcess.dll" ( _ ByVal hwnd As Long) As Long Это аналог функции SetForegroundWindow, поддерживаемой только Windows 95 и Windows NT 4, который предназначен для работы в Windows 98/2000. Такая универсальность достигается внедрением DLL rpiAccessProcess в пространство внешнего процесса, так чтобы функция SetForegroundWindow могла работать внутри приложения, выводя его, таким образом, на передний план. Принцип дейс­ твия данной функции описывается в главе 20. Во всяком случае, вы сможете при­ менять ее в Windows 98/2000, когда потребуется SetForegroundWindow. (Кста­ ти, я считаю, что это свойство следует использовать только в особых ситуациях.) Применение подсчета используемости Теоретически простейший способ решения этой задачи – поддерживать в VB­ приложении небольшой текстовый файл, который содержит одно число в качестве признака используемости данного приложения и находится в каком­нибудь фик­ сированном каталоге, например в системном каталоге Windows. Тогда приложе­ ние может в событии Load основной формы проверять признак используемости, просто открывая файл стандартными средствами. Если значение в файле равно единице, приложение мгновенно завершается без активизации события Unload. Это может быть сделано при помощи самого категоричного оператора End. Если значение в файле равно нулю, приложение устанавливает это значение в единицу и выполняется нормально. Потом в событии Unload приложение устанавливает это значение обратно в нуль. Таким образом, лишь один экземпляр приложения может работать нормально, и только этот эк­ земпляр изменяет признак используемости – число в файле.
13 Конечно, подобный способ может быть реализован более элегантно, с помо­ щью файла, отображаемого в память, но это решение потребует дополнительного программного кода. Далее приведены процедуры событий Load и Unload главной формы прило­ жения в виде псевдокода: Private Sub Form_Load() Dim lUsageCount As Long ' Получить текущее состояние используемости из отображаемого ' в память файла. lUsageCount = GetUsageCount If lUsageCount > 0 Then MsgBox "Приложение уже выполняется" End Else ' Установить состояние используемости в единицу. SetUsageCount 1 End If End Sub Private Sub Form_Unload() SetUsageCount 0 End Sub Выбор реализации этого подхода остается за вами, а сейчас давайте рассмот­ рим более простое решение. Библиотека rpiUsageCount Как вы увидите при обсуждении использования библиотеки rpiAccessProcess в памяти, выделенной другому процессу, исполняемый файл (EXE или DLL) может содержать совместно используемую память. С ней одновременно работают все экземпляры данного загрузочного модуля. Если поместить совместно использу­ емую переменную в DLL, то к ней получит доступ каждый работающий с данной DLL процесс. Следует добавить, что совместно используемая (shared) и глобальная (global) переменные – это не одно и то же. Глобальные переменные доступны всей DLL, но каждый процесс, который загружает DLL, получает отдельную (separate) копию каждой глобальной переменной. Поэтому глобальные переменные доступны в пределах одного процесса, а совместно используемые – по всей системе. Несмотря на то что VB не позволяет создавать совместно используемую память в загрузочном модуле VB, очень просто это сделать в DLL, написанной на VC++. Как определить, выполняется ли приложение
14 Процессы В архиве примеров вы найдете DLL, которая называется rpiUsageCount.dll. Полный исходный код VC++ приведен в листинге 11.5 . Листинг 11.5 . Исходный код VC++ для rpiUsageCount.dll // rpiUsageCount.cpp #include <windows.h> // Задаем раздел совместно используемых данных в DLL. // ВСЕ СОВМЕСТНО ИСПОЛЬЗУЕМЫЕ ПЕРЕМЕННЫЕ ДОЛЖНЫ БЫТЬ ПРОИНИЦИАЛИЗИРОВАНЫ. #pragma data_seg("Shared") long giUsageCount = 0; #pragma data_seg() // Просим редактор связей сделать этот раздел совместно используемым // для чтения/записи. #pragma comment(linker, "/section:Shared,rws") /////////////////////////////////////////////////////////////// // Прототипы экспортируемых функций. /////////////////////////////////////////////////////////////// long WINAPI rpiIncrementUsageCount(); long WINAPI rpiDecrementUsageCount(); long WINAPI rpiGetUsageCount(); long WINAPI rpiSetUsageCount(long lNewCount); /////////////////////////////////////////////////////////////// // DllMain /////////////////////////////////////////////////////////////// HANDLE hDLLInst = NULL; BOOL WINAPI DllMain (HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved) { // Сохраняем дескриптор данного экземпляра для будущего // использования. hDLLInst = hInst; switch(ul_reason_for_call) { case DLL_PROCESS_ATTACH: // Здесь происходит инициализация. break; case DLL_PROCESS_DETACH: // Здесь производится очистка. break; } return TRUE; }
15 /////////////////////////////////////////////////////////////// // Экспортируемые функции. /////////////////////////////////////////////////////////////// long WINAPI rpiIncrementUsageCount() { return InterlockedIncrement(&giUsageCount); } long WINAPI rpiDecrementUsageCount() { return InterlockedDecrement(&giUsageCount); } long WINAPI rpiGetUsageCount() { return giUsageCount; } long WINAPI rpiSetUsageCount(long lNewCount) { giUsageCount = lNewCount; return giUsageCount; } В данной DLL есть одна совместно используемая переменная, принадлежащая к типу long, которая называется lUsageCount. Для управления ей DLL экспор­ тирует четыре функции. (Это даже больше, чем нужно.) rpiIncrementUsageCount rpiDecrementUsageCount rpiGetUsageCount rpiSetUsageCount Их декларации в VB выглядят таким образом: Declare Function rpiIncrementUsageCount Lib "rpiUsageCount.dll" () As Long Declare Function rpiDecrementUsageCount Lib "rpiUsageCount.dll" () As Long Declare Function rpiGetUsageCount Lib "rpiUsageCount.dll" () As Long Declare Function rpiSetUsageCount Lib "rpiUsageCount.dll" () As Long Для того чтобы использовать эту DLL, нужно только добавить код, приведен­ ный в листинге 11.6, к процедурам событий Load и Unload главной формы VB. Листинг 11.6 . Вызов функции rpiGetUsageCount Private Sub Form_Load() Dim lUsageCount As Long ' Получаем текущее состояние используемости. lUsageCount = rpiGetUsageCount If lUsageCount > 0 Then MsgBox "Приложение уже запущено" End Как определить, выполняется ли приложение
1 Процессы Else rpiSetUsageCount 1 End If End Sub Private Sub Form_Unload() rpiSetUsageCount 0 End Sub Недостаток использования данной DLL в том, что она занимает 49152 байт памяти. Кроме того, она не осуществляет автоматического переключения на исполняемый эк­ земпляр данного приложения. Придется по­прежнему использовать FindWindow для получения дескриптора окна, который затем передается SetForegroundWindow (или rpiSetForegroundWindow). Список процессов Последний способ определить, что данное приложение уже выполняется, является самым очевидным и должен работать всегда (хотя при слове «всегда» я почему­то начинаю чувствовать себя не совсем в своей тарелке). Способ заклю­ чается в следующем: программа проходит по списку всех исполняемых процессов и проверяет имя (возможно даже полное имя) файла каждого загрузочного моду­ ля. К сожалению, как вам уже известно, такой подход требует разработки разных программ для Windows NT и Windows 9x. Версией для Windows NT является GetWinNTProcessID. Этой функции передается либо имя, либо полное имя (имя и путь) загрузочного файла. Она проходит по списку процессов и пытается выполнять сравнение с полученным от вас образцом имени (регистр здесь не имеет значения). Функция возвращает идентификатор последнего процесса, имя загрузочного модуля которого совпало с образцом, и общее количество совпадений. Если возвращаемое значение равно нулю, то это приложение не загружено в память. В листингах 11.7 и 11.8 приведен код (обе версии), включающий все необходимые объявления. Листинг 11.7. Обход списка процессов Windows NT Option Explicit ' ************************************ ' ВНИМАНИЕ: Только для Windows NT 4.0. ' ************************************ Public Const MAX_PATH = 260 Public Declare Function EnumProcesses Lib "PSAPI.DLL" ( _ idProcess As Long, _ ByVal cBytes As Long, _ cbNeeded As Long _ ) As Long
1 Public Declare Function EnumProcessModules Lib "PSAPI.DLL" ( _ ByVal hProcess As Long, _ hModule As Long, _ ByVal cb As Long, _ cbNeeded As Long _ ) As Long Public Declare Function GetModuleBaseName Lib "PSAPI.DLL" _ Alias "GetModuleBaseNameA" ( _ ByVal hProcess As Long, _ ByVal hModule As Long, _ ByVal lpBaseName As String, _ ByVal nSize As Long _ ) As Long Public Declare Function GetModuleFileNameEx Lib "PSAPI.DLL" _ Alias "GetModuleFileNameExA" ( _ ByVal hProcess As Long, _ ByVal hModule As Long, _ ByVal lpFileName As String, _ ByVal nSize As Long _ ) As Long Public Const STANDARD_RIGHTS_REQUIRED = &HF0000 Public Const SYNCHRONIZE = &H100000 Public Const PROCESS_VM _READ = &H10 Public Const PROCESS_QUERY_INFORMATION = &H400 Public Const PROCESS_ALL _ACCESS = STANDARD_RIGHTS_REQUIRED Or _ SYNCHRONIZE Or &HFFF Declare Function OpenProcess Lib "kernel32" ( _ ByVal dwDesiredAccess As Long, _ ByVal bInheritHandle As Long, _ ByVal dwProcessId As Long _ ) As Long Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) _ As Long '  Public Function GetWinNTProcessID(sFQEXEName As String, _ sEXEName As String, ByRef cMatches As Long) As Long ' Получает ID процесса по имени или полному имени (путь/имя) EXE. ' If sFQName <> "" then используем этот образец для поиска совпадений. ' If sName <> "" then используем только имя для поиска совпадений. ' Возвращает 0, если таких процессов нет, иначе ID последнего ' совпадающего процесса. ' Возвращает количество совпадений в выходном параметре cMatches. ' Возвращает FQName, если она пуста. Как определить, выполняется ли приложение
1 Процессы ' Возвращает 1, если пусты и sFQName, и sName. ' Возвращает 2, если произошла ошибка при получении списка процессов. Dim i As Integer, j As Integer, l As Long Dim cbNeeded As Long Dim hEXE As Long Dim hProcess As Long Dim lret As Long Dim cProcesses As Long Dim lProcessIDs() As Long Dim sEXENames() As String Dim sFQEXENames() As String '  ' Сначала получаем массив идентификаторов (ID) процессов. '  ' Первая попытка. cProcesses = 25 Do ' Размер массива. ReDim lProcessIDs(1 To cProcesses) ' Перечисляем. lret = EnumProcesses(lProcessIDs(1), cProcesses * 4, cbNeeded) Iflret=0Then GetWinNTProcessID = 2 Exit Function End If ' Сравниваем требуемое количество байтов с размером массива в байтах. ' Если меньше, тогда мы получили все, что требовалось. If cbNeeded < cProcesses * 4 Then Exit Do Else cProcesses = cProcesses * 2 End If Loop cProcesses = cbNeeded / 4 ReDim Preserve lProcessIDs(1 To cProcesses) ReDim sEXENames(1 To cProcesses) ReDim sFQEXENames(1 To cProcesses) '  ' Получаем имена EXE. '  For i = 1 To cProcesses
1 ' Используем OpenProcess для получения дескриптора каждого процесса. hProcess = OpenProcess(PROCESS_QUERY_INFORMATION _ Or PROCESS_VM _READ, 0&, lProcessIDs(i)) ' Осторожнее с системными процессами. Select Case lProcessIDs(i) Case 0 ' Системный процесс Idle. sEXENames(i) = "Idle Process" sFQEXENames(i) = "Idle Process" Case 2 sEXENames(i) = "System" sFQEXENames(i) = "System" Case 28 sEXENames(i) = "csrss.exe" sFQEXENames(i) = "csrss.exe" End Select ' Если ошибка, то пропускаем этот процесс. If hProcess = 0 Then GoTo hpContinue End If ' Теперь получаем дескриптор первого модуля ' этого процесса, так как первый модуль – это EXE. hEXE=0 lret = EnumProcessModules(hProcess, hEXE, 4&, cbNeeded) If hEXE = 0 Then GoTo hpContinue ' Получаем имя модуля. sEXENames(i) = String$(MAX_PATH, 0) lret = GetModuleBaseName(hProcess, hEXE, sEXENames(i), _ Len(sEXENames(i))) sEXENames(i) = Trim0(sEXENames(i)) ' Получаем полный путь. sFQEXENames(i) = String$(MAX_PATH, 0) lret = GetModuleFileNameEx(hProcess, hEXE, sFQEXENames(i), _ Len(sFQEXENames(i))) sFQEXENames(i) = Trim0(sFQEXENames(i)) hpContinue: ' Закрываем дескриптор. lret = CloseHandle(hProcess) Next '  ' Проверяем на совпадение. '  Как определить, выполняется ли приложение
200 Процессы cMatches = 0 If sFQEXEName <> "" Then For i = 1 To cProcesses If LCase$(sFQEXENames(i)) = LCase$(sFQEXEName) Then cMatches = cMatches + 1 GetWinNTProcessID = lProcessIDs(i) End If Next ElseIf sEXEName <> "" Then For i = 1 To cProcesses If LCase$(sEXENames(i)) = LCase$(sEXEName) Then cMatches = cMatches + 1 GetWinNTProcessID = lProcessIDs(i) sFQEXEName = sFQEXENames(i) End If Next Else GetWinNTProcessID = 1 End If End Function В версии для Windows 9x используется Toolhelp. Соответствующая функция (и требуемые декларации) приведены в листинге 11.8 . Листинг 11.8 . Обход списка процессов Windows 9x Option Explicit ' *********************************** ' ВНИМАНИЕ: только для Windows 95/98. ' *********************************** Public Const MAX_MODULE_NAME32 = 255 Public Const MAX_PATH = 260 Public Const TH32CS_SNAPHEAPLIST = &H1 Public Const TH32CS_SNAPPROCESS = &H2 Public Const TH32CS_SNAPTHREAD = &H4 Public Const TH32CS_SNAPMODULE = &H8 Public Const TH32CS_SNAPALL = (TH32CS_SNAPHEAPLIST _ Or TH32CS_SNAPPROCESS Or _ Or TH32CS_SNAPTHREAD _ Or TH32CS_SNAPMODULE) Public Const TH32CS_INHERIT = &H80000000 ''HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags, _ '' DWORD th32ProcessID ); Public Declare Function CreateToolhelp32Snapshot Lib "kernel32" ( _ ByVal dwFlags As Long, _
201 ByVal th32ProcessID As Long _ ) As Long Public Declare Function Process32First Lib "kernel32" ( _ ByVal hSnapShot As Long, _ lppe As PROCESSENTRY32 _ ) As Long Public Declare Function Process32Next Lib "kernel32" ( _ ByVal hSnapShot As Long, _ lppe As PROCESSENTRY32 _ ) As Long Public Type PROCESSENTRY32 dwSize As Long cntUsage As Long th32ProcessID As Long ' Идентификатор (ID) процесса. th32DefaultHeapID As Long th32ModuleID As Long ' Только для функций Toolhelp. cntThreads As Long ' Количество потоков. th32ParentProcessID As Long ' (ID) родительского процесса. pcPriClassBase As Long dwFlags As Long szExeFile As String * MAX_PATH ' Путь/имя файла EXE. End Type Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) _ As Long '  Function GetWin95ProcessID(sFQName As String, sName As String, _ ByRef cMatches As Long) As Long ' *********************************** ' ВНИМАНИЕ: только для Windows 95/98. ' *********************************** ' Получает ID процесса. ' Если sFQName <> "", используем этот образец для поиска ' совпадений. ' Если sName <> "", используем только имя для поиска совпадений. ' Возвращает 0, если таких процессов нет, ID последнего совпадающего ' процесса. ' Возвращает количество совпадений в выходном параметре cMatches. ' Возвращает FQName, если она пуста. ' Возвращает 1, если произошла ошибка при получении списка процессов. Dim i As Integer, c As Currency Dim hSnapShot As Long Как определить, выполняется ли приложение
202 Процессы Dim lret As Long ' Для хранения различных возвращаемых значений. Dim cProcesses As Long Dim cProcessIDs() As Currency Dim lProcessIDs() As Long Dim sEXENames() As String Dim sFQEXENames() As String Dim procEntry As PROCESSENTRY32 procEntry.dwSize = LenB(procEntry) ' Перебираем все процессы. hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0&) If hSnapShot = 1 Then GetWin95ProcessID = 1 Exit Function End If ' Инициализируем. ReDim sFQEXENames(1 To 25) ReDim sEXENames(1 To 25) ReDim cProcessIDs(1 To 25) cProcesses = 0 ' Обрабатываем первый процесс. lret = Process32First(hSnapShot, procEntry) Iflret>0Then cProcesses = cProcesses + 1 sFQEXENames(cProcesses) = Trim0(procEntry.szExeFile) sEXENames(cProcesses) = GetFileName(sFQEXENames(cProcesses)) If procEntry.th32ProcessID < 0 Then c = CCur(procEntry.th32ProcessID) + 2 ^ 32 Else c = CCur(procEntry.th32ProcessID) End If cProcessIDs(cProcesses) = c End If ' Обрабатываем остальные. Do lret = Process32Next(hSnapShot, procEntry) Iflret=0ThenExitDo cProcesses = cProcesses + 1 If UBound(sFQEXENames) < cProcesses Then ReDim Preserve sFQEXENames(1 To cProcesses + 10) ReDim Preserve sEXENames(1 To cProcesses + 10) ReDim Preserve cProcessIDs(1 To cProcesses + 10) End If sFQEXENames(cProcesses) = Trim0(procEntry.szExeFile) sEXENames(cProcesses) = GetFileName(sFQEXENames(cProcesses))
203 If procEntry.th32ProcessID < 0 Then c = CCur(procEntry.th32ProcessID) + 2 ^ 32 Else c = CCur(procEntry.th32ProcessID) End If cProcessIDs(cProcesses) = c Loop CloseHandle hSnapShot '  ' Ищем совпадения. '  cMatches = 0 If sFQName <> "" Then For i = 1 To cProcesses If LCase$(sFQEXENames(i)) = LCase$(sFQName) Then cMatches = cMatches + 1 GetWin95ProcessID = lProcessIDs(i) End If Next ElseIf sName <> "" Then For i = 1 To cProcesses If LCase$(sEXENames(i)) = LCase$(sName) Then cMatches = cMatches + 1 GetWin95ProcessID = lProcessIDs(i) sFQName = sFQEXENames(i) End If Next Else GetWin95ProcessID = 1 End If End Function Как определить, выполняется ли приложение
Глава 12. Потоки Visual Basic не поддерживает создание потоков в приложениях (только в очень ограниченном виде с динамическими библиотеками и управляющими элементами ActiveX), несмотря на то что API­функция CreateThread в VB работает. Более того, так как создание и использование потоков требует особой осторожности, не рекомендуется создавать потоки в приложениях VB. Однако, как вы увидите, это не означает, что нельзя с выгодой управлять существующими потоками. Дескрипторы и идентификаторы потоков Тема о дескрипторах, псевдодескрипторах и идентификаторах потоков анало­ гична предыдущей теме об аналогичных характеристиках процессов. Вы знаете, что функция CreateProcess возвращает идентификатор и де­ скриптор первого (и только первого) потока, выполняющегося во вновь созданном процессе. Функция CreateThread тоже возвращает идентификатор потока, но область действия которого – вся система. Поток может использовать функцию GetCurrentThreadId, чтобы получить собственный ID. Вам известно, что функция GetWindowThreadProcessId воз­ вращает идентификатор того потока, который создал конкретное окно (как иден­ тификатор процесса, которому принадлежит данный поток). Согласно документации Win32, Win32 API не предлагает способа для по­ лучения дескриптора потока по его идентификатору. Если бы дескрипторы можно было находить таким образом, то процесс, которому принадлежат по­ токи, завершался бы неудачей, так как другой процесс смог бы выполнять несанкционированные операции с его потоками, например, приостанавливать поток, возобновлять его действие, изменять приоритет или завершать его ра­ боту. Запрашивать дескриптор следует у процесса, создавшего данный поток, или у самого потока. Наконец, поток может вызывать функцию GetCurrentThread для получе­ ния собственного псевдодескриптора. Как и в случае псевдодескрипторов процес­ сов, псевдодескриптор потока может использоваться только для вызова процесса и не может наследоваться. Можно использовать функцию DuplicateHandle для получения настоящего дескриптора потока по его псевдодескриптору так же, как это делается в процессах. Приоритет потоков Термин многозадачность (multitasking), или мультипрограммирование (multi­ programming), обозначает возможность управлять несколькими процессами (или
205 несколькими потоками) на базе одного процессора. Многопроцессорной обработ- кой (multiprocessing) называется управление некоторым числом процессов или потоков на нескольких процессорах. Компьютер может одновременно выполнять команды двух разных процессов только в многопроцессорной среде. Однако даже на одном процессоре с помощью переключения задач (task switching) можно создать впечатление, что одновремен­ но выполняются команды нескольких процессов. В старой 16­разрядной Windows существовал только один поток. Более того, в данной системе был реализован метод кооперативной (cooperative) многозадач­ ности, который состоит в том, что каждое приложение само отвечает за высвобож­ дение единственного системного потока, после чего могут выполняться другие приложения. Если программа выполняла задачи, требующие значительного вре­ мени, такие как форматирование гибкого диска, все другие загруженные прило­ жения должны были ждать. А если программа, написанная с ошибками, входила в бесконечный цикл, то вся система становилась непригодной для использования («зависала»), требовала перезагрузки с выключением питания. Уровни приоритета потоков Win32 значительно отличается от Win16. Во­первых, она является много­ поточной (multithreaded), что определяет ее многозадачность. Во­вторых, в ней реализована модель вытесняющей (preemptive) многозадачности, в которой опе­ рационная система решает, когда каждый поток получает процессорное время, вы­ деляемое квантами времени (time slices), и сколько именно времени выделяется. Временной интервал в Windows называется квантом. Продолжительность кванта времени зависит от аппаратуры и может факти­ чески меняться от потока к потоку. Например, базовое значение в Windows 95 составляет 20 мс, для Windows NT Workstation (на базе процессора Pentium) – 30 мс, для Windows NT Server – 180 мс. Давайте выясним, каким образом Windows выделяет кванты времени потокам в системе. Эти процедуры в Windows 9x и Windows NT довольно похожи, но не идентичны. Каждый поток в системе имеет уровень приоритета (priority level), который представляет собой число в диапазоне от 0 до 31. Ниже перечислено то самое основное, что нужно знать:  если существуют какие­либо потоки с приоритетом 31, которые требуют процессорного времени, то есть не находятся в состоянии ожидания (not idle), операционная система перебирает эти потоки (независимо от того, каким процессам они принадлежат), выделяя по очереди каждому из них кванты времени. Потокам с более низким приоритетом кванты времени совсем не выделяются, поэтому они не выполняются. Если нет активных потоков с приоритетом 31, операционная система ищет активные потоки с уровнем приоритета 30 и т.д . Не забудьте, однако, что потоки довольно часто простаивают. Тот факт, что приложение загружено, не означает, что все его потоки активны. Поэтому у потоков с более низким приоритетом все же есть возможность работать. Более того, если пользователь нажимает клави­ Приоритет потоков
20 Потоки шу, относящуюся к процессу, потоки которого простаивают, операционная система временно выделяет процессорное время соответствующему потоку, чтобы он мог обработать нажатие клавиши;  приоритет 0 зарезервирован исключительно за специальным системным потоком, который называется потоком нулевой страницы (zero page thread). Он освобождает незадействованные области памяти. Существует также по­ ток idle, который работает с уровнем приоритета 0, опрашивая систему в поисках какой­нибудь работы;  если поток с произвольным приоритетом выполняется в тот момент, когда потоку с большим приоритетом потребовалось процессорное время (на­ пример, он получает сообщение о том, что пользователь щелкнул мышью), операционная система немедленно вытесняет поток с меньшим приоритетом и отдает процессорное время потоку с большим. Таким образом, поток может не успеть завершить выделенный ему квант времени;  для того чтобы перейти с одного потока на другой, система осуществляет пе­ реключение контекста (context switch). Это процедура, сохраняющая состо­ яние процессора (регистров и стека) и загрузки соответствующих значений другого потока. Назначение приоритета потоку Назначение потоку приоритета происходит в два этапа. Во­первых, каждому процессу в момент создания присваивается класс приоритета. Узнать класс при­ оритета можно с помощью функции GetPriorityClass, а изменить – с помо­ щью функции SetPriorityClass. В табл. 12.1 приведены имена классов при­ оритета процессов, уровни приоритета и константы, которые используются с этими вышеупомянутыми функциями (как и с функцией CreateProcess). Таблица 12.1. Уровни приоритета процессов Имя класса Уровень Символьная константа приоритета приоритета класса Idle 4 IDLE_PRIORITY_CLASS=&H40 Normal 8 NORMAL_PRIORITY_CLASS=&H20 High 13 HIGH_PRIORITY_CLASS=&H80 Realtime 24 REALTIME_PRIORITY_CLASS=&H100 Большинство процессов должно получать класс уровня приоритета Normal (обычный). Однако некоторым приложениям, таким как приложения монито­ ринга системы, возможно, более уместно назначать приоритет Idle (ожидания). Назначения приоритета Realtime (реального времени) обычно следует избегать, потому что в этом случае потоки изначально получают приоритет более высокий, чем системные потоки, такие как потоки ввода от клавиатуры и мыши, очистки кэша и обработки нажатия клавиш Ctrl+Alt+Del. Такой приоритет может быть подходящим для краткосрочных, критичных к времени выполнения процессов, которые относятся к взаимодействию с аппаратурой.
20 При создании уровень приоритета потока по умолчанию устанавливается рав­ ным уровню класса приоритета процесса, создавшего данный поток. Тем не менее можно использовать функцию SetThreadPriority, чтобы изменить приоритет потока: BOOL SetThreadPriority( HANDLE hThread, // Дескриптор потока. int nPriority // Уровень приоритета потока. ); Параметр nPriority используется для изменения приоритета потока отно­ сительно приоритета процесса, которому принадлежит данный поток. Возможные значения параметра nPriority и эффект их воздействия на уровень приоритета потока приведены в табл. 12.2. Таблица 12.2 . Корректировка уровней приоритета потоков Константа Уровень приоритета потока THREAD_PRIORITY_NORMAL Уровень приоритета класса THREAD_PRIORITY_ABOVE_NORMAL Уровень приоритета класса + 1 THREAD_PRIORITY_BELOW_NORMAL Уровень приоритета класса – 1 THREAD_PRIORITY_HIGHEST Уровень приоритета класса + 2 THREAD_PRIORITY_LOWEST Уровень приоритета класса – 2 THREAD_PRIORITY_IDLE Устанавливает уровень приоритета 1 для всех классов приоритета процессов за исключением Realtime. В этом случае устанавливает уровень приоритета 16 THREAD_PRIORITY_TIME_CRITICAL Устанавливает уровень приоритета 15 для всех классов приоритета процессов за исключением Realtime. В этом случае устанавливает уровень приоритета 31 Повышение приоритета потока и квант изменений приоритета Диапазон приоритета от 1 до 15 известен как диапазон динамического при­ оритета (dynamic priority), а диапазон от 16 до 31 – как диапазон приоритета реального времени (realtime priority). В Windows NT приоритет потока, находящийся в динамическом диапазоне, мо­ жет временно повышаться операционной системой в различные моменты времени. Соответственно, нижний уровень приоритета потока (установленный програм­ мистом с помощью API­функций) называется уровнем его базового приоритета (base priority). API­функция Windows NT SetProcessPriorityBoost может использоваться для разрешения или запрещения временных изменений приори­ тета (priority boosting). Правда, она не поддерживается в Windows 9x. Бывают также случаи, когда кванты времени, выделяемые потоку, временно увеличиваются. Стремясь плавно выполнять операции, Windows будет повышать приоритет потока или увеличивать продолжительность его кванта времени при следующих условиях: Приоритет потоков
20 Потоки  если поток принадлежит приоритетному процессу (foreground process), то есть процессу, окно которого активно и имеет фокус ввода;  если поток первым вошел в состояние ожидания;  если поток выходит из состояния ожидания;  если поток совсем не получает процессорного времени. Состояния потоков Потоки могут находиться в одном из нескольких состояний:  Ready (готов) – находящийся в пуле (pool) потоков, ожидающих выполнения;  Standby (резерв) – выбран для выполнения в следующем кванте времени про­ цессора. Только один поток процессора может быть в состоянии Standby;  Running (выполнение) – выполняющийся на процессоре;  Waiting (ожидание), также называется idle или suspended, приоста­ новленный – в состоянии ожидания, которое завершается тем, что поток начинает выполняться (состояние Running) или переходит в состояние Ready;  Transition (переход) – готов к выполнению, но его стек отсутствует в фи­ зической памяти, выгружен в файл подкачки на жестком диске. Как только содержимое стека будет возвращено в память, поток перейдет в состояние Ready;  Terminated (завершение) – завершено выполнение всех команд потока. Впоследствии его можно удалить. Если поток не удален, система может вновь установить его в исходное состояние для последующего использования. Синхронизация потоков Выполняющимся потокам часто необходимо каким­то образом взаимодейс­ твовать. Например, если несколько потоков пытаются получить доступ к неко­ торым глобальным данным, то каждому потоку нужно предохранять данные от изменения другим потоком. Иногда одному потоку нужно получить информацию о том, когда другой поток завершит выполнение задачи. Такое взаимодействие обязательно между потоками как одного, так и разных процессов. Синхронизация потоков (thread synchronization) – это обобщенный термин, от­ носящийся к процессу взаимодействия и взаимосвязи потоков. Учтите, что синхро­ низация потоков требует привлечения в качестве посредника самой операционной системы. Потоки не могут взаимодействовать друг с другом без ее участия. В Win32 существует несколько методов синхронизации потоков. Бывает, что в конкретной ситуации один метод более предпочтителен, чем другой. Давайте вкратце ознакомимся с этими методами и используем некоторые из них в прило­ жениях VB. Критические секции Один из методов синхронизации потоков состоит в использовании критичес­ ких секций (critical sections). Это единственный метод синхронизации потоков, который не требует привлечения ядра Windows. (Критическая секция не является
20 объектом ядра.) Однако этот метод может использоваться только для синхрони­ зации потоков одного процесса. Первый шаг – объявить глобальную переменную типа CRITICAL_SECTION: CRITICAL_SECTION gCS; Такой объект называется объектом «критическая секция» (critical section object). Фрагмент кода, рассматриваемый данным потоком как критический, вставляется между вызовами функций EnterCriticalSection и LeaveCriticalSection, которые принимают в качестве параметра объект «критическая секция» gCS (вмес­ то него могут быть и другие): EnterCriticalSection(gCS); . . . LeaveCriticalSection(gCS); Вызов EnterCriticalSection(gCS)означает: «Можно ли получить от gCS разрешение на выполнение следующего далее критического кода?» Вызов LeaveCriticalSection(gCS)сообщает gCS: «Выполнение критического кода завершено. Теперь можно разрешить другому потоку выполнять свой критичес­ кий код». Функция EnterCriticalSection проверяет, не выполняет ли уже какой­ нибудь другой поток критическую секцию своей программы, связанную с данным объектом критической секции. Если нет, поток получает разрешение на выполне­ ние своего критического кода, точнее, ему не запрещают это делать. Если да, то поток, обратившийся с запросом, переводится в состояние ожидания, а о запросе делается запись. Так как нужно создавать записи, объект «критическая секция» представляет собой структуру данных. Когда функция LeaveCriticalSection вызывается потоком, который вла­ деет в текущий момент разрешением на выполнение своей критической секции кода, связанной с данным объектом «критическая секция», система может прове­ рить, нет ли в очереди другого потока, ожидающего освобождения этого объекта. Затем система может вывести ждущий поток из состояния ожидания, и он про­ должит свою работу (в выделенные ему кванты времени). Синхронизация с использованием объектов ядра Многие объекты ядра, включая процесс, поток, файл, мьютекс, семафор, уве­ домление об изменении файла и событие, могут находиться в одном из двух со­ стояний – «свободно» (signaled) и «занято» (nonsignaled). Вероятно, проще пред­ ставлять себе эти объекты подключенными к лампочке, как на рис. 12.1 . Если свет горит, объект свободен, в обратном случае объект занят. Например, в момент создания процесса его объект ядра находится в состоя­ нии «занято». Когда процесс завершается, объект переходит в состояние «свободно». Аналогично выполняющиеся потоки (то есть их объекты) пребывают в состоянии «занято», но переходят в состояние «свободно», когда завершают работу. На самом деле некоторые объекты, такие как мьютекс, семафор, событие, уведомление об Синхронизация потоков
210 Потоки изменении файла, таймер ожидания, существуют исключи­ тельно для того, чтобы вырабатывать сигналы «свободно» и «занято». Смысл всей этой «сигнализации» в том, чтобы поток мог приостанавливать свою работу до того момента, когда заданный объект перейдет в состояние «свободно». Напри­ мер, поток одного процесса может временно прекратить работу до завершения другого, просто подождав, когда объект ядра этого другого процесса перейдет в состояние «свободно». Посредством вызова функций WaitForSingleObject и WaitForMultipleObjects поток приостанавливает свое выполнение до того момента, когда заданный объект (или объекты) перейдет в состояние «свободно». Давайте ограни­ чимся рассмотрением функции WaitForSingleObject, декларация которой выглядит так: DWORD WaitForSingleObject( HANDLE hHandle, // Дескриптор объекта ожидания. DWORD dwMilliseconds // Время ожидания в миллисекундах. ); или в VB: Declare Function WaitForSingleObject Lib "kernel32" _ Alias "WaitForSingleObject" ( _ ByVal hHandle As Long, _ ByVal dwMilliseconds As Long _ ) As Long Параметр hHandle является дескриптором объекта, уведомление о свобод­ ном состоянии которого требуется получить, а dwMilliseconds – это время, которое вызывающий поток готов ждать. Если dwMilliseconds равно нулю, функция немедленно вернет текущий статус заданного объекта. Таким образом можно протестировать состояние объекта. Параметру можно также присваивать значение символьной константы INFINITE (= 1), в этом случае вызывающий поток будет ждать неограниченное время. Функция WaitForSingleObject переводит вызывающий поток в состояние ожидания до того момента, когда она передаст ему свое возвращаемое значение. Ниже перечислены возможные возвращаемые значения:  WAIT_OBJECT_0 – объект находится в состоянии «свободно»;  WAIT_TIMEOUT – интервал ожидания, заданный dwMilliseconds, истек, а нужный объект по­прежнему находится в состоянии «занято»;  WAIT_ABANDONED относится только к мьютексу и означает, что объект не был освобожден потоком, который владел им до своего завершения;  WAIT_FAILED – при выполнении функции произошла ошибка. Объект ядра Рис. 12.1. Объект ядра
211 Ожидание завершения приложения Давайте проведем эксперимент. Требуется написать функцию, с помощью которой приложение VB приостановит свою работу до завершения другого прило­ жения. Другое приложение задается именем своего загрузочного файла, в данном примере именем findtext.exe. (Вам, может быть, потребуется изменить это имя в своем эксперименте.) Кроме самих функций OpenProcess и WaitForSingleObject потребуются некоторые возвращаемые функцией WaitForSingleObject значения: Public Const WAIT_FAILED = &HFFFFFFFF Public Const WAIT_OBJECT_0 = 0 Public Const WAIT_TIMEOUT = &H102 Функция WaitForAppToQuit, приведенная в следующей далее программе, ис­ пользует некоторые функции, которые были определены в главе 11 (они находятся в rpiAPI.bas из архива) – GetWinNTProcessID и ProcHndFromProcIDSync. Обратите особое внимание на возвращаемые коды ошибок, чтобы не перепутать их со значениями, возвращаемыми функцией WaitForSingleObject. Public Function WaitForAppToQuit(sEXEName As String, sFQEXEName As _ String, lWaitSeconds As Long) As Integer ' Приостанавливает выполнение до завершения работы заданного EXE, ' или до истечения заданного времени lWaitSeconds в с. ' ' Если sFQEXEName не пуста, используем ее. Иначе используем sEXEName. ' Возвращает 0 при удачном завершении. ' Возвращает 1, если не существует процесса с заданным именем EXE. ' Возвращает 2, если пусты и sFQName, и sName. ' Возвращает 3, если произошла ошибка во время получения списка ' процессов. ' Возвращает 4, если невозможно получить дескриптор существующего ' процесса. ' Во всех остальных случаях возвращает значения, возвращаемые ' WaitForSingleObject. Dim lret As Long Dim hProcessID As Long Dim hProcess As Long Dim cMatches As Long Dim sEXE As String hProcessID = GetWinNTProcessID(sFQEXEName, sEXEName, cMatches) If hProcessID <= 0 Then ' Ошибка  преобразуем в код ошибки данной функции. If hProcessID = 0 Then WaitForAppToQuit = 1 ' Нет такого процесса. ElseIf hProcessID = 1 Then WaitForAppToQuit = 2 ' Нет заданного EXE. Синхронизация потоков
212 Потоки ElseIf hProcessID = 2 Then WaitForAppToQuit = 3 ' Ошибка при получении списка процессов. End If Exit Function End If ' Получаем дескриптор по ID процесса. hProcess = ProcHndFromProcIDSync(hProcessID) If hProcess = 0 Then WaitForAppToQuit = 4 ' Ошибка при получении дескриптора процесса. Exit Function End If ' Ждем. WaitForAppToQuit = WaitForSingleObject(hProcess, 1000& * lWaitSeconds) CloseHandle hProcess End Function Здесь представлена программа для испытания данной функции: Public Sub WaitForAppToQuitExample() Dim lret As Long Dim t As Long t = Timer lret = WaitForAppToQuit("findtext.exe", "", 10&) Debug.Print "Return: " & lret Select Case lret Case WAIT_TIMEOUT Debug.Print "timeout: " & Timer  t Case WAIT_OBJECT_0 Debug.Print "resume: " & Timer  t Case WAIT_FAILED Debug.Print "Failed: " & GetAPIErrorText(Err.LastDllError) End Select End Sub Прежде чем выполнять эту программу, измените имя загрузочного файла на одно из тех имен приложений, которые работают на вашем компьютере. После завершения работы функции вывод может выглядеть приблизительно так: Return: 258 timeout: 10.51171875
213 Если же вы запустите программу и сразу завершите приложение Findtext.exe, то вывод может быть примерно таким: Return: 258 timeout: 2.3046875 Объекты «мьютекс» Мьютекс – это объект ядра, который можно использовать для синхронизации потоков из разных процессов. Он может принадлежать или не принадлежать не­ которому потоку. Если мьютекс принадлежит потоку, то он находится в состоянии «занято». Если данный объект не относится ни к одному потоку, то он находится в состоянии «свободно». Другими словами, принадлежать для него означает быть в состоянии «занято». Если мьютекс не принадлежит ни одному потоку, первый поток, который вызовет функцию WaitForSingleObject, завладевает данным объектом, и тот переходит в состояние «занято». В определенном смысле мьютекс похож на вы­ ключатель, которым может пользоваться любой поток по принципу «первым при­ шел – первым обслужили» (first­come­first­served). Дело в том, что при попытке с помощью вызова функции WaitForSingleObject завладеть мьютексом, который уже находится в состоянии «занято», поток перево­ дится в состояние ожидания до того момента, когда данный объект освободится, то есть когда «владелец» мьютекса его освободит (переведет в состояние «сво­ бодно»). Мьютексы создаются с помощью вызова функции CreateMutex: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // Указатель на атрибуты защиты. BOOL bInitialOwner, // Флаг первоначального владельца. LPCTSTR lpName // Указатель на имя мьютекса. ); или в VB: Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" ( _ lpMutexAttributes As SECURITY_ATTRIBUTES, _ ByVal bInitialOwner As Long, _ ByVal lpName As String _ ) As Long или для передачи значения NULL в первом параметре: Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" ( _ ByVal lpMutexAttributes As Long, _ ByVal bInitialOwner As Long, _ ByVal lpName As String _ ) As Long Первый параметр функции относится к защите и может быть установлен в NULL для запрета наследования дескриптора порожденными процессами. Пара­ Синхронизация потоков
214 Потоки метр bInitialOwner определяет, будет ли вызывающий процесс изначально вла­ деть данным мьютексом (TRUE), а значит, будет ли этот объект находится в состо­ янии «занято», или же мьютекс не будет изначально принадлежать вызывающему процессу (FALSE) и, следовательно, будет пребывать в свободном состоянии. Параметр lpName является именем создаваемого мьютекса. Он может уста­ навливаться в NULL, если имя не требуется. При наличии имени этот объект может совместно использоваться несколькими процессами. Если каким­то процессом создается мьютекс с именем, то поток другого процесса может вызывать функции CreateMutex или OpenMutex с тем же самым именем. В любом случае система просто передаст вызывающему потоку дескриптор исходного мьютекса. Другой спо­ соб совместно использовать мьютекс – вызвать функцию DuplicateHandle. Чтобы работать с несколькими процессами, данный объект должен быть сов­ местно используемым. Причина проста: чтобы завладеть мьютексом или осво­ бодить его, потоку потребуется его дескриптор. Поток освобождает этот объект с помощью вызова функции ReleaseMutex: BOOL ReleaseMutex( HANDLE hMutex // Дескриптор мьютекса. ); или в VB: Declare Function ReleaseMutex Lib "kernel32" ( _ ByVal hMutex As Long _ ) As Long А что случится, если владеющий мьютексом поток завершится, предварительно не освободив его? В действительности система сама освобождает такой мьютекс. Поток, который вызывает функцию WaitForSingleObject для этого объекта, получит возвращенное значение WAIT_ABANDONED, которое указывает на воз­ никшие проблемы с только что завершимся потоком­владельцем. В этом случае ждущий поток должен определить, стоит продолжать выполнение в обычном режиме или нет. Изменение счетчиков с помощью мьютексов Чтобы рассмотреть использование мьютексов на конкретном примере, созда­ дим два небольших приложения VB. Каждое приложение выполняет отчет до пяти, а затем ждет, пока и другое приложение сосчитает до пяти. Процесс повто­ ряется снова и снова. На рис. 12.2 показано главное окно ThreadSync1.exe. (Этот пример иллюстрирует также использование события.) Для выполнения этого примера запустите приложения ThreadSync1.exe и ThreadSync2.exe и разместите их формы рядом друг с другом. Затем устано­ вите переключатель каждой формы в положение Mutex (Мьютекс). Щелкните по командным кнопкам Create Mutex (Создать мьютекс) каждой из двух форм, а потом по каждой из двух кнопок Start Counting (Начать отсчет). Для пре­ кращения работы нажмите обе командные кнопки Stop Counting (Закончить отсчет).
215 Полный исходный текст этих почти иден­ тичных проектов находится в архиве на сайте издательства «ДМК Пресс» www.dmkpress.ru. Они созданы на основе следующей програм­ мы, которая ведет отчет до пяти, освобождает мьютекс и затем ждет, когда мьютекс освобо­ дится снова. Обратите внимание, что каждое приложение должно освобождать мьютекс по завершении счета, иначе другой процесс не сможет выйти из состояния ожидания. Здесь представлена программа, выполняющая эту задачу: Private Sub cmdStartCountingMutex_ Click() Dim c As Long Dim lret As Long gbStopCounting = False Do c=c+1 txtCount.Text = c Delay 0.5 If(cMod5)=0Then ' Освобождаем мьютекс, чтобы другой процесс мог его получить. lret = ReleaseMutex(hMutex) ' Теперь ждем 30 с, пока мьютекс снова перейдет в состояние ' «свободно». lret = WaitForSingleObject(hMutex, 1000 * 30) End If Loop Until gbStopCounting ' Чтобы освободить другой процесс. lret = ReleaseMutex(hMutex) End Sub События События используются в качестве сигналов о завершении какой­либо опера­ ции. Однако в отличие от мьютексов они не принадлежат ни одному потоку. Например, поток A создает событие с помощью функции CreateEvent и устанав­ ливает объект в состояние «занято». Поток B получает дескриптор этого объекта, вызвав функцию OpenEvent, затем вызывает функцию WaitForSingleObject, чтобы приостановить работу до того момента, когда поток A завершит конкретную задачу и освободит указанный объект. Когда это произойдет, система выведет из Рис. 12.2 . Приложение, демонстрирующее использование мьютексов и событий Синхронизация потоков
21 Потоки состояния ожидания поток B, который теперь владеет информацией, что поток A завершил выполнение своей задачи. Объявление функции CreateEvent записывается таким образом: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // Указатель на атрибуты защиты. BOOL bManualReset, // Флаг интерактивного события. BOOL bInitialState, // Флаг первоначального состояния. LPCTSTR lpName // Указатель на имя события. ); Эта функция возвращает дескриптор создаваемого объекта «событие». Первый параметр определяет, наследуется ли дескриптор порожденными процессами. Если lpEventAttributes имеет значение NULL, дескриптор наследоваться не может. Если параметр bManualReset имеет значение TRUE, то при освобождении объект остается в этом состоянии (в отличие от объекта «мьютекс»). Это значит, что все потоки, ожидающие перехода данного объекта в состояние «свободно», будут выведены системой из состояния ожидания. Такой объект называется со­ бытием с ручным сбросом (manual­reset event), поскольку «разбуженный» (выве­ денный из состояния ожидания) поток может самостоятельно сбросить состояние объекта «событие» в «занято». Если параметр bManualReset имеет значение FALSE, то система автоматически сбрасывает состояние рассматриваемого объек­ та в «занято» после «пробуждения» первого потока, ожидающего освобождения данного объекта. Только один поток выводится из состояния ожидания, как и в случае с мьютексами. Такое событие называют событием с автоматическим сбро­ сом (auto­reset event). Параметр bInitialState определяет первоначальное состояние (если TRUE, то «свободно», если FALSE, то «занято») данного события. Параметру lpName может быть присвоено имя события. Имя предоставляет способ совместного ис­ пользования, например посредством функции OpenEvent. Можно преобразовать CreateEvent к виду VB следующим образом: Declare Function CreateEvent Lib "kernel32" Alias "CreateEventA" ( _ lpEventAttributes As SECURITY_ATTRIBUTES, _ ByVal bManualReset As Long, _ ByVal bInitialState As Long, _ ByVal lpName As String _ ) As Long В качестве дополнительного варианта, если вы не хотите иметь дела с вопроса­ ми защиты, можно установить lpEventAttributes в NULL (0&). В таком случае декларация примет следующий вид: Declare Function CreateEvent Lib "kernel32" Alias "CreateEventA" ( _ ByVal lpEventAttributes As Long, _ ByVal bManualReset As Long, _ ByVal bInitialState As Long, _ ByVal lpName As String _ ) As Long
21 Так же, как и другие дескрипторы, дескриптор события должен быть закрыт с использованием функции CloseHandle. Объявление функции OpenEvent выглядит так: HANDLE OpenEvent( DWORD dwDesiredAccess, // Флаг доступа. BOOL bInheritHandle, // Флаг наследования. LPCTSTR lpName // Указатель на имя события. ); где параметр dwDesiredAccess может принимать одно из трех значений:  EVENT_ALL_ACCESS предоставляет полный доступ к событию;  EVENT_MODIFY_STATE разрешает использование дескриптора события в функциях SetEvent и ResetEvent, так что вызывающий процесс может изменить состояние данного события (но ничего больше). Это важно для событий со сбросом вручную;  SYNCHRONIZE разрешает использование дескриптора события в любых фун­ кциях ожидания (таких как WaitForSingleObject), ждущих освобожде­ ния данного объекта. Декларация функции OpenEvent в VB может быть такой: Declare Function OpenEvent Lib "kernel32" Alias "OpenEventA" ( _ ByVal dwDesiredAccess As Long, _ ByVal bInheritHandle As Long, _ ByVal lpName As String _ ) As Long Следующие декларации также используются совместно с событиями: Declare Function SetEvent Lib "kernel32" (ByVal hEvent As Long) As Long Declare Function ResetEvent Lib "kernel32" (ByVal hEvent As Long) As Long Declare Function PulseEvent Lib "kernel32" (ByVal hEvent As Long) As Long Каждая из этих функций принимает дескриптор события в качестве аргумента. Функция SetEvent устанавливает состояние данного события в «свободно», а ResetEvent «сбрасывает» событие, то есть присваивает событию статус «заня­ то». Функция PulseEvent вызывает SetEvent для освобождения ожидающих потоков, а затем вызывает ResetEvent для перевода данного события в состояние «занято». Изменение счетчиков с помощью событий Пример с рис. 12.2 показывает, как та же цель может быть достигнута с помо­ щью двух событий. Основная программа представлена ниже: Private Sub cmdStartCountingEvent_Click() Dim c As Long Dim lret As Long gbStopCounting = False Синхронизация потоков
21 Потоки Do c=c+1 txtCount.Text = c Delay 0.5 If(cMod5)=0Then ' Генерируем событие для оповещения других процессов. lret = PulseEvent(hEvent1) ' И ждем 30 с сигнала от внешнего события. lret = WaitForSingleObject(hEvent2, 1000 * 30) End If Loop Until gbStopCounting ' Чтобы вывести из ожидания другие процессы. lret = SetEvent(hEvent1) End Sub В этом случае каждое приложение генерирует собственные события, оповещая другое приложение о том, что его задача отсчета до пяти завершена. Затем прило­ жение ждет соответствующего сигнала от другого приложения. Если оба приложения ThreadSync.exe уже выполняются, то просто щелк­ ните по командной кнопке Create Event (Создать событие) каждого проекта, чтобы создать соответствующие им события. Затем нажмите кнопки Open Event (Открыть событие) каждого приложения, чтобы открыть внешние события. Наконец, щелкните по кнопкам Start Counting (Начать отсчет), чтобы начать процесс счета. Завершить работу можно при помощи кнопок Stop Counting (Закончить отсчет). Семафоры Семафоры (semaphores) используют для учета ресурсов. Этот объект нахо­ дится в состоянии «свободно» (signaled), когда счетчик ресурсов (resource count) имеет положительное значение, и в состоянии «занято» (nonsignaled), когда счет­ чик ресурсов равен нулю. Положительное значение счетчика ресурсов всегда ука­ зывает на то, что ресурсы свободны. Например, после того как поток вызывает функцию WaitForSingleObject и передает ей дескриптор семафора, система проверяет счетчик ресурсов данного семафора. Если значение счетчика положительно (семафор в состоянии «свобод­ но»), функция возвращает значение, показывающее, что ресурсы доступны для этого процесса (он может продолжить свою работу и запросить соответствующие ресурсы). Если счетчик ресурсов равен нулю (семафор в состоянии «занято»), система переводит данный поток в состояние ожидания, пока значение счетчика ресурсов не станет положительным, что происходит, как правило, при освобож­ дении ресурса другим процессом. Но поток, освобождая ресурс, должен также освободить и соответствующий семафор, тогда система инкрементирует счетчик ресурсов этого семафора. Таким образом, семафоры могут быть полезны при совместном использовании ограниченных ресурсов. Предположим, имеется три приложения, каждое из кото­
21 рых должно выполнить вывод на печать, а у компьютера только два параллельных порта. Установив семафор с начальным значением счетчика ресурсов, равным двум, можно заставить приложения запрашивать сервис печати только тогда, когда есть свободный параллельный порт. Проблемы, связанные с состоянием ожидания Следует напомнить, что иногда возникают проблемы, связанные с состояни­ ем ожидания. Это объясняется тем, что некоторые потоки должны быть готовы в определенные моменты обрабатывать сообщения от других приложений. О со­ общениях будет рассказываться в главе 16, а пока достаточно знать, что сущест­ вуют широковещательные сообщения, предназначенные каждому окну системы. В случае их получения поток, который обрабатывает предназначенные конкрет­ ному окну сообщения и ожидает освобождения таких объектов, как мьютекс или событие, не сможет обработать входящие сообщения. Это приведет к тупиковой ситуации (deadlock), так как приложение, которому предназначены сообщения, не может их обработать, а приложение, которое посылает сообщения, не может продолжать выполнение до тех пор, пока не получит ответ. Решение данной проблемы состоит в том, что не нужно переводить потоки, отвечающие за обработку сообщений, в состояние ожидания путем вызова фун­ кции WaitForSingleObject. К счастью, это не имеет отношения к VB, пос­ кольку программисты VB не создают программ для потоков, обрабатывающих сообщения. Проблемы, связанные с состоянием ожидания
Глава 13. Архитектура памяти Windows В этой главе рассказывается о том, как Windows NT и Windows 9x используют память. Вы получите некоторую предварительную информацию о виртуальной памяти. Как правило, у программистов на Visual Basic нет необходимости непос­ редственно управлять памятью, но существует один важный повод этим заниматься, связанный с доступом к внешним процессам. В любом случае тема управления па­ мятью очень интересна, поэтому ее ключевые вопросы освещаются в этой книге. Типы памяти Следует начать с определения основных терминов. На рис. 13.1 представлены некоторые из понятий, касающихся данной темы. Виртуальная страница 1 Виртуальная страница 2 Виртуальная страница 3 Виртуальная страница 4 Виртуальное адресное пространство процесса Физическая память Диск Файл подкачки EXE или DLL (загрузочный модуль) Файл данных Физическая страница A Физическая страница B 4Kб 4Kб 4Kб 4Kб Рис. 13.1. Типы памяти
221 Физическая память Физическая память (physical memory) – это реальные микросхемы RAM, уста­ новленные в компьютере. Каждый байт физической памяти имеет физический адрес (physical address), который представляет собой число от нуля до числа на единицу меньшего, чем количество байтов физической памяти. Например, ПК с установлен­ ными 64 Мб RAM, имеет физические адреса &H00000000–&H04000000 в шестнад­ цатеричной системе счисления, что в десятичной системе будет 0–67 108 863. Физическая память (в отличие от файла подкачки и виртуальной памяти) является исполняемой (executable), то есть памятью, из которой можно читать и в которую центральный процессор может посредством системы команд записывать данные. В качестве примера возьмем следующую команду на языке ассемблера: mov [si], ax Для того чтобы она могла быть реально выполнена, адрес в регистре si должен быть физическим адресом. Виртуальная память Виртуальная память (virtual memory) – это просто набор чисел, о которых го­ ворят как о виртуальных адресах. Программист может использовать виртуальные адреса, но Windows не способна по этим адресам непосредственно обращаться к данным, поскольку такой адрес не является адресом реального физического запо­ минающего устройства, как в случае физических адресов и адресов файла подкач­ ки. Для того чтобы код с виртуальными адресами можно было выполнить, такие адреса должны быть отображены на физические адреса, по которым действительно могут храниться коды и данные. Эту операцию выполняет диспетчер виртуальной памяти (Virtual Memory Manager – VMM). Его работа будет рассматриваться несколько позже. Операционная система Windows обозначает некоторые области виртуальной памяти как области, к которым можно обратиться из программ поль­ зовательского режима. Все остальные области указываются как зарезервирован­ ные. Какие области памяти доступны, а какие зарезервированы, зависит от версии операционной системы (Windows 9x или Windows NT). Страничные блоки памяти Как известно, наименьший адресуемый блок памяти – байт. Однако самым маленьким блоком памяти, которым оперирует Windows VMM, является страни- ца (page) памяти, называемая также страничным блоком (page frame) памяти. На компьютерах с процессорами Intel объем страничного блока равен 4 Кб. В книге использован как термин виртуальные страницы (virtual pages), так и термин фи- зические страницы (physical pages). Память файла подкачки Страничный файл (pagefile), который называется также файлом подкачки (swap file), в Windows находится на жестком диске. Он используется для хранения данных и программ точно так же, как и физическая память, но его объем обычно превышает Типы памяти
222 Архитектура памяти Windows объем физической памяти. Windows использует файл подкачки (или файлы, их может быть несколько) для хранения информации, которая не помещается в RAM, производя, если нужно, обмен страниц между файлом подкачки и RAM. Таким образом, диапазон виртуальных адресов скорее согласуется с адресами в файле подкачки, чем с адресами физической памяти (см. рис. 13.1). Когда та­ кое согласование достигается, говорят, что виртуальные адреса спроецированы (backed) на файл подкачки, или являются проецируемыми на файл подкачки (pagefile­backed). Набор виртуальных адресов может проецироваться на физическую память, файл подкачки или любой файл. Файлы, отображаемые в память В главе 10 кратко обсуждались файлы, отображаемые в память, там же был приведен пример отображения файла. Любой файл применяется для проецирова­ ния виртуальной памяти так же, как для этих целей используется файл подкачки. Фактически единственное назначение файла подкачки – проецирование (backing) виртуальной памяти. Поэтому файлы, проецируемые в память подобным образом, называются отоб­ ражаемыми в память (memory­mapped). На рис. 13.1 изображены именно такие файлы. Соответствующие виртуальные страницы являются спроецированными на файл (file­backed). Следует напомнить, что функция CreateFileMapping объявляется так: Private Declare Function CreateFileMapping Lib "kernel32" _ Alias "CreateFileMappingA" ( _ ByVal hFile As Long, _ ByVal lpSecurityAttributes As Long, _ ByVal flProtect As Long, _ ByVal dwMaximumSizeHigh As Long, _ ByVal dwMaximumSizeLow As Long, _ ByVal lpName As String _ ) As Long Она создает объект «отображение файла» (file­mapping object), используя де­ скриптор открытого файла, и возвращает дескриптор этого объекта. Дескриптор может использоваться с функцией MapViewOfFile, отображающей файл в вир­ туальную память: Private Declare Function MapViewOfFile Lib "kernel32" ( _ ByVal hFileMappingObject As Long, _ ByVal dwDesiredAccess As Long, _ ByVal dwFileOffsetHigh As Long, _ ByVal dwFileOffsetLow As Long, _ ByVal dwNumberOfBytesToMap As Long _ ) As Long Начальный адрес объекта «отображение файла» в виртуальной памяти воз­ вращает функция MapViewOfFile. Можно также сказать, что представление проецируется на файл с дескриптором hFile.
223 Если параметр hFile, передаваемый функции CreateFileMapping, уста­ новлен в –1, то объект «отображение файла» (и любые представления, созданные на основе этого объекта) проецируется на файл подкачки, а не на заданный файл. Совместно используемая физическая память О физической памяти говорят, что она совместно используется (shared), если она отображается на виртуальное адресное пространство нескольких процессов, хотя виртуальные адреса в каждом процессе могут отличаться. Рис. 13.2 иллюст­ рирует это утверждение. Виртуальное адресное пространство процесса 1 Физическая память Совместно используемая физическая память Виртуальное адресное пространство процесса 2 Рис. 13.2 . Совместно использу емая физическая память Если файл, такой как DLL, находится в совместно используемой физической памяти, то о нем можно говорить как о совместно используемом. Одно из преимуществ файлов, отображаемых в память, заключается в том, что их легко использовать совместно. Вы уже знаете из главы 10, что присвоение имени объекту «отображение файла» делает возможным совместное использование файла несколькими процессами. В этом случае его содержимое отображено на совместно используемую физическую память (см. рис. 13.3). Возможно также совместное использование содержимого файла подкачки с помощью механизма отображения файла. В частности, можно создать такой объект, проецируемый на файл подкачки, просто установив параметр hFile функции CreateFileMapping в –1 . Типы памяти
224 Архитектура памяти Windows Адресное пространство процесса Каждый процесс Win32 получает виртуальное адресное пространство (virtual address space), называемое также адресным пространством, или пространством процесса (process space), объем которого равен 4 Гб. Таким образом, код процесса может ссылаться на адреса с &H00000000 по &HFFFFFFFF (или с 0 по 232 – 1 = = 4294967295 в десятичной системе счисления). Конечно, так как виртуальные адреса – это просто числа, заявление о том, что каждый процесс получает свое собственное виртуальное адресное пространство, выглядит довольно бессмыслен­ ным. (Это все равно, что сказать, что каждый человек получает свой собственный диапазон возраста от 0 до 150). Тем не менее это утверждение должно означать, что Windows не видит никакой взаимосвязи в том, что и процесс A, и процесс B используют один и тот же вирту­ альный адрес, например &H40000000. В частности, Windows может сопоставить (или не сопоставить) виртуальным адресам каждого процесса разные физические адреса. Виртуальное адресное пространство процесса 1 Виртуальное адресное пространство процесса 2 Физическая память Диск Совместно используемая физическая память Файл подкачки, загрузочный модуль или файл данных Рис. 13.3. Совместно используемое отображение файла
225 Использование адресного пространства в Windows NT На рис. 13.4 показана общая схема использования адресного пространства процесса в Windows NT. Область A Как видно из рис. 13.4, Windows NT резервирует первые 64 Кб виртуальной памяти для специального назначения и помечает эту область как недоступную для Адресное пространство процесса Рис. 13.4 . Использование адресного пространства процесса в Windows NT D Зарезервировано Windows NT для исполнительной системы Windows, ядра и драйверов устройств. Недоступно в пользовательском режиме. (2 Гб) C Используется для некорректно инициализированных указателей. Недоступно в пользовательском режиме. (64 Кб) B Адресное пространство процессов содержит прикладные модули EXE и DLL, Win32 DLL (kernel32.dll, user.dll и т.д .), файлы, отображаемые в память. Доступно в пользовательском режиме. (2Гб128Кб) A Используется для неинициализированных указателей (null pointer). Недоступно в пользовательском режиме. (64 Кб)
22 Архитектура памяти Windows программ пользовательского режима. При работе с указателями, а работать с ними программистам VC++ приходится довольно часто, легко забыть проинициализи­ ровать один из них. Например, рассмотрим следующий код: int *pi; // Объявляем указатель на тип integer. *pi = 5; // Устанавливаем указатель на целочисленное значение. Этот код не будет работать. Проблема в том, что указатель должен ссылаться на переменную (содержать ее адрес), а не хранить ее значение. В первой строке кода объявляется указатель, который изначально ничем не инициализирован, то есть является указателем типа NULL (NULL pointer). Во второй строке выполня­ ется операция записи числа 5 по нулевому адресу памяти. Чтобы предотвратить доступ к защищенной области, данная нижняя часть памяти резервируется Windows (и Windows NT, и Windows 9x). В результате код, подобный представленному в предыдущем примере, приведет к общей ошибке защиты (GPF). Таким образом программист информируется об ошибочной ини­ циализации указателя. Область B Область B, это видно из рис. 13.4, начинается на верхней границе 64 Кб рассмотренной выше области A и распространяется до отметки, лежащей на 64 Кб ниже, чем отметка 2 Гб. Таким образом, размер области B равен 2 Гб – 128 Кб (почти половина всего адресного пространства). Отдельные ее части отображают основной загрузочный модуль приложения, любые относящиеся к приложению DLL, в их числе системные DLL, такие как KERNEL32.DLL, USER32. DLL, GDI32.DLL и т.д . На рис. 13.5 в качестве примера показано адресное пространство приложения Designer (компании Micrografx Inc.) . Обратите внимание на следующее:  основной загрузочный модуль размещен по адресу &H400000, который яв­ ляется базовым адресом, устанавливаемым по умолчанию, для программ, написанных на VC++. (Мне неизвестно, какая среда разработки использо­ валась для создания приложения Designer.);  относящаяся к приложению библиотека DS70RES.DLL размещена по адре­ су &H01350000, который располагается несколько выше над загрузочным модулем;  следующий модуль (rpcltc.dll), расположенный по адресу &H015B0000, является DLL компании Microsoft;  далее помещаются несколько DLL от компаний Micrografx и Microsoft и даже DLL от компании Creative Labs;  другие DLL, начиная с COMCTR32.DLL, принадлежат Microsoft, включая USER32.DLL по адресу &H77E70000, GDI32.DLL по адресу &H77ED0000 и KERNEL32.DLL по адресу &H77F00000. Стоит отметить особо, что эта область может включать как автономные (un­ shared), так и совместно используемые файлы. Например, DLL, относящиеся к приложению Designer, похоже, используются независимо (пока не выполняются другие экземпляры этого приложения). Системные DLL, такие как KERNEL32.DLL,
22 используются совместно, так как в физической памяти присутствует только одна копия каждой из них. Как правило, эта копия отображается на одни и те же вир­ туальные адреса каждого процесса, но это необязательно. Область C Область C, как следует из рис. 13.4, – это область объемом 64 Кб, сразу под отметкой 2 Гб, где начинается зарезервированное Windows адресное пространство. Она используется как своего рода разграничительная полоса, препятствующая обращениям к адресам памяти, которые разделяют открытую для доступа область B и недоступную область D, принадлежащую Windows. Область D Эта область зарезервирована для использования Windows NT (например, ис­ полнительной системой, ядром и драйверами устройств). Приложения не могут обращаться к памяти в этом диапазоне виртуальных адресов. Любая попытка сделать это приведет к ошибке нарушения доступа (access violation, GPF). Рис. 13.5 . Адресное пространство приложения Designer Адресное пространство процесса
22 Архитектура памяти Windows Использование адресного пространства в Windows 9x На рис. 13.6 показана общая схема использования адресного пространства процесса в Windows 9x. Область A Как следует из рис. 13.6, Windows 9x резервирует область A, объем которой всего лишь 4 Кб, для того же, что и Windows NT первые 64 Кб памяти, – с целью Рис. 13.6 . Использование адресного пространства процесса в Windows 9x E D C Здесь размещаются драйвера виртуальных устройств, диспетчер памяти, файловая система, исполняемые файлы Windows. Доступно в пользовательском режиме. (1 Гб) Используется для DOS и 16разрядных приложений Windows. Доступно в пользовательском режиме. (4Мб4Кб) B A Используется для неинициализированных указателей (null pointer). Недоступно в пользовательском режиме. (4 Кб) Адресное пространство процессов. Доступно в пользовательском режиме. (2Гб4Мб) Предназначено для Windows. Для отображаемых в память файлов, совместно используемых Win32 DLL, 16разрядных приложений Windows, для выделения памяти. Доступно в пользовательском режиме. (1 Гб)
22 предупреждения о нулевых указателях. Эта область защищена и попытка обраще­ ния к ней из программы пользовательского режима приводит к ошибке нарушения доступа. Область B Данная область памяти используется для поддержания совместимости с при­ ложениями DOS и 16­разрядными приложениями Windows. Несмотря на потен­ циальную доступность она не должна использоваться для программирования. Область C Область C – это адресное пространство, используемое прикладными програм­ мами и их DLL. Здесь размещаются также и модули Windows. Например, если приложению требуется управляющий элемент OCX, его модуль будет находиться в этой области. Область D Windows 9x отображает системные DLL Win32 (KERNEL32.DLL, USER32.DLL и т.д .) в это адресное пространство. Данные файлы используются совместно, то есть несколько процессов могут обращаться к единственной копии такого файла в физической памяти. Область D доступна для программ пользовательского режима (однако разме­ щать их здесь не рекомендуется). Область E Данная область также содержит совместно используемые файлы Windows, такие как исполнительная система Windows и ядро, драйверы виртуальных уст­ ройств, файловая система, программы управления памятью и пр. Она также доступна для программ пользовательского режима. Пример использования функции GetSystemInfo API­функция GetSystemInfo извлекает некоторую информацию об исполь­ зовании адресных пространств. Синтаксис ее таков: VOID GetSystemInfo( LPSYSTEM_INFO lpSystemInfo // Адрес структуры с системной // информацией. ); или в VB: Declare Sub GetSystemInfo Lib "kernel32" Alias "GetSystemInfo" ( _ lpSystemInfo As SYSTEM_INFO) Структура с системной информацией определяется таким образом: struct SYSTEM_INFO { union { DWORD dwOemId; struct { WORD wProcessorArchitecture; Пример использования GetSystemInfo
230 Архитектура памяти Windows WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD dwActiveProcessorMask; DWORD dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; } Объединение (union) – конструкция языка VC++, которая означает, что конк­ ретная переменная может в разные моменты времени хранить данные различного типа. Например, переменная, которая объявлена как следующее объединение: union uExample { int i; char ch; } может быть или целым числом, или символом. Конечно, система должна зарезерви­ ровать достаточно места для самого «объемного» из ее типов, в данном случае четы­ ре байта для целого типа VC++. Естественно, вы должны тоже учитывать это. В структуре SYSTEM_INFO содержатся переменная типа DWORD и еще одна струк­ тура, состоящая из двух переменных типа WORD. Поэтому размер этого объединения равен размеру типа DWORD (или двух WORD), что составляет четыре байта. Указанную структуру можно определить в VB так: Type SYSTEM_INFO wProcessorArchitecture As Long dwPageSize As Long lpMinimumApplicationAddress As Long lpMaximumApplicationAddress As Long dwActiveProcessorMask As Long dwNumberOfProcessors As Long dwProcessorType As Long dwAllocationGranularity As Long wProcessorLevel As Integer wProcessorRevision As Integer End Type Ниже представлена программа, вызывающая функцию GetSystemInfo, и результат ее выполнения на моем компьютере: Public Sub GetSystemInfoExample() Dim si As SYSTEM_INFO GetSystemInfo si Debug.Print "Page size: " & si.dwPageSize
231 Debug.Print "Max app address: " & Hex(si.lpMaximumApplicationAddress) Debug.Print "Min app address: " & Hex(si.lpMinimumApplicationAddress) Debug.Print "Application Granularity: " & si.dwAllocationGranularity Debug.Print "Processor architecture: " & si.wProcessorArchitecture Debug.Print "Processor count: " & si.dwNumberOfProcessors Debug.Print "Processor type: " & si.dwProcessorType Debug.Print "Processor level: " & si.wProcessorLevel Debug.Print "Processor revision: " & Hex(si.wProcessorRevision) End Sub Page size: 4096 Max app address: 7FFEFFFF Min app address: 10000 Application Granularity: 65536 Processor architecture: 0 Processor count: 1 Processor type: 586 Processor level: 6 Processor revision: 501 В итоге, как и ожидалось, размер страницы памяти получился равным 4 Кб. Кроме того, в Windows NT (в которой я работаю) нижняя граница адресного пространства приложения равна &H1000 (то есть 64 Кб), а верхняя граница – &H7FFEFFFF, как и следовало предполагать в соответствии с рис. 13.4 . Гранулярность1 приложения (application granularity) будет рассматриваться несколько позже. В соответствии с документацией, переменная wProcessorArchitecture равна нулю, или, в терминах символьных констант, PROCESSOR_ARCHITECTURE_INTEL. (Параметр dwOemID устарел.) Уровень (level) процессора 6 указывает на процес­ сор Pentium II. Редакция (revision) процессора номер 501 расшифровывается как модель (мodel) 5 шаг (stepping) 1. Для получения более подробной информации обращайтесь к документации по GetSystemInfo. Распределение виртуальной памяти Каждая страница виртуального адресного пространства может находиться в одном из трех состояний:  Reserved (зарезервирована) – страница зарезервирована для использова­ ния;  Committed (передана) – для данной виртуальной страницы выделена фи­ зическая память в файле подкачки или в файле, отображаемом в память;  Free (свободна) – данная страница не зарезервирована и не передана, и поэтому в данный момент она недоступна для процесса. Виртуальная память может быть зарезервирована или передана с помощью вызова API­функции VirtualAlloc: Распределение виртуальной памяти 1 См. далее раздел «Гранулярность при распределении памяти». – Прим. науч. ред.
232 Архитектура памяти Windows LPVOID VirtualAlloc( LPVOID lpAddress, // Адрес резервируемой или выделяемой области. DWORD dwSize, // Объем области. DWORD flAllocationType, // Тип распределения. DWORD flProtect // Тип защиты от доступа. ); Параметр flAllocationType может принимать значения следующих конс­ тант (помимо других):  MEM_RESERVE – параметр, резервирующий область виртуального адресного пространства процесса без выделения физической памяти. Тем не менее память может быть выделена при следующем вызове этой же функции;  MEM_COMMIT – параметр, выделяющий физическую память в оперативной памяти или в файле подкачки на диске для заданного зарезервированного набора страниц. Эти две константы могут объединяться для того, чтобы зарезервировать и выделить память одной операцией. Разделение процедур резервирования и передачи памяти имеет некоторые преимущества. Например, резервирование памяти является очень полезной про­ цедурой с точки зрения практичности. Если приложению требуется большой объем памяти, можно зарезервировать всю память, а выделить только ту часть, которая нужна в данный момент, раздвигая, таким образом, временные рамки более трудоемкой операции выделения физической памяти. Windows тоже использует этот подход, когда выделяет память под стек каждого вновь создаваемого потока. Система резервирует 1 Мб виртуальной памяти под стек каждого потока, но выделяет первоначально только две стра­ ницы (8 Кб). Защита памяти Заметьте, что параметр flProtect функции VirtualAlloc использует­ ся для задания типа защиты от доступа, соответствующего вновь выделенной (committed) виртуальной памяти (это не относится к резервируемой памяти). Существуют следующие методы защиты:  PAGE_READONLY присваивает доступ «только для чтения» выделенной вир­ туальной памяти;  PAGE_READWRITE назначает доступ «чтение–запись» выделенной виртуаль­ ной памяти;  PAGE_WRITECOPY устанавливает доступ «запись копированием» (copy­on ­ write) выделенной виртуальной памяти. Об этом будет рассказано чуть позже;  PAGE_EXECUTE разрешает доступ «выполнение» выделенной виртуальной памяти. Тем не менее любая попытка чтения – записи этой памяти приведет к нарушению доступа;  PAGE_EXECUTE_READ назначает доступ «выполнение» и «чтение»;  PAGE_EXECUTE_READWRITE разрешает доступ «выполнение», «чтение» и «запись»;
233  PAGE_EXECUTE_WRITECOPY присваивает доступ «выполнение», «чтение» и «запись копированием»;  PAGE_NOACCESS запрещает все виды доступа к выделенной виртуальной памяти. Любые из этих значений, за исключением PAGE_NOACCESS, могут комбиниро­ ваться при помощи логического оператора OR со следующими двумя флагами:  PAGE_GUARD определяет помеченные страницы как защищенные (guard page). При любой попытке обращения к защищенной странице система воз­ буждает исключительную ситуацию STATUS_GUARD_PAGE и снимает с дан­ ной страницы статус защищенной. Таким образом, защищенные страницы предупреждают только о первом обращении к ним;  PAGE_NOCACHES запрещает кэширование выделенной памяти. Следует объяснить, что такое доступ «запись копированием». Допустим, не­ которая страница физической памяти совместно используется двумя процессами. Если она помечена как «только для чтения», то два процесса без проблем могут совместно пользоваться этой страницей. Однако возможны ситуации, когда каж­ дому процессу требуется разрешить запись в эту память, но без воздействия на другой процесс. После установки защиты «запись копированием» при попытке записи в совместно используемую страницу система создаст ее копию специаль­ но для процесса, которому нужно осуществить запись. Таким образом, данная страница перестает быть совместно используемой, а представление ее данных в других процессах остается неизменным. Необходимо отметить, что атрибуты защиты страницы могут быть изменены с помощью API­функции VirtualProtect. Гранулярность при распределении памяти Если параметр lpAddress не является кратным 64 Кб, то система округля­ ет указанный адрес в меньшую сторону до ближайшего числа, кратного 64 Кб. Windows всегда выравнивает начальный адрес виртуальной памяти на границу гранулярности распределения (allocation granularity), которая является числом, кратным 64 Кб (при использовании процессоров Intel). Другими словами, началь­ ный адрес любого блока зарезервированной памяти представляет собой число, кратное 64 Кб. Кроме того, объем выделяемой памяти всегда кратен объему системной стра­ ницы, то есть 4 Кб. Поэтому функция VirtualAlloc будет округлять любое запрашиваемое количество байтов в большую сторону до ближайшего числа, крат­ ного объему страницы. Кстати, было время, когда Windows выделяла виртуальную память от имени процесса, но для собственных нужд. В таких случаях операционная система не всегда следовала тем правилам выравнивания на границу гранулярности распре­ деления, которые выполняются при выделении памяти по запросу программиста (хотя ограничения на объем страницы соблюдались). Распределение виртуальной памяти
234 Архитектура памяти Windows Дескриптор виртуальных адресов Система отслеживает, какие из виртуальных страниц являются зарезервиро­ ванными, при помощи структуры, называемой дескриптором виртуальных адресов (Virtual Address Descriptor – VAD). Другого способа определения не существует. Пример использования функции GlobalMemoryStatus API­функция GlobalMemoryStatus, записывающаяся таким образом: Declare Sub GlobalMemoryStatus Lib "kernel32" Alias _ "GlobalMemoryStatus" (lpBuffer As MEMORYSTATUS) выводит множество данных, имеющих отношение к памяти, в составе следующей структуры: struct _MEMORYSTATUS { DWORD dwLength; // Размер структуры MEMORYSTATUS. DWORD dwMemoryLoad; // Процент используемой памяти. DWORD dwTotalPhys; // Количество байтов физической памяти. DWORD dwAvailPhys; // Количество свободных байтов физической памяти. DWORD dwTotalPageFile; // Размер в байтах файла подкачки. DWORD dwAvailPageFile; // Количество свободных байтов файла подкачки. DWORD dwTotalVirtual; // Количество байтов адресного пространства, // доступного пользователю. DWORD dwAvailVirtual; // Количество свободных байтов памяти, // доступных пользователю. End Type В VB код будет таким: Type MEMORYSTATUS dwLength As Long 'Размер структуры MEMORYSTATUS. dwMemoryLoad As Long 'Процент используемой памяти. dwTotalPhys As Long 'Количество байтов физической памяти. dwAvailPhys As Long 'Количество свободных байтов физической памяти. dwTotalPageFile As Long 'Размер в байтах файла подкачки. dwAvailPageFile As Long 'Количество свободных байтов файла подкачки. dwTotalVirtual As Long 'Количество байтов адресного пространства, 'доступного пользователю. dwAvailVirtual As Long 'Количество свободных байтов памяти, 'доступных пользователю. End Type Например, представленная ниже программа: Dim ms As MEMORYSTATUS GlobalMemoryStatus ms Debug.Print "Virtual memory: " & ms.dwTotalVirtual Debug.Print "Short of 2 GB: " & (2 ^ 31  ms.dwTotalVirtual) / 1024 & _ " KB" Debug.Print "Available Virtual: " & ms.dwAvailVirtual может вывести следующие результаты:
235 Virtual memory: 2147352576 Short of 2 GB: 128 KB Available Virtual: 1965658112 В соответствии с рис. 13.4, доступный объем виртуальной памяти на 128 Кб меньше, чем объем нижней половины общего адресного пространства процесса, равный 2 Гб. Управление виртуальной памятью Давайте рассмотрим, как диспетчер виртуальной памяти Windows преобразует адреса виртуальной памяти в физические. Преобразование виртуальных адресов в физические: попадание На рис. 13.7 показан процесс преобразования при отображении виртуальных ад­ ресов в физические. Он называется попаданием в (физическую) страницу (page hit). Рис. 13.7. Преобразование виртуальных адресов в физические в случае попадания Все виртуальные адреса делятся на три части. Самая левая часть (биты 22–31) содержит индекс каталога страниц процесса. Windows поддерживает отдельный каталог страниц для каждого процесса. Его адрес хранится в одном из регистров центрального процессора, который называется CR3. (В операцию переключения задач входит переведение CR3 в состояние, когда он указывает на каталог страниц процесса, на который осуществляется переключение.) Каталог страниц содержит 1024 четырехбайтовых элемента. Windows также поддерживает для каждого процесса совокупность таблиц страниц (page table). Каждый элемент каталога страниц содержит уникальный номер таблицы. Поэтому Windows поддерживает до 1024 таблиц страниц. (В дейс­ твительности таблицы страниц создаются только при попытке обращения к дан­ Управление виртуальной памятью
23 Архитектура памяти Windows ным или коду по конкретному виртуальному адресу, а не тогда, когда выделяется виртуальная память.) Следующая часть виртуального адреса (биты 12–21) используется в качестве индекса в таблице страниц, соответствующей выбранному элементу каталога стра­ ниц. Каждый элемент таблицы, соответствующий указанному индексу, содержит в 20 старших разрядах номер страничного блока, который задает конкретный страничный блок физической памяти. Третья, и последняя, часть виртуального адреса (биты 0–11) представляет со­ бой смещение в данном страничном блоке. Сочетание номера страничного блока и смещения дают в совокупности адрес физической памяти. Так как каждая таблица страниц состоит из 1024 элементов и количество таб­ лиц равно 1024, общее количество страничных блоков, которое можно определить таким образом, будет 1024 × 1024 = 210 × 210 = 220. Так как каждый страничный блок имеет объем 4 Кб = 4 × 210 байт, то теоретический предел физического адресного пространства будет 4 × 230 = 4 Гб. У этой довольно сложной схемы преобразования есть несколько важных пре­ имуществ. Одно из них – очень небольшой объем страничных блоков, которые легко могут быть размещены в памяти. Гораздо легче найти непрерывный блок памяти размером 4 Кб, чем, скажем, 64 Кб. Но основное преимущество заключается в том, что адреса виртуальной памя­ ти двух процессов могут быть сознательно преобразованы в разные или в одни и те же физические адреса. Предположим, что Process1 и Process2 обращаются в программе к одному и тому же виртуальному адресу. При преобразовании виртуальных адресов в фи­ зические для каждого из процессов используются их собственные каталоги стра­ ниц. Поэтому, хотя индексы в каталогах страниц одинаковы и в том, и в другом случаях, они все же представляют собой индексы из разных каталогов. Таким способом VMM может гарантировать, что виртуальные адреса каждого процесса будут преобразованы в разные физические адреса. С другой стороны, VMM может также дать гарантию, что виртуальные адреса двух процессов, независимо от того являются ли они одинаковыми или нет, будут преобразованы в один и тот же физический адрес. Один из способов добиться этого – установить соответствующий элемент в обоих каталогах страниц на одну и ту же таблицу страниц и, следовательно, на один и тот же страничный блок. Таким образом, процессы могут совместно использовать физическую память. Каталог и таблицы системных страниц Нужно также упомянуть, что Windows поддерживает каталог системных стра­ ниц (system page directory) для работы с виртуальной памятью, зарезервированной Windows, так же, как и соответствующую совокупность таблиц системных страниц. Формат действительного элемента таблицы страниц Об элементе указанной таблицы говорят, что он является действительным (valid), если ссылается на физический страничный блок, как на рис. 13.7. На рис. 13.8 изображен формат действительного элемента таблицы страниц (некоторые данные опущены).
23 Обратите внимание, что элемент таблицы страниц содержит флаги, описыва­ ющие различные характеристики данного страничного блока, например, осущест­ влялись ли его запись и чтение, разрешен ли доступ к этой памяти из программ пользовательского режима. Самый младший разряд является битом достовернос- ти (valid bit), называемый также битом присутствия (present bit), и указывает на то, что этот элемент действителен. Преобразование виртуальных адресов в физические: промах Если виртуальная страница спроецирована не на физическую память, а на файл подкачки, то процесс преобразования завершится ошибкой из­за отсутствия страницы в памяти (page fault). Эта ситуация изображена на рис. 13.9 . Номер блока страницы в физической памяти 1 Бит достоверности (страница отображается в физическую память) Чтение/запись или только запись Разрешение доступа из пользовательского режима Запись в обход запрета Запрет кэширования Бит доступа (страница читалась) Бит записи (страница была кудато записана) Рис. 13.8 . Формат действительного элемента таблицы страниц Рис. 13.9 . Преобразование виртуальных адресов в физические в случае промаха Управление виртуальной памятью
23 Архитектура памяти Windows В данном случае система определяет, что некоторый элемент таблицы страниц не является действительным (invalid), так как бит достоверности этого элемента равен нулю. Формат недействительного элемента, страница которого спроециро­ вана на файл подкачки, показан на рис. 13.10. Рис. 13.10. Формат элемента таблицы страниц для страницы, спроецированной на файл подкачки Для системы равенство бита достоверности нулю означает, что в этом эле­ менте таблицы страниц содержится номер файла подкачки (в диапазоне от 0 до 15), на который спроецирован данный виртуальный адрес. Кроме того, этот же элемент содержит номер страницы внутри файла подкачки, на которую спроеци­ рован данный виртуальный адрес. Система может затем переместить страницу файла подкачки в физическую память, изменив при этом значение бита досто­ верности элемента с нуля на единицу и использовав, как и раньше, 12 разрядов виртуального адреса в качестве смещения в физическом страничном блоке. Та­ кая процедура называется подкачкой страниц (paging). Совместно используемые страницы До сих пор наши рассуждения касались только той памяти, которая не используется совместно. Ситуация с совместно используемой физической па­ мятью является значительно более сложной, не будем углубляться в детали, а отметим только, что VMM использует концепцию, называемую прототипиро- ванием элементов таблицы страниц. Идея заключается в том, что обычные эле­ менты таблицы каждого из совместно использующих память процессов указы­ вают не на физическую память, а на общий прототип элемента таблицы страниц. А тот, в свою очередь, может ссылаться на совместно используемую физическую память. Рабочие наборы Уже говорилось о том, что каждая страница виртуального адресного про­ странства процесса объемом 4 Гб существует в одном из трех состояний – сво­ бодном (free), зарезервированном (reserved) или переданном (committed). Теперь можно также сказать, что каждая переданная страница (committed page) является или действительной, или недействительной. Совокупность действительных стра­ ниц, то есть спроецированных на физическую память, называют рабочим набором (working set) процесса. Рабочий набор, конечно, постоянно меняется по мере того, как страницы подкачиваются в память или выполняется обратное действие. На­ помним, что утилита rpiEnumProcsNT показывает также текущий размер рабочего набора процесса. 0 Номер страницы в файле подкачки Номер файла подкачки
23 Системный рабочий набор (system working set) характеризует виртуальные страницы системной памяти, которые в данный момент отображены на физичес­ кую память. Размер рабочего набора процесса ограничен теми установками, которые оп­ ределяет Windows в зависимости от объема физической памяти. Эти значения приведены в табл. 13.1 . Таблица 13.1. Пределы размера рабочего набора процесса Модель Объем Минимальный размер Максимальный размер памяти памяти рабочего набора процесса рабочего набора процесса Small <= 19 Мб 20 страниц (80 Кб) 45 страниц (180 Кб) Medium 20–32 Мб 30 страниц (120 Кб) 145 страниц (580 Кб) Large >= 32 Мб 50 страниц (200 Кб) 345 страниц (1380 Кб) Заметьте, что эти пределы могут быть изменены с помощью API­функции SetProcessWorkingSetSize: BOOL SetProcessWorkingSetSize( HANDLE hProcess, // Открытый дескриптор интересующего процесса. DWORD dwMinimumWorkingSetSize, // Задает мин. размер рабочего набора в байтах. DWORD dwMaximumWorkingSetSizе // Задает макс. размер рабочего набора в байтах. ); Между прочим, присвоение каждому из двух параметров размера значения –1 приведет к тому, что функция сожмет размер рабочего набора до 0 и тем самым временно удалит данный процесс из физической памяти. Отметим также, что действительный размер рабочего набора процесса может изменяться во времени, так как Windows увеличивает рабочий набор, если заме­ чает, что у процесса большое количество страничных промахов. Пределы размера системного рабочего набора приведены в табл. 13.2 . Таблица 13.2 . Пределы размера системного рабочего набора Модель Объем Минимальный размер Максимальный размер памяти памяти рабочего набора процесса рабочего набора процесса Small <= 19 Мб 388 страниц (1,5 Мб) 500 страниц (2,0 Мб) Medium 20–32 Мб 688 страниц (2,7 Мб) 1150 страниц (4,5 Мб) Large >= 32 Мб 1188 страниц (4,6 Мб) 2050 страниц (8 Мб) База данных страничных блоков Windows фиксирует состояние каждой физической страницы памяти в струк­ туре данных, называемой базой данных страничных блоков (Page Frame Database). Каждая физическая страница может находиться в одном из восьми различных состояний: Управление виртуальной памятью
240 Архитектура памяти Windows  активная, или действительная (active, valid). Страница в текущий момент, отображается на виртуальную память, входя, таким образом, в рабочий на­ бор страниц;  переходная (transition). Страница в процессе перехода к активному состоянию;  резервная (standby). Страница только что вышла из состояния «активная», но осталась неизменной;  измененная (modified). Страница вышла из состояния «активная». Ее содер­ жание, пока она находилась в указанном состоянии, было изменено, но еще не записано на диск;  измененная незаписанная (modified no write). Страница находится в со­ стоянии «измененная», но особо помечена как страница, содержимое ко­ торой не сброшено на диск. Используется драйверами файловой системы Windows;  свободная (free). Страница свободна, но содержит произвольные записи и, следовательно, не может использоваться процессом;  обнуленная (zeroed). Страница свободна и инициализирована нулями пото­ ком нулевой страницы. Может быть выделена процессу;  плохая (bad). В странице были отмечены ошибки четности или какие­то другие аппаратные ошибки, поэтому она не должна использоваться. Кучи памяти Если говорить простым языком, куча (heap) – это область виртуальной памя­ ти, которая зарезервирована Windows. В Win32 API есть функции для выделения и освобождения памяти из кучи, изменения размера выделенной памяти. Данные функции предоставляют более наглядный способ управления памятью приложе­ ния, чем функция VirtualAlloc, и особенно подходят для управления огромным количеством небольших блоков памяти. Хотя программисты VB не используют кучи непосредственно, сама концепция достаточно часто упоминается в документации. Давайте кратко ее обсудим. Кучи в 32-разрядной Windows При создании процесса Windows назначает ему кучу по умолчанию (default heap), то есть изначально резервирует область виртуальной памяти объемом 1 Мб. Тем не менее при необходимости система будет регулировать размер кучи, которая используется самой Windows для различных целей. Например, как вам известно из главы 6, Visual Basic преобразует строки Unicode в строки ANSI для передачи в точку входа ANSI некоторой DLL. Память для этих временных строк ANSI бе­ рется из кучи по умолчанию. API­функция GetProcessHeap используется для получения дескриптора кучи. При помощи функции HeapCreate, возвращающей дескриптор кучи, программист может создавать дополнительные кучи. Есть несколько причин создавать дополнительные кучи вместо того, чтобы использовать кучу по умолчанию. Например, те кучи, которые предназначены для конкретных задач, часто оказываются более эффективными. Кроме того, ошибки записи данных в кучу, память для которой выделена из специализированной кучи,
241 не затронут данных других куч. Наконец, выделение памяти из специализирован­ ной кучи в общем случае будет означать, что данные в памяти упакованы более плотно друг к другу, а это может уменьшить потребность в загрузке страниц из фай­ ла подкачки. Следует также упомянуть, что доступ к куче упорядочен (serialized), то есть система заставляет каждый поток, пытающийся обратиться к памяти кучи, дожидаться своей очереди, пока другие потоки не закончат производимые опера­ ции. Следовательно, только один поток в каждый момент времени может выделять или освобождать память кучи во избежание неприятных конфликтов. Между прочим, 16­разрядная Windows поддерживает и глобальную, и локаль­ ную кучи. Соответственно в данной системе реализованы функции GlobalAlloc и LocalAlloc. Они выполняются, но не очень эффективны, поэтому следует избегать их применения в Win32. Однако, как вы узнаете позже, их все­таки при­ ходится использовать для некоторых целей, таких как создание окна просмотра буфера обмена. Функции работы с кучей Для работы с кучами используются следующие функции:  GetProcessHeap возвращает дескриптор кучи процесса по умолчанию;  GetProcessHeaps возвращает список дескрипторов всех куч, используе­ мых в данный момент процессом;  HeapAlloc выделяет блок памяти из заданной кучи;  HeapCompact дефрагментирует кучу, объединяя свободные блоки. Может также освобождать неиспользуемые страницы памяти кучи;  HeapCreate создает новую кучу в адресном пространстве процесса;  HeapDestroy удаляет заданную кучу;  HeapFree освобождает предварительно выделенные блоки памяти кучи;  HeapLock блокирует кучу, при использовании данной функции только один поток имеет к ней доступ. Другие потоки, запрашивающие доступ, пе­ реводятся в состояние ожидания до тех пор, пока поток, владеющий кучей, не разблокирует ее. Это одна из форм синхронизации потоков, то есть тот прием, которым система реализует упорядоченность доступа;  HeapReAlloc перераспределяет блоки памяти кучи. Используется для из­ менения размера блока;  HeapSize возвращает размер выделенного блока памяти кучи;  HeapUnlock разблокирует кучу, которая до этого была заблокирована фун­ кцией HeapLock;  HeapValidate проверяет пригодность кучи (или отдельного ее блока), если имеются ли какие­либо повреждения;  HeapWalk позволяет программисту просматривать содержимое кучи. Обыч­ но используется при отладке. Пример отображения виртуальной памяти Функция Win32 API VirtualQuery может использоваться для получения информации о состоянии адресов виртуальной памяти. Синтаксис ее таков: Пример отображения виртуальной памяти
242 Архитектура памяти Windows DWORD VirtualQuery( LPCVOID lpAddress, // Адрес области. PMEMORY_BASIC_INFORMATION lpBuffer, // Адрес информационного // буфера. DWORD dwLength // Размер буфера. ); В VB можно использовать следующую декларацию: Declare Function VirtualQuery Lib "kernel32" ( _ ByVal lpAddress As Long, _ lpBuffer As MEMORY_BASIC_INFORMATION, _ ByVal dwLength As Long _ ) As Long Используется также функция VirtualQueryEx, расширенная версия VirtualQuery, которая позволяет получать информацию о внешних виртуаль­ ных адресных пространствах: DWORD VirtualQueryEx( HANDLE hProcess, // Дескриптор процесса. LPCVOID lpAddress, // Адрес области. PMEMORY_BASIC_INFORMATION lpBuffer, // Адрес информационного // буфера. DWORD dwLength // Размер буфера. ); или в VB: Declare Function VirtualQueryEx Lib "kernel32" ( _ ByVal hProcess As Long, _ ByVal lpAddress As Long, _ lpBuffer As MEMORY_BASIC_INFORMATION, _ ByVal dwLength As Long _ ) As Long Как и следовало ожидать, параметру hProcess требуется дескриптор про­ цесса. Параметр lpAddress – это начальный адрес для записи результирующих данных, который будет округляться в меньшую сторону до ближайшего кратного размеру страницы (4 Кб). Обе функции возвращают информацию в следующей структуре: struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; // Базовый адрес области. PVOID AllocationBase; // Базовый адрес выделенной области. DWORD AllocationProtect; // Первоначальная защита от доступа. DWORD RegionSize; // Размер области в байтах. DWORD State; // Передана, зарезервирована, свободна. DWORD Protect; // Текущая защита от доступа. DWORD Type; // Тип страниц. }
243 В VB код будет следующим: Type MEMORY_BASIC_INFORMATION BaseAddress As Long ' Базовый адрес области. AllocationBase As Long ' Базовый адрес выделенной области. AllocationProtect As Long ' Первоначальная защита от доступа. RegionSize As Long ' Размер области в байтах. State As Long ' Передана, зарезервирована, свободна. Protect As Long ' Текущая защита от доступа. Type As Long ' Тип страниц. End Type Чтобы понять принцип действия членов этой структуры, необходимо знать о назначении данной функции. К сожалению, в документации дано довольно пло­ хое объяснение. Чтобы сделать определение более понятным, назовем страницу, которой принадлежит адрес lpAddress, заданной (specified). Рис. 13.11 поможет разобраться в новой терминологии. Область размещения Заданная страница Заданная область (логически упорядоченные страницы) IPAddress Виртуальная память Рис. 13.11. Функция VirtualQueryEx Функция VirtualQueryEx всегда заполняет следующие члены структуры MEMORY_BASIC_INFORMATION:  BaseAddress, которая возвращает базовый адрес заданной страницы;  RegionSize, представляющая собой количество байтов от начала заданной страницы до вершины заданной области. Несмотря на подобное наимено­ вание, это размер не всей заданной области (определение заданной области будет дано позже в этой главе). Если страница, содержащая адрес lpAddress, свободна (не зарезервирована и не передана), член структуры State содержит символьную константу MEM_FREE. Остальные члены (кроме BaseAddress и RegionSize) не имеют значения. Пример отображения виртуальной памяти
244 Архитектура памяти Windows Если страница, содержащая адрес lpAddress, не свободна, функция опреде­ ляет выделенную область (allocation region), то есть область виртуальной памяти, которая включает заданную страницу и была первоначально выделена с помощью вызова функции VirtualAlloc. Начиная с базового адреса заданной страницы, функция последовательно просматривает все страницы выделенной области, проверяя, совпадают ли их типы выделения (allocation type) и защиты (protection type) с аналогичными ти­ пами заданной страницы. Совокупность всех совпадающих упорядоченных стра­ ниц представляет собой заданную область. К ней относятся значения структуры MEMORY_BASIC_INFORMATION. В частности, страница считается совпадающей с заданной страницей, если она удовлетворяет двум следующим условиям:  страница имеет тот же тип выделения, что и первоначальная страница, в со­ ответствии со следующими значениями флага: MEM_COMMIT, MEM_RESERVE, MEM_FREE, MEM_PRIVATE, MEM_MAPPED или MEM_IMAGE;  страница имеет тот же тип защиты, что и первоначальная страница, в соответс­ твии со следующими значениями флага: PAGE_READONLY, PAGE_READWRITE, PAGE_NOACCESS, PAGE_WRITECOPY, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD или PAGE_NOCACHE. Теперь рассмотрим остальные члены структуры MEMORY_BASIC_INFORMATION:  AllocationBase – базовый адрес выделенной области;  AllocationProtect – первоначальный тип защиты выделенной области;  State – одно из трех значений: MEM_FREE, MEM_RESERVE или MEM_COMMIT. Относится к заданной области;  Protect – текущий тип защиты заданной области;  Type – одно из трех значений: MEM_IMAGE, MEM_MAPPED или MEM_PRIVATE. Относится к заданной области. Эти константы имеют следующий смысл: MEM_IMAGE указывает, что область отображена на файл образа задачи (image file), то есть на загрузочный; MEM_MAPPED указывает, что область отображе­ на на не загрузочный отображаемый в память файл (например, файл дан­ ных); MEM_PRIVATE указывает, что область используется одним процессом, а не совместно. Наконец, необходимо заметить, что эти функции возвращают размер структу­ ры MEMORY_BASIC_INFORMATION. Следующая процедура заполняет окно списка картой виртуальной памяти. Вы найдете эту процедуру в приложениях rpiEnumProcsNT и rpiEnumProcs95, содержащихся в архиве. Sub MapMemory(lProcessID As Long) Dim hProcess As Long Dim lret As Long Dim meminfo As MEMORY_BASIC_INFORMATION Dim lCurRegion As Long Dim sName As String Dim sItem As String
245 Dim Tabstops(1 To 5) As Long Dim bIsFree As Boolean lstMain.FontName = "Courier New" lstMain.FontSize = 9 ' Получаем дескриптор процесса по его идентификатору. hProcess = ProcHndFromProcID(lProcessID) If hProcess = 0 Then Stop lstMain.Clear ' Устанавливаем позиции табуляции для окна списка. Tabstops(1) = 4 * 22 Tabstops(2) = Tabstops(1) + 4 * 10 Tabstops(3) = Tabstops(2) + 4 * 10 Tabstops(4) = Tabstops(3) + 4 * 12 SendMessage lstMain.hwnd, LB_SETTABSTOPS, 5, Tabstops(1) Do ' Получаем информацию об этой области. lret = VirtualQueryEx(hProcess, lCurRegion, meminfo, LenB(meminfo)) ' Она свободна (free)? bIsFree = (meminfo.State = MEM_FREE) ' Создаем элемент окна со списком. sItem = Hex(lCurRegion) & "  " & Hex(lCurRegion + _ meminfo.RegionSize  1) _ & vbTab & meminfo.RegionSize / 4096 & " pp" _ & vbTab & MemType(meminfo.State) If Not bIsFree Then sItem = sItem & vbTab & MemType(meminfo.Type) _ & vbTab & AccessType(meminfo.Protect) End If ' Получаем имя модуля. Пропускаем 0, так это имя EXE. If lCurRegion > 0 And Not bIsFree Then sName = String$(MAX_PATH + 1, 0) lret = GetModuleFileName(lCurRegion, sName, MAX_PATH) If lret <> 0 Then sItem = sItem & vbTab & Trim0(sName) End If ' Отображаем. lstMain.AddItem sItem ' Следующее начало области. lCurRegion = lCurRegion + meminfo.RegionSize If lCurRegion >= &H7FFEFFFF Then Exit Do Loop Пример отображения виртуальной памяти
24 Архитектура памяти Windows CloseHandle hProcess End Sub Часть вывода этой программы для одного процесса показана ниже: 0–FFFF16ppFREE 10000 – 10FFF 1pp COMMIT PRIVATE READWRITE 11000 – 1FFFF 15 pp FREE 20000 – 20FFF 1pp COMMIT PRIVATE READWRITE 21000 – 2FFFF 15 pp FREE 30000 – 12CFFF 253 pp RESERVE PRIVATE 0 12D000 – 12DFFF 1pp COMMIT PRIVATE 260 12E000 – 12FFFF 2pp COMMIT PRIVATE READWRITE 130000 – 130FFF 1pp COMMIT PRIVATE READWRITE 131000 – 13FFFF 15 pp FREE 140000 – 158FFF 25 pp COMMIT PRIVATE READWRITE 159000 – 23FFFF 231 pp RESERVE PRIVATE 0 240000 – 240FFF 1pp COMMIT MAPPED READWRITE 241000 – 24FFFF 15 pp RESERVE MAPPED 0 250000 – 265FFF 22 pp COMMIT MAPPED READONLY 266000 – 26FFFF 10 pp FREE 270000 – 293FFF 36 pp COMMIT MAPPED READONLY 294000 – 29FFFF 12 pp FREE 2A0000 – 2E0FFF 65 pp COMMIT MAPPED READONLY 2E1000 – 2EFFFF 15 pp FREE 2F0000 – 2F2FFF 3pp COMMIT MAPPED READONLY 2F3000 – 2FFFFF 13 pp FREE 300000 – 305FFF 6pp COMMIT MAPPED EXECUTE_READ 306000 – 3BFFFF 186 pp RESERVE MAPPED 0 3C0000 – 3C0FFF 1pp COMMIT MAPPED EXECUTE_READ 3C1000 – 3C7FFF 7pp RESERVE MAPPED 0 3C8000 – 3FFFFF 56 pp FREE 400000 – 400FFF 1pp COMMIT IMAGE READONLY G:\Visual Studio\VB98\VB6.EXE 401000 – 40BFFF 11 pp COMMIT IMAGE EXECUTE_READ 40C000 – 40DFFF 2pp COMMIT IMAGE READWRITE Ниже представлены две вспомогательные функции, которые использованы в программе MapMemory: Function MemType(vValue As Variant) As String ' Функция возвращает имя константы по заданному значению. Dim sName As String Select Case vValue Case &H1000 sName = "COMMIT" Case &H2000 sName = "RESERVE" Case &H4000 sName = "DECOMMIT"
24 Case &H8000 sName = "RELEASE" Case &H10000 sName = "FREE" Case &H20000 sName = "PRIVATE" Case &H40000 sName = "MAPPED" Case &H80000 sName = "RESET" Case &H100000 sName = "TOP_DOWN" Case &H1000000 sName = "IMAGE" Case Else sName = vValue End Select MemType = sName End Function '  Function AccessType(vValue As Variant) As String ' Функция возвращает имя константы по заданному значению. Dim sName As String Select Case vValue Case &H1 sName = "NOACCESS" Case &H2 sName = "READONLY" Case &H4 sName = "READWRITE" Case &H8 sName = "WRITECOPY" Case &H10 sName = "EXECUTE" Case &H20 sName = "EXECUTE_READ" Case &H40 sName = "EXECUTE_READWRITE" Case &H80 sName = "EXECUTE_WRITECOPY" Case &H100 sName = "GUARD" Case &H200 sName = "NOCACHE" Case Else sName = vValue End Select AccessType = sName End Function Пример отображения виртуальной памяти
Глава 14. PE файлы Эта глава посвящена рассмотрению темы об исполняемых файлах. Главная ваша задача здесь состоит в создании приложения VB, которое будет выводить раз­ личную информацию о данном исполняемом файле, включая таблицу экспорта (export table) файла. Таблица представляет собой список функций DLL, до­ ступных вызывающим программам. Подобное приложение находится в архиве примеров на сайте издательства «ДМК Пресс» www.dmkpress.ru и называется rpiPEInfo. Исполняемые файлы – это файлы, которые имеют формат «переносимый ис­ полняемый файл» (Portable Executable File – РЕF), или формат PE-файла. В со­ ответствии с документацией название означает, что такой формат не зависит от архитектуры. Однако это вступает в противоречие с присутствием в файле флага машины (machine flag), о котором в документации говорится следующее: файл образа задачи может выполняться только на заданной машине или системе, эмулиру­ ющей ее. Во всяком случае, PE­файлы включают файлы EXE и DLL, а также OCX, DRV и другие файлы. Но, к сожалению, термин исполняемый файл часто применяется только к ехе­файлам. Исполняемые файлы часто называют также файлами образа задачи (image file). Кстати, с PE­файлами тесно связаны файлы, которые являются результатом работы компиляторов, таких как Visual C++. Они называются объектными фай- лами (object file) и имеют общий формат объектных файлов (Common Object File Format – COFF). Вы будете часто встречать упоминание об этих файлах в одном контексте с PE­файлами. Формат PE­файла очень сложен, что усугубляется еще и плохим описанием в библиотеке MSDN. Однако в данной главе будут рассматриваться только наибо­ лее важные детали, а не полное описание формата. Вы можете бегло ознакомиться с этим материалом и использовать его в будущем как справочную информацию. Перемещение модуля Каждый модуль имеет базовый адрес, устанавливаемый по умолчанию и хра­ нящийся в самом файле. По умолчанию базовый адрес для DLL, скомпилирован­ ной в Visual C++, представляет собой &Н10000000, а в Visual Basic – &H11000000, хотя в принципе эти адреса можно легко модифицировать. В VB можно изменить установку базового адреса по умолчанию, используя вкладку Compile (Компиля­ ция) диалогового окна Project Properties (Свойства проекта).
24 При загрузке модуля (DLL, OCX и пр.) в адресное пространство процесса Windows пытается поместить этот модуль по его базовому адресу, установленному по умолчанию. Однако если этот адрес уже занят другим модулем, происходит конфликт и системе приходится перемещать загружаемый модуль. Давайте рассмотрим несколько не очень известных последствий перемещения модуля, потому что они могут оказывать негативное влияние на характеристики приложения. Проблема заключается в следующем. Исполняемые файлы содержат ссылки на адреса памяти. Например, объектный код, соответствующий следующему ис­ ходному коду: Dim i As Integer i=5 будет содержать ссылку на ячейку памяти с данными (число 5). Это прямая адре­ сация памяти (direct memory reference). То же самое с вызовом функции Call AFunction(7) являющимся переходом на адрес, по которому находится функция AFunction. Так как данный переход осуществляется относительно адреса команды Call, объ­ ектный код этого вызова будет содержать смещение (offset) адреса самой функции относительно адреса команды вызова функции. Это относительная адресация па­ мяти (relative memory reference). Имейте в виду, что функция AFunction может находиться в том же самом исполняемом файле, что и код с вызовом функции, а может быть и в другом исполняемом файле. Теперь, поскольку действительные адреса во время редактирования, или сбор­ ки исполняемого файла (link time), неизвестны, Редактор связей в приведенных выше примерах не может заменить объектный код реальными адресами. Лучшее, что он может сделать, – это использовать адреса, которые являются относитель­ ными (relative), то есть определяются относительно некоторого адреса в модуле. Для прямой адресации адрес определяется относительно базового адреса данного модуля, устанавливаемого по умолчанию. Например, предположим, что базовый адрес модуля равен &H10000000. Если переменная i имеет смещение &H100 от начала модуля, то на машинном языке команда «поместить число 5 в эту переменную» может выглядеть следующим образом (ассемблер): mov dword ptr [10000100], 5 Обратите внимание на присутствие базового адреса исполняемого модуля, который жестко встроен в этот исполняемый модуль. Похожая проблема возникает и тогда, когда вызов обращен к функции, кото­ рая находится в другом модуле (например, в другой DLL). Если базовый адрес другой DLL равен, скажем, &H12000000, команда перехода определялась бы от­ носительно этого базового адреса. Проблема заключается в следующем: нет гарантии, что модуль будет загружен по своему базовому адресу, установленному по умолчанию. Фактически только один Перемещение модуля
250 PE-файлы модуль может быть загружен по любому заданному адресу, но, как вы видите с помощью утилиты просмотра адресного пространства, в нем может находить­ ся очень много модулей. Неудивительно, что какие­то два модуля имеют одни и те же базовые адреса, установленные по умолчанию. К счастью, Microsoft внимательно отнеслась к назначению базовых адресов, устанавливаемых модулям по умолчанию, возможность конфликтов сведена к ми­ нимуму. Вы можете убедиться в этом, просматривая утилиту rpiEnumProcs. Однако все модули, создаваемые в VB (или в VC++), будут иметь один и тот же адрес, установленный по умолчанию, если только вы не измените это значение. Для того чтобы модуль был перемещаемым, в исполняемые файлы введена настроечная информация (relocation information) для перемещаемой программы, то есть для программы, требующей настройки в случае ее загрузки не по базовому адресу, установленному по умолчанию. Эта настроечная информация называется также адресной привязкой (fixup information), или адресными записями (fixups). Если модуль требуется переместить для конкретного процесса, его перемеща­ емая программа должна быть изменена только для этого процесса, но не для дру­ гих процессов, использующих этот модуль. Предположим, например, что модуль Module1 загружен в адресное пространство процесса Process1 по своему базовому адресу по умолчанию. Процессу Process2 требуется загрузить модуль Module1, но его базовый адрес по умолчанию уже занят другим модулем. Поэтому модуль Module1 нужно переместить в адресном пространстве процесса Process2, что потребует каких­то изменений в перемещаемой программе модуля Module1. Так как для процесса Process1 неприемлемы никакие изменения программы моду­ ля Module1, Windows должна создать новую физическую копию данного моду­ ля, чтобы изменить все необходимые адресные записи и отобразить измененный модуль в виртуальное адресное пространство процесса Process2. Это делается с помощью механизма «копирование при записи» (copy­on ­write), о котором гово­ рилось раньше. Дело, конечно, не только в том, что процесс копирования при записи требует дополнительного процессорного времени, но и в том, что новая физическая ко­ пия модуля дополнительно расходует драгоценную физическую память. Таким образом, следует стремиться минимизировать конфликты базовых адресов при создании модулей. Формат PE-файла Теперь вы подготовлены к разговору о формате исполняемого файла. Напом­ ним, что основная задача – создать приложение rpiPEInfo, которое выводит раз­ личную информацию о конкретном исполняемом файле, включая его таблицу экспорта. PE­файл имеет следующую общую структуру:  заголовок PE­файла;  таблица разделов (таблица заголовков разделов);  разделы. На рис. 14.1 общая структура показана более подробно.
251 Заголовок PE-файла PE­файл начинается с заголовка PE­файла (PE file header), который состоит из следующих пунктов:  заглушка MS­DOS;  подпись PE­файла;  заголовок COFF­файла (иногда также называемый заголовком PE­файла);  необязательный (optional) заголовок. Формат PE-файла Рис. 14 .1. Структура PEфайла PEфайл Заголовок PEфайла Табли ца разделов Раздел ы Заголовок PEфайла Разделы Таблица разделов Заголовок раздела 1 Заголовок раздела 2 Текст (испол няемая программа) . . . Заголовок раздела Name VirtualSize VirtualAddress SizeOfRawData PointerToRawData PointerToRelocations PointerToLinenumbers NumberOfRelocations NumberOfLinenumbers Characteristics Необязательный заголовок файла Стандартные пол я Оконно зависимые поля Каталог и данных Заголовок COFFфайла ComputerType NumberofSections TimeDateStamp PointerToSymbolTable NumberOfSymbols SizeOptionalHeader Characteristics Magic MajorLinkerVersion MinorLinkerVersion SizeOfCode SizeOfInitializeData SizeOfUni nitializeData AddressOfEntryPoint BaseOfCode BaseOfData ImageBase SectionAlignment FileAlignment MajorOperatingSystemVersion MinorOperatingSystemVersion MajorImageVersion MinorImageVersion MajorSubsystemVersion MinorSubsystemVersion Reserved SizeOfImage SizeOfHeaders Checksum Subsystem DllCharacteristics SizeOfStackReserve SizeOfStackCommit SizeOfHeapReserve SizeOfHeapCommit LoaderFlags NumberOfRvaAndSizes Таблица экспорта Таблица импорта Таблица ресурсов Таблица исключений Таблица сертифи катов Таблица базовой переадресации Отладка Архитектура Таблица TLS Таблица загрузочной конфи гурации Связанный импорт Дескриптор задержки импорта IAT Delay Import Descriptor Зарезервировано Размер в памяти Размер в памяти Подпись PE Загл ушка MSDOS Подпись PE Заголовок COFFфайла Необязател ьный заголовок Данные Ресурсы Экспорт Импорт и т.д.
252 PE-файлы Заглушка MS-DOS Заголовок PE­файла начинается с заглушки MS DOS, которая представляет собой обычное приложение DOS. По умолчанию это приложение всего лишь вы­ водит знакомое сообщение: «Эта программа не может работать в режиме DOS» (This program cannot be run in DOS mode), если подобную задачу пытаются вы­ полнить в режиме DOS. PE-подпись После заглушки MS­DOS идет подпись PE­файла (PE signature). Подпись PE­файла располагается в файле со смещением, указанным по адресу &H3C, и состоит в настоящее время из четырех байт: "P"/"E"/NULL/NULL Однако такую же подпись может иметь файл любого другого формата. Заголовок COFF-файла Заголовок COFF­файла помещается сразу за подписью PE­файла. (Заметим, что в PE­файле заголовок COFF­файла находится сразу за подписью, а в COFF­ файле он расположен в самом начале.) Этот заголовок содержит различную ин­ формацию о файле, которая представлена в табл. 14.1 . Таблица 14.1. Заголовок COFFфайла для PEфайла Смещение Размер Название поля Описание в необяза в байтах тельном заголовке 0 2 ComputerType Требуемый тип процессора. 0 – неизвестный тип процессора, &H14C – процессоры Intel или совместимые с ними 2 2 NumberOfSections Количество разделов (то есть количество элементов в таблице разделов) 4 4 TimeDateStamp Время и дата создания файла 8 4 PointerToSymbolTable Смещение таблицы имен в COFFфайле или 0, если таблица отсутствует 12 4 NumberOfSymbols Количество элементов таблицы имен. Можно использовать для определения положения таблицы строк, которая находится сразу за таблицей имен 16 2 SizeOfOptionalHeader Размер необязательного заголовка PEфайла 18 2 Characteristics Атрибуты файла Флаг Characteristics (атрибуты) содержит различную информацию о файле:
253  IMAGE_FILE_RELOCS_STRIPPED (&H0001). Указывает на то, что в файле отсутствует базовое значение для перемещения и, следовательно, он загру­ жается по своему предпочтительному базовому адресу, установленному по умолчанию;  IMAGE_FILE_EXECUTABLE_IMAGE (&H0002). Указывает на то, что файл образа задачи является корректным и может быть выполнен. Если такой флаг не установлен, значит, произошла ошибка в работе Редактора связей;  IMAGE_FILE_AGGRESSIVE_WS_TRIM (&H0010). Активная настройка (trim) рабочего набора. Однако как это делается, мне неизвестно;  IMAGE_FILE_LARGE_ADDRESS_AWARE (&H0020). Приложение может ра­ ботать с адресами, превосходящими 2 Гб;  IMAGE_FILE _BYTES_REVERSED_LO (&H0080). Указывает на то, что ис­ пользуется прямой порядок (little­endian) хранения байтов в памяти. Млад­ ший байт 16­разрядного слова хранится в памяти первым, а после него рас­ полагаются байты по старшинству в порядке возрастания. (Эта тема уже рассматривалась в предыдущих главах.) Все компьютеры на базе процес­ соров Intel и совместимых с ними используют прямой порядок хранения байтов. В компьютерах Macintosh используется обратный порядок хранения байтов;  IMAGE_FILE_BYTES_REVERSED_HI (&H8000). Используется обратный порядок (big­endian) хранения байтов;  IMAGE_FILE_32BIT_MACHINE (&H0100). Архитектура компьютера бази­ руется на 32­разрядном машинном слове;  IMAGE_FILE_DEBUG_STRIPPED (&H0200). Отладочная информация была удалена из файла образа задачи;  IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP (&H0400). При попытке вы­ полнить файл с образом задачи со съемного носителя происходит копиро­ вание файла в файл подкачки, откуда он и загружается;  IMAGE_FILE _SYSTEM (&H1000). Данный файл образа задачи является системным файлом, а не пользовательской программой;  IMAGE_FILE_DLL (&H2000). Данный файл образа задачи представляет собой динамически подключаемую библиотеку (DLL);  IMAGE_FILE _UP_SYSTEM_ONLY (&H4000). Файл должен выполняться только на UP­машине (чтобы это ни означало);  IMAGE_FILE_LINE_NUMS_STRIPPED (&H0004). Была удалена нумерация COFF­строк;  IMAGE_FILE_LOCAL_SYMS_STRIPPED (&H0008). Элементы таблицы имен COFF­файла для локальных имен были удалены;  IMAGE_FILE_16BIT_MACHINE (&H0040). Зарезервировано. Утилита rpiPEInfo проверяет подпись PE­файла. Если подпись есть, програм­ ма дешифрирует флаг атрибутов COFF­заголовка. Необязательный заголовок Необязательный заголовок (который, кстати, не является необязательным в PE­файле) предоставляет информацию загрузчику (loader), то есть системной Формат PE-файла
254 PE-файлы компоненте Windows, которая отвечает за загрузку исполняемого модуля в память. К сожалению, этот необязательный заголовок называют также PE­заголовком (в противоположность заголовку PE­файла). Необязательный заголовок имеет три части. Часть 1. Стандартные поля. Стандартные поля описаны в табл. 14.2 . Следует еще раз напомнить, что не стоит беспокоиться об усвоении всей информации, содержащейся в этой и последующих таблицах. Сведения представлены здесь большей частью как справочные данные. Таблица 14.2 . Стандартные поля в необязательном заголовке Смещение Размер Название поля Описание в необяза в байтах тельном заголовке 0 2 Magic Положительное целое число, определяющее состояние файла образа задачи. Наиболее распространенное значение &H10B указывает на обычный исполняемый файл 2 1 MajorLinkerVersion Основной номер версии Редактора связей 3 1 MinorLinkerVersion Дополнительный номер версии Редактора связей 4 4 SizeOfCode Размер раздела кода (текста) PEфайла или сумма всех разделов кода, если их несколько 8 4 SizeOfInitializedData Размер раздела с инициализиро ванными данными PEфайла или сумма всех таких разделов, если их несколько 12 4 SizeOfUninitializedData Размер раздела с инициализиро ванными данными (BSS) PEфайла или сумма всех таких разделов, если их несколько 16 4 AddressOfEntryPoint Смещение точки входа PEфайла относительно базового адреса образа задачи после его загрузки в память. Для образов программ – это стартовый адрес. Необязателен для DLL 20 4 BaseOfCode Смещение начала раздела кода относительно базового адреса образа задачи после его загрузки в память 24 4 BaseOfData Смещение начала раздела данных относительно базового адреса образа задачи после его загрузки в память
255 Часть 2. Поля, относящиеся к Windows. Следующие поля (21 поле) в необя­ зательном заголовке содержат дополнительную информацию, необходимую для Редактора связей и загрузчика Windows. Они описаны в табл. 14.3 . Таблица 14.3. Поля, относящиеся к Windows Смещение Размер Название Описание в необяза в байтах поля тельном заголовке 28 4 ImageBase Предпочтительный базовый адрес образа задачи после загрузки в память. Он должен быть выровнен на границу выделения памяти (произведение 64 Кб). По умолча нию для DLL – &H10000000. По умолчанию для exeфайлов Windows NT или Windows 9x – &H00400000 32 4 SectionAlignment Выравнивание (в байтах) разделов PEфайла после загрузки в память. По умолчанию устанавливается размер страницы для архитектуры Intel, 4 Кб 36 4 FileAlignment Выравнивание (в байтах) для разделов необработанных данных в файле образа задачи 40 2 MajorOperatingSystemVersion Основной номер версии операционной системы 42 2 MinorOperatingSystemVersion Дополнительный номер версии операционной системы 44 2 MajorImageVersion Основной номер версии образа задачи 46 2 MinorImageVersion Дополнительный номер версии образа задачи 48 2 MajorSubsystemVersion Основной номер версии подсистемы 50 2 MinorSubsystemVersion Дополнительный номер версии подсистемы 52 4 Reserved Зарезервировано 56 4 SizeOfImage Размер образа задачи в байтах, включая все заголовки 60 4 SizeOfHeaders Общий размер заглушки MSDOS, PEзаголовка и заголовков разде лов, выровненных на величину, кратную FileAlignment 64 4 CheckSum Контрольная сумма файла образа задачи, которая используется для обнаружения ошибок 68 2 Subsystem Подсистема, требующаяся для выполнения этого образа задачи Формат PE-файла
25 PE-файлы Таблица 14.3. Поля, относящиеся к Windows (окончание) Смещение Размер Название Описание в необяза в байтах поля тельном заголовке 70 2 DllCharacteristics Характеристики DLL 72 4 SizeOfStackReserve Размер зарезервированного стека 76 4 SizeOfStackCommit Размер переданного стека 80 4 SizeOfHeapReserve Размер зарезервированной области локальной кучи 84 4 SizeOfHeapCommit Размер переданной области локальной кучи 88 4 LoaderFlags Устарело 92 4 NumberOfRvaAndSizes Количество элементов словаря данных в остатке необязательного заголовка Поле подсистемы может иметь одно из следующих значений:  IMAGE_SUBSYSTEM_UNKNOWN (0). Указывает на неизвестную подсистему;  IMAGE_SUBSYSTEM_NATIVE (1). Используется для драйверов устройств и собственных (native) процессов Windows NT;  IMAGE_SUBSYSTEM_WINDOWS_GUI (2). Образ задачи графического (GUI) режима Windows;  IMAGE_SUBSYSTEM_WINDOWS_CUI (3). Образ задачи символьного (CUI) режима Windows, выполняется в окне символьной консоли;  IMAGE_SUBSYSTEM_POSIX_CUI (7). Образ задачи Posix;  IMAGE_SUBSYSTEM_WINDOWS_CE_GUI (9). Работает в Windows CE. Часть 3. Каталог данных. Каждый PE­файл содержит несколько таблиц и строк, которые требуются для Windows. Каталог данных (data directory) пред­ ставляет собой таблицу, которая описывает положение и размер указанных ресур­ сов. Это будет очень важно при создании программы rpiPEFile. Элементы каталога данных также называются каталогами данных. Каждый элемент каталога данных имеет форму, определенную в операторе typedef и, следовательно, размер 8 байт: typedef struct _IMAGE_DATA _DIRECTORY { DWORD RVA; DWORD Size; // Размер элемента в байтах. } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA _DIRECTORY; Поле RVA содержит относительный виртуальный адрес (Relative Virtual Address – RVA) элемента каталога данных (таблицы или строки). Вероятно, это требует некоторых пояснений. В документации даются следующие определения: в файле образа задачи RVA представляет собой адрес элемента, загруженного в па­ мять, минус базовый адрес файла образа задачи. RVA элемента будет почти всегда отличаться от его позиции в файле на диске (указатель файла). VA (виртуальный
25 адрес) представляет собой почти то же самое, что и RVA, но базовый адрес файла образа задачи не вычитается. Этот адрес называется виртуальным, поскольку Windows NT создает собственные виртуальные адресные пространства для каждо­ го процесса, независимые от физической памяти. Почти всегда виртуальный адрес следует рассматривать просто как адрес. Виртуальный адрес не так предсказуем, как RVA, поскольку загрузчик может и не разместить образ задачи в памяти по его предпочтительному адресу. На первый взгляд все представляется простым и понятным. Кажется, что можно просто прибавить RVA к базовому адресу файла образа задачи после за­ грузки в память, чтобы получить адрес таблицы. Однако такое решение неверно. К счастью, существует API­функция ImageRvaToVa, которая, как следует из до­ кументации, определяет местоположение относительного виртуального адреса в образе заголовка в отображении файла в память и возвращает виртуальный адрес соответствующего байта в файле. Вероятно, если бы для получения VA требовалось только добавить RVA к базо­ вому адресу, то функция ImageRvaToVa, которой необходим базовый адрес, была бы не нужна. Это еще раз подтверждает, что здесь происходит что­то другое. В табл. 14.4 показан стандартный каталог данных. Заметьте, что размер этой таблицы не ограничен и может расти. Поле NumberOfRvaAndSizes в необяза­ тельном заголовке (табл. 14.3) указывает номера элементов в каталоге данных. Таблица 14.4 . Каталог данных Смещение Размер Название Описание в необяза в байтах поля тельном заголовке 96 8 Таблица экспорта Адрес и размер таблицы экспорта 104 8 Таблица импорта Адрес и размер таблицы импорта 112 8 Таблица ресурсов Адрес и размер таблицы ресурсов 120 8 Таблица исключений Адрес и размер таблицы исключений 128 8 Таблица сертификатов Адрес и размер таблицы атрибутов сертификатов 136 8 Таблица баз перемещения Адрес и размер таблицы баз перемещения 144 8 Отладка Начальный адрес и размер отладочных данных 152 8 Архитектура Адрес и размер данных, зависящих от архитектуры 160 8 Глобальный Ptr Относительный виртуальный адрес регистра глобального указателя 168 8 Таблица TLS Адрес и размер таблицы локальной памяти потока (Thread Local Storage – TLS) 176 8 Таблица конфигураций Адрес и размер таблицы загрузки конфигураций загрузки Формат PE-файла
25 PE-файлы Таблица 14.4 . Каталог данных (окончание) Смещение Размер Название Описание в необяза в байтах поля тельном заголовке 184 8 Границы импорта Адрес и размер таблицы границ импорта 192 8 IAT Адрес и размер таблицы адресов импорта (Import Address Table – IAT) 200 8 Дескриптор задержки Адрес и размер дескриптора импорта задержки импорта 208 16 Зарезервировано Таблица разделов После необязательного заголовка (и, следовательно, после заголовка PE­фай­ ла) находится таблица разделов, каждый элемент которой является заголовком раздела. Большую часть PE­файла составляют разделы (section), заголовками которых и являются элементы таблицы. Заметьте, что все заголовки идут подряд, а после них размещаются все разделы (вместо того, чтобы сразу после заголовка шел относящийся к нему раздел). Кроме всего прочего, каждый элемент таблицы разделов содержит смещение и размер соответствующего раздела. Обратите внимание, что таблица разделов находится сразу за необязательным заголовком, и это единственная возможность определить ее расположение. (Раз­ мер необязательного заголовка задается в заголовке COFF­файла.) Кроме того, количество элементов таблицы разделов содержится в поле NumberOfSections в заголовке COFF­файла. Элементы таблицы нумеруются, начиная с единицы. Каждый заголовок раздела (элемент таблицы разделов) имеет размер 40 байт и формат, показанный в табл. 14.5 . Таблица 14.5 . Формат элемента таблицы разделов (заголовок раздела) Смещение Размер Название Описание в необяза в байтах поля тельном заголовке 0 8 Name Строка из 8 байт с завершающим нулем. 8 4 VirtualSize Общий размер раздела после загрузки в память. Если это значение больше, чем SizeOfRawData, то раздел заканчивается нулем 12 4 VirtualAddress Адрес первого байта раздела после загрузки в память относительно базы образа задачи 16 4 SizeOfRawData Размер инициализированных данных на диске
25 Таблица 14.5. Формат элемента таблицы разделов (заголовок раздела) (окончание) Смещение Размер Название Описание в необяза в байтах поля тельном заголовке 20 4 PointerToRawData Указатель позиции первой страницы раздела внутри файла. Это значение должно быть кратно величине FileAlignment из необязательного заголовка 24 4 PointerToRelocations Для PEфайлов равно нулю 28 4 PointerToLinenumbers Указатель позиции в файле на начало элементов номеровстрок раздела. Установлен в нуль, если номера строк COFF отсутствуют 32 2 NumberOfRelocations Для PEфайлов равно нулю 34 2 NumberOfLinenumbers Количество элементов номера строки раздела 36 4 Characteristics Флаги, описывающие характерис тики раздела Разделы Файл образа задачи, как правило, имеет некоторые или все из следующего списка разделов. Раздел текста Раздел текста (исполняемый код) обозначается расширением .text и содержит исполняемый код данного файла. Разделы данных Разделы данных обозначаются расширениями .bss, .rdata и .data. Раздел .bss со­ держит неинициализированные данные, включая статические переменные. Раздел .rdata – данные «только для чтения», такие как символьные строки и константы. Все остальные переменные хранятся в разделе .data. Раздел ресурсов Этот раздел, обозначаемый .rsrc, содержит информацию о ресурсах, использу­ емых данным файлом. Раздел перемещения Этот раздел, обозначаемый .reloc, хранит таблицу адресных записей (fix­up), в которой находятся элементы для всех адресных привязок указанного файла. Данная тема уже обсуждалась ранее в этой главе, когда рассматривалось переме­ щение модуля. Как правило, адресные привязки нужны для настройки адресов в файле образа задачи на основе реального адреса загрузки, который может отли­ чаться от адреса загрузки, установленного по умолчанию. Формат PE-файла
20 PE-файлы Раздел экспорта Раздел данных экспорта, обозначаемый .edata, содержит информацию об эк­ спортируемых функциях и глобальных переменных. Он начинается с таблицы каталога экспорта (export directory table), показанной в табл. 14.6 . Таблица 14.6 . Таблица каталога экспорта Смещение Размер Поле Описание в необяза в байтах тельном заголовке 0 4 Флаги экспорта Зарезервировано 4 4 Штамп время/дата Время и дата создания экспортных данных 8 2 Основная версия Основной номер версии 10 2 Дополнительная версия Дополнительный номер версии 12 4 RVA имени RVA ASCIIстроки, которая содержит имя DLL 16 4 База порядковых номеров Начальный порядковый номер экспорта в соответствии с таблицей адресов экспорта 20 4 Элементы таблицы адресов Количество элементов в таблице адресов экспорта 24 4 Количество указателей имен Количество элементов в таблице указателей имен (и таблице порядковых номеров) 28 4 RVA таблицы адресов RVA таблицы адресов экспорта экспорта 32 4 RVA указателя имен RVA таблицы указателей имен экспорта 36 4 RVA порядковых номеров RVA таблицы порядковых номеров Кстати, таблица каталога экспорта содержит адрес экспортируемых функций в PE­файле, но в данном случае нас интересуют только их имена, представленные таблицах (см. рис. 14.2). Заметьте, что смещение 32 (относительно базы образа задачи) в таблице каталога экспорта имеет адрес таблицы указателей имен экспорта (export name pointer table). Она является массивом указателей в таблице имен экспорта (export name table), ко­ торая и содержит имена (строки с завершающим нулем) экспортируемых функций. Так как у этих имен может быть разная длина, нужен указатель на каждое имя. Бывает, что у некоторых DLL, экспортирующих функции, отсутствует раздел экспорта (по крайней мере, раздел, обозначаемый .edata). Это означает, что для получения таблицы каталога экспорта простой поиск раздела экспорта непри­ годен. В приложении rpiPEFile следует применить другой подход. К счастью, элемент данных таблицы каталога экспорта (см. табл. 14.4) почти всегда является действительным, так что можно использовать этот элемент для получения RVA таблицы каталога экспорта.
21 Раздел импорта Этот раздел, обозначаемый .idata, содержит информацию о функциях, которые данный файл импортирует. Давайте ближе познакомимся с этим разделом. На рис. 14.3 показано его строение. Между прочим, в документации утверждается, что в таблице Имя/Описание имя в каждом элементе имеет смещение 4, но из экспериментов следует, что правильное значение смещения – 2. Этот раздел начинается с таблицы каталога импорта (import directory table). В ней представлены 20­байтовые блоки для каждой DLL, импортирующей функ­ ции данному исполняемому файлу. Будем называть эти библиотеки DLL импор­ 20байтовый элемент Нулевой (null) элемент 20байтовый элемент Справочная таблица импорта для DLL1 31разрядный RVA в таблице Имя/Описание 31разрядный RVA в таблице Имя/Описание 31разрядный RVA в таблице Имя/Описание 31разрядный RVA в таблице Имя/Описание 31разрядный RVA в таблице Имя/Описание 31разрядный RVA в таблице Имя/Описание 00...0 x x 0 31 1 в 31ом бите указывает на импорт по имени 0 указывает на импорт по порядку (по позиции) 0 00...0 x x 0 31 0 x x Смещение в блоке Смещение в элементе x Имя функции (завершается нулем) Имя функции (завершается нулем) Имя функции (завершается нулем) 0 2 ? x x Замыкающий бит. Добавляемый, если размер блока  нечетное число Таблица каталога импорта RVA справочной таблицы импорта для DLL1 RVA строки с именем DLL1 RVA справочной таблицы импорта для DLL2 RVA строки с именем DLL2 0 12 0 0 Таблица Имя/Описание (одна на раздел) Рис. 14.3. Детали раздела таблицы импорта Формат PE-файла Рис. 14 .2 . Таблицы имен экспорта Таблица каталога экспорта Количество имен в таблице экспортируемых имен RVA таблицы экспортируемых имен Таблица указателей экспортируемых имен Имя функции/0 Имя функции/0 Имя функции/0 Имя функции/0 Имя функции/0 Имя функции/0 Имя функции/0 Имя функции/0 Имя функции/0 Таблица экспортируемых имен
22 PE-файлы та (import DLL). Каждый блок этой таблицы имеет два важных элемента: RVA справочной таблицы импорта для соответствующих DLL импорта и RVA строки, в которой указано имя данной DLL импорта. Каждый элемент справочной таблицы импорта является 32­разрядным полем и представляет одну из импортируемых функций данной DLL импорта. Старший бит каждого элемента позволяет определить, является ли данная функция импор­ тируемой по имени (бит имеет значение 1) или по порядковому номеру, то есть по расположению (бит имеет значение 0). Остальные 31 бит формируют RVA в таблице Имя/Описание (Hint/Name table). Каждый элемент таблицы Имя/Описание содержит, начиная со смещения 4, имя данной функции (а не указатель на имя). Таким образом, размер эле­ ментов таблицы Имя/Описание будет варьироваться. Элемент завершается дополнительным битом только тогда, когда необходимо дополнить длину эле­ мента до четного значения, чтобы следующий элемент начинался на четной линии раздела. Пример получения информации о PE-файле Теперь, когда вы имеете некоторое представление о формате PE­файла, можно перейти к созданию утилиты rpiPEInfo. На рис. 14.4 показана обработка ею файла COMCTL32.DLL. Полный исходный код находится в архиве примеров, поэтому будем рассмат­ ривать только основные моменты. Вам известно, что PE­файл содержит относи­ тельные виртуальные адреса для большинства элементов. Как говорилось выше, эти адреса относятся к отображению файла в память, а это не то же самое, что образ файла на диске. Принятый при создании утилиты подход заключается в том, что­ бы отобразить анализируемый исполняемый файл в память, а затем преобразовать относительные виртуальные адреса в виртуальные. Для отображения PE­файла в память здесь используется функция MapAndLoad, экспортируемая библиотекой IMAGEHLP.DLL: BOOL MapAndLoad( IN LPSTR ImageName, IN LPSTR DllPath, OUT PLOADED_IMAGE LoadedImage, IN BOOL DotDll, IN BOOL ReadOnly); Так код функции выглядит в VB: Public Declare Function MapAndLoad Lib "Imagehlp.dll" ( _ ByVal ImageName As String, _ ByVal DLLPath As String, _ LoadedImage As LOADED_IMAGE, _ DotDLL As Long, _ ReadOnly As Long) As Long
23 В Win32 API имеется также соответствующая функция UnMapAndLoad: BOOL UnMapAndLoad( [IN] PLOADED_IMAGE LoadedImage ); В VB она записывается следующим образом: Public Declare Function UnMapAndLoad Lib "Imagehlp.dll" ( _ LoadedImage As LOADED_IMAGE) As Long Функция MapAndLoad отображает исполняемый файл в виртуальную память и заполняет структуру LOADED_IMAGE всеми видами полезных данных: Public Type LOADED_IMAGE ' 48 байт (46 байт упаковано). ModuleName As Long hFile As Long MappedAddress As Long ' Базовый адрес отображенного файла. pFileHeader As Long ' Указатель на IMAGE_PE _FILE _HEADER . pLastRvaSection As Long ' Указатель на первый заголовок раздела COFF. ' (Таблица раздела)? Рис. 14.4 . Окно утилиты rpiPEInfo Пример получения информации о PE-файле
24 PE-файлы NumberOfSections As Long pSections As Long ' Указатель на первый заголовок раздела COFF ' (Таблица раздела)? Characteristics As Long ' Собственное значение образа. fSystemImage As Byte fDOSImage As Byte Links As LIST_ENTRY ' Два значения типа long. SizeOfImage As Long End Type В данном случае важной информацией являются базовый адрес загруженного об­ раза задачи (MappeAddress) и указатель на заголовок PE­файла (pFileheader), который, кстати, в документации называется также заголовком NT (NT headers). Для получения VA из RVA можно использовать следующую функцию: LPVOID ImageRvaToVa( [IN] PIMAGE_PE _FILE _HEADER NtHeaders, [IN] LPVOID Base, [IN] DWORD Rva, [IN, OUT] PIMAGE_SECTION_HEADER *LastRvaSection ); или в VB: Public Declare Function ImageRvaToVa Lib "Imagehlp.dll" ( _ ByVal NTHeaders As Long, _ ByVal Base As Long, _ ByVal RVA As Long, _ ByVal LastRvaSection As Long) As Long Обратите внимание, что данной функции требуется не только RVA, но также указатель на заголовок PE­файла (заголовки NT) и базовый адрес загруженного образа задачи. Это еще раз доказывает, что преобразование RVA в VA – не просто прибавление базового адреса. (Последний параметр ImageRvaToVa не имеет значения.) Функция возвращает VA. Структуры Вам потребуется несколько структур (пользовательских типов). Они нахо­ дятся в заголовочном файле Winnt.h и являются отражением различных таблиц PE­файла. (Изменено несколько имен для приведения в соответствие с докумен­ тацией по PE­файлам.) В программе rpiPEFile следует объявлять структуры снизу вверх, чтобы избе­ жать сообщений VB об ошибках, связанных с опережающими ссылками (forward reference). В этом может помочь рис. 14.1 . Начнем с элемента каталога данных, который используется для получения RVA таблиц экспорта и импорта: ' Элемент каталога данных. Public Type IMAGE_DATA _DIRECTORY ' Восемь бит. RVA As Long size As Long End Type
25 Далее следует необязательный заголовок, все три его части. Константа IMAGE_NUMBEROF_DIRECTORY_ENTRIES (последний член структуры) опреде­ ляется равной 16 в одном из заголовочных файлов, хотя в документации и утверж­ дается, что количество элементов каталога данных не зафиксировано. ' Необязательный заголовок (все три части). Public Type IMAGE_OPTIONAL_HEADER ' 232 байта. ' Стандартные поля. Magic As Integer MajorLinkerVersion As Byte MinorLinkerVersion As Byte SizeOfCode As Long SizeOfInitializedData As Long SizeOfUninitializedData As Long AddressOfEntryPoint As Long BaseOfCode As Long BaseOfData As Long ' Дополнительные поля NT. ImageBase As Long SectionAlignment As Long FileAlignment As Long MajorOperatingSystemVersion As Integer MinorOperatingSystemVersion As Integer MajorImageVersion As Integer MinorImageVersion As Integer MajorSubsystemVersion As Integer MinorSubsystemVersion As Integer Win32VersionValue As Long SizeOfImage As Long SizeOfHeaders As Long CheckSum As Long Subsystem As Integer DllCharacteristics As Integer SizeOfStackReserve As Long SizeOfStackCommit As Long SizeOfHeapReserve As Long SizeOfHeapCommit As Long LoaderFlags As Long NumberOfRvaAndSizes As Long '96. ' Каталоги данных. DataDirectory(0 To IMAGE_NUMBEROF_DIRECTORY_ENTRIES) _ As IMAGE_DATA _DIRECTORY ' 17 * 8 + 96 = 232. End Type Далее идет заголовок PE­файла без заглушки MS DOS: ' Заголовок PEфайла без заглушки MS DOS. Public Type IMAGE_PE _FILE_HEADER ' 256 байт. Пример получения информации о PE-файле
2 PE-файлы Signature As Long ' 4 байта  подпись PE. FileHeader As IMAGE_COFF_HEADER ' 20байтэто ' заголовок COFF. OptionalHeader As IMAGE_OPTIONAL_HEADER ' 232 байта. End Type Объявлен, но не использован заголовок самого COFF­файла: ' Заголовок COFFфайла. Public Type IMAGE_COFF_HEADER ' 20 байт. Machine As Integer NumberOfSections As Integer TimeDateStamp As Long PointerToSymbolTable As Long NumberOfSymbols As Long SizeOfOptionalHeader As Integer Characteristics As Integer End Type Наконец, работаем с таблицей каталога экспорта: ' Таблица каталога экспорта. Public Type IMAGE_EXPORT_DIRECTORY_TABLE ' 40 байт. Characteristics As Long TimeDateStamp As Long MajorVersion As Integer MinorVersion As Integer Name As Long Base As Long NumberOfFunctions As Long NumberOfNames As Long ' Это нам нужно. pAddressOfFunctions As Long ExportNamePointerTableRVA As Long ' Это нам нужно. pAddressOfNameOrdinals As Long End Type Здесь объявлено еще несколько структур, вы можете найти их в архиве. Получение информации о версии Первая функция, которая вызывается, когда пользователь выбирает имя фай­ ла, – GetVersionInfo. Она работает с несколькими функциями из библиотеки VERSION.DLL: GetFileVersionInfoSize, GetFileVersionInfo, VerQueryValue, и VerLanguageName. Эти функции используются программами установки. Функция GetFileVersionInfo заполняет буфер данными о версии файла. Для их получения можно использовать функцию VerQueryValue: Public Declare Function VerQueryValue Lib "version.dll" Alias _ "VerQueryValueA" ( _ pBlock As Byte, _ ByVal lpSubBlock As String, _
2 lplpBuffer As Long, _ puLen As Long _ ) As Long передавая различные строки с описанием в параметр lpSubBlock. Например, передавая строку «\», получим корневой блок (root block), который является указателем на следующую структуру (MS означает старший, most significant, а LS – младший, least significant): Public Type VS_FIXEDFILEINFO dwSignature As Long dwStrucVersion As Long dwFileVersionMS As Long dwFileVersionLS As Long dwProductVersionMS As Long dwProductVersionLS As Long dwFileFlagsMask As Long dwFileFlags As Long dwFileOS As Long dwFileType As Long dwFileSubtype As Long dwFileDateMS As Long dwFileDateLS As Long End Type Есть также довольно сложная операция для получения названия компании. Сначала нужно найти массив перевода, который задает языки, поддерживаемые данным файлом. Отыскав нужный язык и кодовую страницу с кодами английского языка, вы можете передать функции VerQueryValue подраздел (subblock): "\StringFileInfo\" & sCodePageID & "\CompanyName" Получение характеристик файла Следующий шаг – открыть файл и отыскать подпись PE­файла. В соответс­ твии с документацией по PE­файлам, смещение подписи находится по адресу &H3C относительно начала файла. При помощи приведенного ниже кода функция GetPEFileChars получает 4 байта с подписью по указанному смещению и про­ веряет их на правильность: 'Проверяем подпись PEфайла. Get #fr, &H3C + 1, bSigOffset Get #fr, bSigOffset + 1, lSignature If Not lSignature = &H4550 Then ' PE\0\0 перевернуто в памяти. Close fr txtDetails = txtDetails & vbCrLf & " Неверная PEподпись" Exit Function End If Затем можно непосредственно получить флаг атрибутов и некоторые дру­ гие данные, например имена разделов, из заголовка COFF­файла. Однако здесь есть небольшая проблема. При рассмотрении рис. 14.4 вы можете заметить, что Пример получения информации о PE-файле
2 PE-файлы кроме имен разделов программа выдает какой­то «мусор». Это объясняется тем, что количество разделов, которое используется для расчета смещения таблицы разделов, после извлечения из необязательного заголовка не всегда определяется корректно. Получение имен экспорта Программа начинается с функции, используемой для получения таблицы эк­ спорта. Эта функция вызывается только для PE­файлов. Общее описание проце­ дуры таково. Во­первых, программа отображает файл (sFile) в память и загружает струк­ туру LOADED_IMAGE: Dim loadimage As LOADED_IMAGE lret = MapAndLoad(sFile, "", loadimage, True, True) После этого можно получить базовый адрес загруженного образа задачи: baseaddr = loadimage.MappedAddress Далее заголовок PE­файла копируется в вашу собственную переменную, что­ бы можно было получить доступ к его полям: Dim peheader As IMAGE_PE _FILE _HEADER CopyMemory ByVal VarPtr(peheader), ByVal loadimage.pFileHeader, 256 Далее извлекается VA из RVA первого каталога данных, который входит в таблицу каталога экспорта. Константа IMAGE_DIRECTORY_ENTRY_EXPORT опре­ делена со значением 0 – это индекс первого каталога данных: rvaExportDirTable = peheader.OptionalHeader. _ DataDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT).RVA vaExportDirTable = ImageRvaToVa(loadimage.pFileHeader, _ loadimage.MappedAddress, rvaExportDirTable, 0&) Снова данные копируются в вашу собственную структуру: ' Каталог экспорта. Dim exportdir As IMAGE_EXPORT_DIRECTORY_TABLE CopyMemory ByVal VarPtr(exportdir), ByVal vaExportDirTable,_ LenB(exportdir) Из этой копии (см. рис. 14.2) можно получить количество экспортируемых имен: cNames = exportdir.NumberOfNames Теперь exportdir.ExportNamePointerTableRVA является RVA таблицы указателей имен экспорта (см. рис. 14.2), и вы получаете ее VA следующим образом: ExportNamePointerTableVA = ImageRvaToVa(loadimage.pFileHeader, _ loadimage.MappedAddress, exportdir.ExportNamePointerTableRVA, 0&) Теперь можно просто пройти по таблице указателей имен экспорта, собирая искомые строки:
2 ' Начинаем с первого имени. pNextAddress = ExportNamePointerTableVA ' Получаем следующий адрес (имени экспорта). VBGetTarget lNextAddress, pNextAddress, 4 lvExports.ListItems.Clear Fori=0TocNames1 ' Преобразуем адрес этого имени из RVA в VA. lNextAddress = ImageRvaToVa(loadimage.pFileHeader, _ loadimage.MappedAddress, lNextAddress, 0&) ' Преобразуем строку ANSI в BSTR. sName = LPSTRtoBSTR(lNextAddress) lvExports.ListItems.Add , , sName ' Смещаемся на следующий адрес в таблице. pNextAddress = pNextAddress + 4 ' Получаем адрес. VBGetTarget lNextAddress, pNextAddress, 4 Next И в заключение вызывается функция UnMapAndLoad. Получение имен импорта Извлечение информации об импорте требует другого подхода, так как струк­ туры таблиц отличаются. Далее следует общее описание процедуры. Снова нужно отобразить и загрузить файл. Это можно было бы сделать один раз и для экспорта, и для импорта, но код легче сопровождать, разделив две задачи. Так же, как и для экспорта, вы получаете VA таблицы каталога импорта (см. рис. 14.3): rvaImportDirTable = peheader.OptionalHeader. _ DataDirectory(IMAGE_DIRECTORY_ENTRY_IMPORT).RVA ' Вызываем RvaToVa, чтобы получить VA по RVA. vaImportDirTable = ImageRvaToVa(loadimage.pFileHeader, _ loadimage.MappedAddress, rvaImportDirTable, 0&) Затем следует перебрать элементы этой таблицы, получая справочные табли­ цы импорта и имена DLL для каждого элемента, пока не будет достигнут нулевой элемент. Для каждого ненулевого элемента следующий цикл Do собирает имена импорта: Do VBGetTarget LookupTableEntry, pLookupTableEntry, 4 If LookupTableEntry = 0 Then Exit Do ' Проверяем старший разряд. ' Если 0, то пропускаем его, так как это по порядку, а не по имени. Пример получения информации о PE-файле
20 PE-файлы If LookupTableEntry >= 0 Then cNames = cNames + 1 ' Маскируем MSB. LookupTableEntry = LookupTableEntry And &H7FFFFFFF ' Преобразуем из RVA в VA, чтобы получить адрес имени функции. pImportFunctionName = ImageRvaToVa(loadimage.pFileHeader, loadimage.MappedAddress, LookupTableEntry, 0&) ' Имя имеет смещение 2 внутри элемента. sFunctionName = LPSTRtoBSTR(pImportFunctionName + 2) Set li = lvImports.ListItems.Add() li.Text = sFunctionName li.ListSubItems.Add , , sDLLName End If ' Следующий элемент. pLookupTableEntry = pLookupTableEntry + 4 Loop
Глава 15. Основы Глава 16. Сообщения Windows Глава 17. Классы окон и процесс создания окна Глава 18. Модификация класса окна Глава 19. Ловушки Windows Глава 20. Внедрение DLL и доступ к внешнему процессу Часть III Окна. Программирование User32.DLL
Глава 15. Основы В данной главе излагаются основы Microsoft Windows, включая многие термины, относящиеся к окнам. Терминология Давайте начнем изучение с основных терминов, связанных с окнами. Следует отметить, что многие из этих терминов еще не устоялись и их значение может ме­ няться в зависимости от контекста. Тем не менее данные здесь определения будут служить общими ориентирами:  Windows, начиная свою работу, автоматически создает окно Рабочего стола (desktop window), системное окно, которое определяет фон экрана и служит основой для окон всех приложений;  все приложения Windows с графическим интерфейсом (graphical­based) в от­ личие от приложений командной строки (console­based) создают, по крайней мере, одно окно, которое называется главным (main) окном приложения. Ниже представлены основные виды окон. Окно приложения (application window) представляет собой окно, которое обыч­ но имеет один или несколько следующих атрибутов: строку заголовка со значком и кнопками Развернуть (maximize), Свернуть (minimize), Закрыть (close), строку меню и полосы прокрутки. Понятие, с которым многие пользователи не знакомы, но которое имеет большое значение для программистов – клиентская область (client area). Это область окна, которая может быть областью вывода текста или графики. Таким образом, клиентская область не включает, например, масштабных линеек, строк меню, рамок окна или полос прокрутки. Часть окна, включающая все, перечисленное выше, называется неклиентской областью (nonclient area). Управляющий элемент (control) – специализированное окно, предназначен­ ное обычно только для связи с пользователем. Каждый управляющий элемент является окном­потомком некоторого другого окна, называемого контейнером управляющих элементов (control’s container). Диалоговое окно (dialog box) представляет собой окно, основная функция кото­ рого – быть контейнером управляющих элементов, и, следовательно, обеспечивать многофункциональную связь с пользователем. Окно сообщения (message box) – окно, которое используется для вывода сооб­ щений на экран. Некоторые окна рассматриваются как высокоуровневые окна (top­level windows). К сожалению, указанный термин определяется по­разному в различных разделах
23 документации MSDN. С одной стороны, под ним подразумевается окно, у которо­ го нет родительского (parent) окна. (Родительские окна будут описаны немного позже.) С другой стороны, тем же термином определяется окно, не имеющее роди­ тельских окон или родительским окном которого является окно Рабочего стола. То же самое можно сказать и о API­функции EnumChildWindows. В соответствии с документацией, она перечисляет все высокоуровневые окна (если определенный параметр установлен в нуль). Однако вы увидите, что эта функция перечисляет не­ которые окна, все­таки имеющие родительское окно, и это не окно Рабочего стола. Единственный разумный вывод заключается в том, что термин «высокоуровневое окно» используется непоследовательно. Тем не менее он обычно относится к окну, которое не является окном­потомком. В главе 16 данная тема будет рассматриваться более подробно, а сейчас вам надо знать только то, что с каждым окном ассоциируются следующие элементы:  дескриптор окна (window handle) является 32­разрядным числом, которое однозначно идентифицирует данное окно в системе;  класс окна (window class) – отличительный признак, используемый для со­ здания окна и определяющий начальные характеристики окна. Классы окон будут обсуждаться в главе 17;  стили окна (window styles) представляют собой свойства, задающие вне­ шний вид, функциональные параметры окна. Рассматриваются в следующем разделе. Стили окон Кроме дескриптора и класса каждое окно имеет один или несколько стилей, которые определяют его характеристики, такие как отсутствие или наличие рамок, строк заголовка, кнопок Развернуть и т.д . Стили окон задаются символьными константами, которых, по грубым подсчетам, около 200. Обычно стили комби­ нируются (с помощью констант) для достижения нужного эффекта. Например, следующая установка стиля окна: WS_MAXIMIZEBOX Or WS_MINIMIZEBOX Or WS_SYSMENU определяет наличие у окна кнопок Развернуть и Свернуть, меню окна (window menu), которое часто называют системным меню (system menu). Заметьте, что некоторые стили являются комбинациями других стилей. На­ пример, стиль WS_POPUPWINDOW определяется так: WS_POPUPWINDOW = WS_POPUP Or WS_BORDER Or WS_SYSMENU Это очень полезно, если учесть существование множества различных стилей. Многие стили точно вписываются в те виды окон, о которых говорится в сле­ дующем разделе. Стили, которые определяют общие характеристики окон Некоторые стили помогают определить общие характеристики окна (в про­ тивоположность специфическим характеристикам, таким как наличие кнопки Развернуть). Стили окон
24 Основы Перекрывающиеся окна Перекрывающееся (overlapped) окно – это высокоуровневое окно со стилем WS_OVERLAPPED или WS_OVERLAPPEDWINDOW. У окон со стилем WS_OVERLAPPED есть строка заголовка и рамка, а со стилем WS_OVERLAPPEDWINDOW – строка за­ головка, граница установки размера окна, оконное меню и кнопки Развернуть и Свернуть. Главным окном приложения является, как правило, перекрывающееся окно. Всплывающие окна Всплывающее (pop­up) окно – окно со стилем WS_POPUP. Это особые виды пере­ крывающихся окон, которые используются в качестве диалоговых окон, окон сообще­ ний и других временных окон, появляющихся вне главного окна приложения. Всплы­ вающее окно может иметь или не иметь строку заголовка. В остальном всплывающие окна не отличаются от перекрывающихся окон со стилем WS_OVERLAPPED. Окна-потомки Окно-потомок – это окно, которое имеет стиль WS_CHILD. Данный стиль дол­ жен быть задан при создании окна. У функций CreateWindow и CreateWindowEx есть параметры, которые используют для того, чтобы задать дескриптор родитель­ ского окна окну­потомку. Окно­потомок ограничено клиентской областью родительского окна. Оно обычно используется для разделения клиентской области родительского окна на функциональные зоны. Окно­потомок может иметь строку заголовка, меню окна, кнопки Развернуть и Свернуть, рамку, полосы прокрутки, но в нем отсутствует главное меню. Заметьте, что функция GetParent может использоваться для получения де­ скриптора родительского окна. Она будет задействована в примере, который вам предстоит выполнить. Стили оконных рамок Следующие несколько стилей определяют характеристики рамки окна:  WS_BORDER определяет окно с одинарной тонкой рамкой;  WS_DLGFRAME соответствует окну с двойной рамкой, обычно используется с диалоговыми окнами. Такое окно не может иметь строку заголовка;  WS_EX_DLGMODALFRAME определяет окно с двойной рамкой, возможно на­ личие строки заголовка (если окно имеет также стиль WS_CAPTION);  WS_EX_STATICEDGE соответствует окну с рамкой в объемном стиле. Пред­ назначено для использования с окнами, которые не принимают пользова­ тельского ввода;  WS_THICKFRAME определяет окно с рамкой, меняющей свой размер (sizing border). Стили, которые влияют на неклиентскую область окна Неклиентская область окна может включать строку заголовка, меню окна, кноп­ ки Развернуть и Свернуть, рамку с установкой размера, горизонтальные и верти­ кальные полосы прокрутки. Данной области соответствуют следующие стили:
25  WS_CAPTION используется при создании окна со строкой заголовка (вклю­ чает стиль WS_BORDER);  WS_HSCROLL определяет окно с горизонтальной полосой прокрутки;  WS_MAXIMIZEBOX применяется при создании окна с кнопкой Развернуть;  WS_MINIMIZEBOX определяет окно с кнопкой Свернуть;  WS_SYSMENU определяет наличие меню окна в строке заголовка;  WS_VSCROLL используется при создании окна с вертикальной полосой про­ крутки. Стили, которые влияют на начальное состояние окна Следующие стили определяют исходное состояние окна:  WS_DISABLED соответствует окну, которое в исходном состоянии заблоки­ ровано;  WS_MAXIMIZE представляет собой состояние, когда окно в исходном виде развернуто;  WS_MINIMIZE используется при создании окна, которое в исходном состо­ янии свернуто;  WS_VISIBLE определяет окно, которое в исходном состоянии является ви­ димым. Стили родителей и потомков Одно окно может являться потомком другого окна. Область отсечения (clipping region) окна представляет собой ту часть клиентской области окна, которую Windows в данный момент разрешает отображать. Существует два стиля, которые влияют на области отсечения:  WS_CLIPCHILDREN отсекает все окна­потомки от области изображения родительского окна;  WS_CLIPSIBLINGS отделяет окно­потомок от его братских окон. Когда одно из окон­потомков требует перерисовки, будет обновлена только его видимая часть, что предохраняет от перерисовки перекрывающее его братское окно. Области отсечения будут обсуждаться более подробно в главе 22. Расширенные стили Существует очень много расширенных (extended) стилей. Здесь рассматрива­ ются только два из них:  WS_EX_TOPMOST определяет, что окно переднего плана (topmost) должно быть размещено поверх всех окон непереднего плана (nontopmost), даже в том случае, когда активным является другое приложение. Конечно, одно окно переднего плана может перекрывать другие аналогичные окна. Напри­ мер, некоторые справочные системы позволяют пользователю определить их окна как окна переднего плана;  WS_EX _TOOLWINDOW определяет инструментальное окно (tool window), которое предназначено для использования в качестве плавающей инстру­ ментальной панели. Обсуждение стилей и возможностей их изменения продолжено в главе 17. Стили окон
2 Основы Подчиненные окна Перекрывающееся или всплывающее окно может быть подчинено другому такому же окну. Это несколько отличается от взаимосвязи родитель­потомок, так как в случае подчиненных окон оба окна должны быть перекрывающимися или всплывающими. В частности, окно­потомок не может быть ни окном­владельцем, ни подчиненным окном. Подчиненными окнами по умолчанию являются диало­ говые окна и окна сообщений. Перечислим основные характеристики подчиненного (owned) окна:  подчиненное окно всегда лежит поверх своего окна­владельца (owner) по Z­координате;  Windows автоматически удаляет подчиненное окно при уничтожении его окна­владельца;  если окно­владелец сворачивается, подчиненное окно становится скрытым (hidden). Функцию GetWindow можно использовать для получения дескриптора окна­ владельца подчиненного окна. Упорядоченность по Z-координате Упорядоченность по Z­координате (Z order) относится к расположению окон на экране. Ей определяется относительная позиция окна по отношению к вообра­ жаемой z­оси, которая выходит из правого угла монитора перпендикулярно экрану по направлению к пользователю, как показано на рис. 15.1 . Windows хранит расположение всех окон по Z­координате в одном списке, но этот список особым образом упорядочен. В частности, окна­потомки родительского окна всегда группируются вместе со своим родителем, и если положение родитель­ ского окна на Z­координате меняется, то вместе с ним меняется и положение всех его окон­потомков. На рис. 15.2 показано, что происходит, когда высокоуровневое окно Window2 перемещается на вершину Z­координаты. Оно передвигается вместе со своими потомками. Окна переднего плана (имеющие стиль WS_EX _TOPMOST), если такие в данный момент существуют, всегда появляются на вершине Z­координаты и поэтому всегда видимы, если только их не перекрывают другие окна переднего плана. Здесь дейс­ твует группировка родитель­потомок. При создании окно помещается на вершину Z­ координаты среди окон своего типа. Так, новое родительское окно помещается сра­ зу под окнами переднего плана, а новое окно­потомок – под его родительским окном, следовательно, выше любых других окон­ потомков данного родительского окна. Глубина = 1 Глубина = 2 Глубина = 3 x y z Рис. 15.1. Упорядоченность по Zкоординате
2 Обратите внимание, что такая схема перемещения придает окнам­потомкам одного родителя соответствующую упорядоченность по Z­координате. Существует несколько API­функций, которые связаны с упорядочением окон по Z­координате. Функция BringWindowToTop Функция BringWindowToTop перемещает окно на вершину Z­координаты среди окон данного типа: BOOL BringWindowToTop( HWND hWnd // Дескриптор окна. ); Если окно является высокоуровневым, то оно активизируется. Если окно является потомком, то его родительское высокоуровневое окно становится ак­ тивным и, следовательно, перемещается на вершину Z­координаты среди окон своего типа. Заметьте, что эта функция не изменяет тип окна, то есть она не переводит окно непереднего плана на передний план, и не помещает высокоуровневое окно поверх окон переднего плана, а только поверх других высокоуровневых окон. Функция SetWindowPos Функция SetWindowPos может использоваться для изменения размера окна, его позиции и положения на Z­координате, как это следует из ее декларации: BOOL SetWindowPos( HWND hWnd, // Дескриптор окна. HWND hWndInsertAfter, // Описатель порядка расположения. int x, // Положение по горизонтали. int y, // Положение по вертикали. int cx, // Ширина. int cy, // Высота. UINT uFlags // Флаги позиционирования окна. ); Окно Window2 сместилось в вершину Zкоординаты Вершина Zкоординаты Рис. 15.2 . Перемещение вверх по Zкоординате Упорядоченность по Z-координате
2 Основы Параметр hWndInsertAfter может принимать одно из следующих значений:  HWND_BOTTOM помещает окно в самом низу Z­координаты. Если окно явля­ лось окном переднего плана, оно теряет свой статус и помещается ниже всех остальных окон;  HWND_NOTOPMOST располагает окно выше всех окон непереднего плана и, следовательно, ниже всех окон переднего плана. На окно непереднего плана этот флаг не оказывает никакого воздействия;  HWND_TOP помещает окно на вершину Z­координаты среди окон данного типа. Если применяется к окну­потомку, то перемещает его высокоуровне­ вое родительское окно на вершину Z­координаты среди окон данного типа аналогично функции BringWindowToTop;  HWND_TOPMOST присваивает высокоуровневому окну атрибут окна переднего плана и помещает его на вершину Z­координаты. Это свойство будет исполь­ зоваться в некоторых из программ, демонстрируемых в данной книге. Функция GetTopWindow Функция GetTopWindow имеет следующий синтаксис: HWND GetTopWindow( HWND hWnd // Дескриптор родительского окна. ); Она проверяет только окна­потомки заданного родительского окна и извлекает дескриптор окна, которое находится выше других окон­потомков этого родитель­ ского окна по Z­оси. Если hWnd имеет значение NULL (0), тогда данная функция извлекает дескриптор того окна, которое находится на вершине Z­координаты по отношению ко всем окнам. Аналогичный смысл имеет функция GetNextWindow: HWND GetNextWindow( HWND hWnd, // Дескриптор текущего окна. UINT wCmd // Флаг направления. ); Она получает дескриптор окна, которое является следующим (нижним) или предшествующим (верхним) по Z­координате среди окон того же типа. Так, если hWnd ссылается на высокоуровневое (непереднего плана) окно, функция получа­ ет дескриптор соответствующего высокоуровневого окна, а если hWnd указывает на окно­потомок, то функция возвращает дескриптор окна­потомка (для одного и того же родительского окна). Если соответствующих окон не существует, фун­ кция возвращает NULL. Перечисление окон Win32 API предоставляет набор функций перечисления (enumeration), которые могут использоваться для перечисления различных объектов, включая окна. Давай­ те рассмотрим эти функции, так как они неоднократно используются в примерах данной книги (и уже встречались в приложении rpiEnumProcs).
2 Функции перечисления Ниже приводится несколько примеров функций перечисления:  EnumProcesses перечисляет идентификаторы для каждого существующе­ го в системе процесса;  EnumProcessesModules указывает дескрипторы каждого модуля для за­ данного процесса;  EnumWindows перечисляет высокоуровневые (top­level) окна;  EnumChildWindows перечисляет окна­потомки некоторого окна;  EnumThreadWindows указывает все ассоциированные с потоком окна, не являющиеся потомками;  EnumFonts перечисляет шрифты, доступные на заданном устройстве;  EnumFontFamilies указывает все доступные шрифты данного семейства шрифтов;  EnumObjects перечисляет перья или кисти, доступные заданному контек­ сту устройства;  EnumDateFormats выводит длинные и короткие форматы даты, которые доступны в данной локализации;  EnumDeviceDrivers указывает адреса загрузки каждого зарегистрирован­ ного в системе драйвера устройства;  EnumPrinterDrivers перечисляет все установленные на текущий момент драйверы принтеров;  EnumPorts выводит доступные для печати порты. Главный вопрос, связанный с указанными функциями, состоит в том, как вернуть результаты перечисления. Проблема возникает потому, что не существует способа узнать заранее, сколько данных требуют перечисления, поэтому переда­ вать функции структуру данных (типа массива) не очень практично. И все же некоторые функции перечисления вынуждают программиста пос­ тупать именно так. Сначала делается предположение о размере буфера для воз­ вращаемого значения, так как функции возвращают не только данные, но и коли­ чество байтов, необходимых для хранения этих данных. Если буфер, который вы выбрали, имеет недостаточный размер, то придется его увеличить и попробовать еще раз. Метод не очень изящен, но исправно работает. С другой стороны, некоторые функции перечисления «уклоняются» от своих обязанностей, сообщая отдельно о каждом перечисляемом элементе, а вы пытае­ тесь разобраться с этой информацией. Способ, с помощью которого функция выполняет описанное выше действие, заключается в вызове функции, предоставляемой вызывающей программой. Ее называют функцией обратного вызова (callback function). Используемый метод состоит в том, что функция перечисления будет вызы­ вать функцию обратного вызова по одному разу для каждого перечисляемого эле­ мента, передавая информацию, такую как дескриптор перечисляемого элемента, в параметрах функции обратного вызова. Это напоминает события VB – Windows возбуждает событие для каждого перечисляемого объекта. Перечисление окон
20 Основы Сигнатура (типы параметров и т.д.) функции обратного вызова должна быть предоставлена в документации, но за вами остается создание функции, передача ее адреса функции перечисления и подготовка кода, обрабатывающего информацию, возвращаемую в параметрах. Обычно функция перечисления проверяет возвраща­ емое значение функции обратного вызова, чтобы определить, что следует делать дальше – продолжать перечисление или завершить работу. К счастью, VB позволяет определять адрес функции обратного вызова с по­ мощью оператора AddressOf. Именно для этого данный оператор и был введен в Visual Basic. В качестве примера можно привести декларацию функции EnumWindows: BOOL EnumWindows( WNDENUMPROC lpEnumFunc, // Указатель на функцию обратного вызова. LPARAM lParam // Значение, определяемое приложением. ); Параметру lpEnumFunc должен быть присвоен адрес функции обратного вызова. Параметр lParam может определяться вызывающей программой и пе­ редаваться функции обратного вызова каждый раз, когда ее вызывает функция EnumWindows. Это значение используется в качестве счетчика. Функция EnumWindows может быть преобразована в VB в следующем виде: Declare Function EnumWindows Lib "USER32" ( _ ByVal lpFunct As Long, _ lParam As Long _ ) As Long где lParam передается по ссылке. Теперь объявим переменную­счетчик Dim c As Long за пределами функции обратного вызова (это важно) и передадим ее по ссылке в функцию EnumWindows. EnumWindows(AddressOf OurCallBack, c) В свою очередь, EnumWindows передает переменную c функции обратного вы­ зова, которая будет просто увеличивать значение счетчика на единицу. Поскольку каждый раз при вызове функция обратного вызова увеличивает значение внешней (external) по отношению к ней переменной c на единицу, то переменная c, после того как функция EnumWindows завершит свою работу, будет содержать общее количество перечисленных окон. Использование утилиты rpiEnumWins Архив примеров содержит исходный код утилиты rpiEnumWins, которая пе­ речисляет все окна и помещает информацию о каждом из них в управляющий элемент TreeView. На рис. 15.3 показано ее главное окно. Интересно наблюдать за окнами, создаваемыми Windows. Утилита имеет одно забавное свойство, которое заключается в том, что она изображает на экране, в точке текущего расположения окна, красный прямоугольник, независимо от того, видимо это окно или нет.
21 Эта утилита использует API­функцию EnumChildWindows, которая похожа на EnumWindows, но перечисляет не только высокоуровневые окна, но и окна­ потомки. Ее декларация представлена ниже: BOOL EnumChildWindows( HWND hWndParent, // Дескриптор родительского окна. WNDENUMPROC lpEnumFunc, // Указатель на функцию обратного вызова. LPARAM lParam // Значение, определяемое приложением. ); Давайте посмотрим, что говорится в документации о параметре hWndParent. Если этот параметр имеет значение NULL, то родительским является окно Ра­ бочего стола и данная функция перечисляет все высокоуровневые окна. Перечисление окон Рис. 15.3. Окно утилиты rpiEnumWins
22 Основы Но здесь есть некоторые противоречия, так как подразумевается, что родите­ лем всех высокоуровневых окон является окно Рабочего стола, что не соответс­ твует действительности. В любом случае эта функция перечисляет все высокоу­ ровневые окна независимо от того, что это означает на самом деле. Основную роль в утилите rpiEnumWins выполняет функция обратного вызова EnumChildProc. Здесь собираются все данные о каждом окне – его дескриптор, заголовок, имя класса, область окна (координаты окна на экране), идентифика­ торы процесса и потока. Для каждого окна создается новый узел в TreeView. Свойство узла Tag может иметь тип Variant, и, следовательно, хранить целый массив значений. Именно здесь и содержаться все данные об окнах. Function EnumChildProc(ByVal hwnd As Long, lParam As Long) As Long ' Эта функция вызывается Windows для каждого окнапотомка. ' ByVal необходимо для того, чтобы использовать hwnd. ' lParam может передаваться ByVal или ByRef, но должно совпадать ' с оператором Declare! Dim lret As Long Dim lTitleLen As Long Dim sWinTitle As String Dim sWinClass As String Dim hOwner As Long Dim s As String Dim uNode As Node Dim r As RECT Dim lThreadID As Long Dim lProcID As Long Dim bIsVisible As Boolean Dim bIsEnabled As Boolean ' Собираем данные об окне. lThreadID = GetWindowThreadProcessId(hwnd, lProcID) ' Проверяем фильтр. If glProcessFilter > 0 And glProcessFilter <> lProcID Then ' Продолжаем. EnumChildProc = True Exit Function End If If glThreadFilter > 0 And glThreadFilter <> lThreadID Then ' Продолжаем. EnumChildProc = True Exit Function End If lTitleLen = GetWindowTextLength(hwnd) + 1 sWinTitle = String$(lTitleLen, 0)
23 lret = GetWindowText(hwnd, sWinTitle, lTitleLen) sWinTitle = Left$(sWinTitle, lret) sWinClass = GetClass(hwnd) hOwner = GetWindow(hwnd, GW_OWNER) bIsVisible = IIf(IsWindowVisible(hwnd) = 0, False, True) bIsEnabled = IIf(IsWindowEnabled(hwnd) = 0, False, True) GetWindowRect hwnd, r ' Прибавляем единицу к счетчику окон. lParam = lParam + 1 s = HexFormat(hwnd) & " """ & sWinTitle & """ " & sWinClass ' Добавляем состояния Visible и Enabled. s = s & IIf(bIsVisible, " [V", " [NV") s = s & IIf(bIsEnabled, "/E]", "/NE]") ' Если не нуль, добавляем владельца. If hOwner <> 0 Then s = s & " [Owner: " & HexFormat(hOwner) & "]" End If ' Добавляем ID потока и процесса. s=s&"[Pr:"&lProcID&";Th:"&lThreadID&"]" Set uNode = frmEnumWins.trvWins.Nodes.Add( _ frmEnumWins.trvWins.Nodes(idxCurrentParent).Key, _ tvwChild, "Key" & Format$(lParam), s) ' Tag – это массив. uNode.Tag = Array(hwnd, sWinTitle, sWinClass, _ hOwner, r.Bottom, r.Left, r.Right, r.Top, lProcID, lThreadID) ' Продолжаем. EnumChildProc = True End Function Функции размера и положения В Win32 API существует множество функций, которые относятся к позици­ онированию и изменению размеров окон на экране. Честно говоря, они не очень интересны, но следует хотя бы познакомиться с некоторыми из них, чтобы впо­ следствии при необходимости можно было бы легко их изучить. Функция SetWindowPlacement Функция SetWindowPlacement устанавливает состояние визуализации (show state) и положения «восстановлено», «свернуто» и «развернуто» заданного окна: Функции размера и положения
24 Основы BOOL SetWindowPlacement( HWND hWnd, // Дескриптор окна. CONST WINDOWPLACEMENT *lpwndpl // Адрес структуры с данными // о положении. ); Здесь lpwndpl – это адрес структуры WINDOWPLACEMENT: typedef struct _WINDOWPLACEMENT { UINT length; // Размер данной структуры. UINT flags; UINT showCmd; POINT ptMinPosition; // Верхний левый угол свернутого окна. POINT ptMaxPosition; // Верхний левый угол развернутого окна. RECT rcNormalPosition; } WINDOWPLACEMENT; Обратите внимание, что поле length этой структуры должно быть заполнено до вызова SetWindowPlacement. Не будем обсуждать все возможные значения членов данной структуры, заметим только, что в число вероятных значений для showCmd входят SW_HIDE, SW_RESTORE, SW_SHOW, SW_SHOWMAXIMIZED, SW_ SHOWMINIMIZED и SW_SHOWNORMAL, которые достаточно понятны и без поясне­ ний. В Windows API довольно часто используются структуры POINT и RECT. Струк­ тура POINT определяется следующим образом: typedef struct tagPOINT { LONG x; LONG y; } POINT; Структура RECT декларируется так: typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT; Перевод этих структур на VB очень прост, так как типы VC++ UINT и LONG становятся типом VB Long. Единственная трудность заключается в том, что слово POINT является ключевым словом VB, поэтому нужно выбрать другое слово: Type POINTAPI x As Long y As Long End Type Type RECT Left As Long Top As Long Right As Long Bottom As Long
25 End Type Type WINDOWPLACEMENT Length As Long flags As Long showCmd As Long ptMinPosition As POINTAPI ptMaxPosition As POINTAPI rcNormalPosition As RECT End Type Теперь можно записать декларацию функции SetWindowPlacement: Declare Function SetWindowPlacement Lib "user32" Alias _ "SetWindowPlacement" ( _ ByVal hwnd As Long, _ lpwndpl As WINDOWPLACEMENT _ ) As Long Как вы могли догадаться, существует также и функция GetWindowPlacement, которая заполняет текущими установками окна структуру WINDOWPLACEMENT. Функция MoveWindow Функция MoveWindow изменяет положение и размер окна. Важно отметить, что позиция высокоуровневого окна определяется относительно левого верхнего угла экрана, в то время как позиция окна­потомка – относительно левого верхнего угла клиентской области родительского окна. Декларация этой функции выглядит так: BOOL MoveWindow( HWND hWnd, // Дескриптор окна. int x, // Позиция левой стороны окна (в пикселах). int y, // Позиция верхней части окна (в пикселах). int nWidth, // Ширина (в пикселах). int nHeight, // Высота (в пикселах). BOOL bRepaint // Флаг перерисовки. ); Таким образом функция объявляется в VB: Declare Function MoveWindow Lib "user32" Alias "MoveWindow" ( _ ByVal hwnd As Long, _ ByVal x As Long, _ ByVal y As Long, _ ByVal nWidth As Long, _ ByVal nHeight As Long, _ ByVal bRepaint As Long _ ) As Long Флаг bRepaint должен быть установлен в значение True, чтобы Windows перерисовывала окно при его перемещении или при изменении его размеров. К сожалению, всем параметрам, передаваемым функции, должны быть присво­ ены значения, поэтому даже если требуется изменить только размер окна, нужно Функции размера и положения
2 Основы вычислить текущее положение окна, чтобы присвоить соответствующие значения передаваемым параметрам положения. Это может быть сделано с помощью фун­ кции GetWindowRect. Функция SetWindowPos О функции SetWindowPos уже говорилось ранее в этой главе. Ее можно ис­ пользовать для изменения размера окна, его расположения на экране и положения на Z­координате: BOOL SetWindowPos( HWND hWnd, // Дескриптор окна. HWND hWndInsertAfter, // Описатель порядка расположения. int x, // Положение по горизонтали. int y, // Положение по вертикали. int cx, // Ширина. int cy, // Высота. UINT uFlags // Флаг позиционирования окна. ); Обратите внимание, что соответствующей функции GetWindowPos не су­ ществует. Функции GetWindowRect и GetClientRect Функция GetWindowRect извлекает прямоугольник окна (windows’s rectangle), или ограничивающий прямоугольник (bounding rectangle), в который точно вписы­ вается все окно (и клиентская, и неклиентская области). Его размеры опреде­ ляются экранными координатами (screen coordinates), то есть координатами (в пикселах) относительно верхнего левого угла экрана. Функция декларируется следующим образом: BOOL GetWindowRect( HWND hWnd, // Дескриптор окна. LPRECT lpRect // Адрес структуры Rect, которую заполняет функция. ); Функция GetClientRect делает то же самое, но для клиентской области окна: BOOL GetClientRect( HWND hWnd, // Дескриптор окна. LPRECT lpRect // Адрес структуры с координатами клиентской области. ); Вы можете подумать, что координаты, возвращаемые структурой RECT, долж­ ны быть оконными координатами (window coordinates), то есть координатами относительно верхнего левого угла окна, но в данном случае это не так. Основное назначение этой функции – определение размера клиентской области, поэтому ее задачу можно упростить, возвращая клиентские координаты (client coordinates), то есть координаты относительно самой клиентской области. Следовательно, ко­ ординаты верхнего левого угла всегда будут (0,0).
2 Невежество правит миром На минутку задержимся на следующем фрагменте кода: SetWindowPos Text1.hwnd, HWND_TOP, 0, 0, 2, 2, SWP_SHOWWINDOW Or _ SWP_NOMOVE GetWindowRect Text1.hwnd, r Debug.Print r.Top, r.Bottom, r.Left, r.Right Он устанавливает значение высоты и ширины текстового поля в два пиксела. Результат этого примера таков: 46 48 17 19 Можно было бы ожидать, что значения Top и Bottom будут отличаться толь­ ко на единицу (см. рис. 15.4), так как текстовое поле имеет высоту два пиксела. Однако функция GetWindowRect возвращает координаты (r.Right, r.Bottom) пиксела, который находится ниже и справа от окна, как показано на рис. 15.4 . Следовательно, этот пиксел не является частью окна. Верхний левый угол (r.Left, r.Top) Правый нижний угол (r.Right, r.Bottom) Прямоугольник размером 2x2 Рис. 15.4 . Определение размеров прямоугольника Это сделано специально, чтобы можно было применять следующие уравнения: r.Height = r.Bottom – r .Top r.Width = r.Right – r .Left По­моему, это очевидный пример невежества – сознательно возвращать непра­ вильные значения свойств Bottom и Right только для того, чтобы не заставлять программистов учить правильные формулы: r.Height = r.Bottom – r .Top + 1 r.Width = r.Right – r.Left + 1 Функции ClientToScreen и ScreenToClient Функции ClientToScreen и ScreenToClient могут использоваться для преобразования между клиентскими и экранными координатами. Выглядят они Функции размера и положения
2 Основы одинаково, обе принимают структуру POINT в качестве входных данных и заме­ няют ее исходные значения преобразованными. Следовательно, структура POINT является параметром типа IN/OUT. Декларации функций представлены ниже: BOOL ClientToScreen( HWND hWnd, // Дескриптор окна, координаты которого нужно // преобразовать. LPPOINT lpPoint // Указатель на структуру, содержащую экранные // координаты. ); BOOL ScreenToClient( HWND hWnd, // Дескриптор окна, координаты которого нужно // преобразовать. LPPOINT lpPoint // Указатель на структуру, содержащую координаты. ); Данные функции встречаются в нескольких утилитах, используемых в при­ мерах данной книги.
Глава 16. Сообщения Windows Операционная система Windows использует сообщения (messages) для передачи данных в окна и из окон. Сообщения могут быть классифицированы так:  сообщения, генерируемые Windows в ответ на пользовательский ввод с кла­ виатуры или от мыши;  сообщения, генерируемые приложением, например с помощью функции SendMessage. Сообщение состоит из четырех частей:  дескриптор, идентифицирующий окно (target window), которому предна­ значено сообщение;  идентификатор сообщения (message identifier), 32­разрядная величина типа Long;  два 32­разрядных числа, называемых параметрами сообщения (message para­ meters). Эти четыре части отчетливо видны в декларации функции SendMessage: LRESULT SendMessage( HWND hWnd, // Дескриптор окнаприемника сообщения. UINT Msg, // Передаваемое сообщение. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam // Второй параметр сообщения. ); Я насчитал почти 1000 различных сообщений, и уверен, что это еще не предел. Эти сообщения можно сгруппировать в несколько основных категорий:  сообщения, имеющие отношение к типам управляющих элементов, – стати­ ческий элемент (static), окно со списком (listbox), комбинированный элемент (combo box), кнопка (button), элемент редактирования (edit), полоса прокрут­ ки (scroll bar), представление в виде дерева (treeview), представление в виде списка (listview), панель инструментов (toolbar), строка состояния (statusbar), индикатор выполнения (progressbar), всплывающая подсказка (tooltip), уп­ равляющие стрелки (updown), табуляторный элемент (tab control);  сообщения мыши;  сообщения клавиатуры;  сообщения буфера обмена;  сообщения диалоговых окон;  сообщения MDI;  другие.
20 Сообщения Windows Со стороны Microsoft было очень любезно создать символьные константы для идентификации сообщений. Например, в табл. 16.1 приведены 45 идентификато­ ров сообщений, относящихся к управляющему элементу «окно со списком». По именам сообщений можно заранее определить, что делают многие из них. Таблица 16.1. Сообщения окна списка Идентификаторы LB_ADDFILE LB_GETITEMHEIGHT LB_SELECTSTRING LB_ADDSTRING LB_GETITEMRECT LB_SELITEMRANGE LB_CTLCODE LB_GETLOCALE LB_SELITEMRANGEEX LB_DELETESTRING LB_GETSEL LB_SETANCHORINDEX LB_DIR LB_GETSELCOUNT LB_SETCARETINDEX LB_ERR LB_GETSELITEMS LB_SETCOLUMNWIDTH LB_ERRSPACE LB_GETTEXT LB_SETCOUNT LB_FINDSTRING LB_GETTEXTLEN LB_SETCURSEL LB_FINDSTRINGEXACT LB_GETTOPINDEX LB_SETHORIZONTALEXTENT LB_GETANCHORINDEX LB_INITSTORAGE LB_SETITEMDATA LB_GETCARETINDEX LB_INSERTSTRING LB_SETITEMHEIGHT LB_GETCOUNT LB_ITEMFROMPOINT LB_SETLOCALE LB_GETCURSEL LB_MSGMAX LB_SETSEL LB_GETHORIZONTALEXTENT LB_OKAY LB_SETTABSTOPS LB_GETITEMDATA LB_RESETCONTENT LB_SETTOPINDEX Любое окно основано на классе окна (window class). Каждый такой класс связан с оконной процедурой (window procedure), называемой также оконной функцией (window function). Она должна быть создана программистом VC++. Программисту VB можно об этом не задумываться. Оконная процедура вызывается специальной системой для уведомления окна о сообщении. Следовательно, данная процедура представляет собой процедуру обратного вызова. Ее типичный вид показан ниже. Параметры, представляющие собой описанные ранее четыре части сообщения, заполняются системой. // Оконная процедура класса окна. LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, _ LPARAM lParam) { switch(iMsg) // Действие основано на идентификаторе окна. { // Обрабатываем все сообщения об изменении размера. case WM_SIZE: // Здесь размещается код для обработки сообщения. return 0; // Обрабатываем сообщение об уничтожении. case WM_DESTROY:
21 // Здесь размещается код для обработки сообщения. return 0; } // Просим Windows вызвать оконную процедуру по умолчанию. return DefWindowProc(hwnd, iMsg, wParam, lParam); } Сообщения обрабатываются на уровне потока. Иными словами, система находит поток, владеющий окном, которому предназначено сообщение, и передает сообщение этому потоку. Поток определяет, какая оконная процедура должна быть вызвана и «просит» систему сделать это путем вызова функции DispatchMessage. Пос­ ледовательность данных действий станет понятнее после их подробного рассмот­ рения. Очереди сообщений потока Каждый поток имеет несколько очередей сообщений, которые используются для постановки в очередь входящих сообщений, то есть сообщений, посланных системой (в ответ на действия пользователя или вызов другим потоком функции SendMessage). Существует четыре вида очереди (см. рис. 16.1):  очередь асинхронных сообщений, то есть полученных с помощью API­фун­ кции PostMessage;  очередь синхронных сообщений, то есть полученных с помощью API­функ­ ции SendMessage (и аналогичных ей);  очередь межпоточных сообщений, то есть полученных в ответ на синхрон­ ные сообщения (асинхронные сообщения не требуют ответа); Структура THREADINFO (Недокументировано) Указатель на очередь асинхронных сообщений Указатель на очередь синхронных сообщений Указатель на очередь межпоточных сообщений Очередь асинхронных (posted) сообщений Очередь синхронных (sent) сообщений Очередь межпоточных (reply) собщений Виртуальная очередь ввода Указатель на виртуальную очередь ввода Флаги пробуждения Переменные состояния локального ввода Рис. 16.1. Очередь сообщений потока Очереди сообщений потока
22 Сообщения Windows  виртуальная очередь ввода – очереди сообщений, которые являются следс­ твием аппаратного ввода, например, с клавиатуры или от мыши. Специальная структура, называемая THREADINFO, хранит указатели на очереди вместе с некоторой дополнительной информацией, о которой будет рассказывать­ ся в этой главе. Однако следует отметить, что данная структура недокументирова­ на. Единственный источник информации, из которого я узнал о ней, – это книга Джеффри Рихтера «Windows для профессионалов», третье издание Microsoft Press Русская Редакция. Таким образом, данная информация получена из вторых рук. Обычно я не включаю обсуждение недокументированных возможностей опе­ рационной системы в свои книги, но данная структура позволяет очень логично объединить излагаемую тему в одно целое, поэтому для нее сделано исключение. Система сообщений Windows На рис. 16.2 показана общая схема системы сообщений Windows. Давайте рассмотрим ее составляющие. Следует заметить, что документация ссылается просто на очередь сообщений потока. Полагаю, это сделано, чтобы не брать на себя ответственность за сущест­ вование раздельных асинхронных, синхронных, межпоточных очередей и вирту­ альной очереди ввода. В документации также подразумевается, что синхронные сообщения вообще не ставятся в очередь, а система немедленно вызывает оконную процедуру того окна, которому предназначено сообщение. Это соответствует действительности, когда функция SendMessage вызывается потоком, которому принадлежит окно­ приемник сообщения. В таком случае поток просто вызывает оконную процедуру окна­приемника так же, как и любую другую процедуру. Но если окно­приемник принадлежит внешнему потоку (в том же или ином процессе), то такое сообще­ ние должно быть помещено в очередь синхронных сообщений этого потока. (В 16­разрядной Windows синхронные сообщения никогда не ставились в очередь.) Доступ к очереди сообщений потока Как уже было сказано, сообщения генерируются системой (в ответ на действия пользователя – ввод данных с клавиатуры или при помощи мыши) и приклад­ ными программами. Сообщения ввода от аппаратуры, например, создаваемые клавиатурой или мышью, помещаются драйверами аппаратных средств в сис­ темную очередь сообщений. Оттуда их извлекает специальный системный поток, называемый потоком необработанного ввода (Raw Input Thread – RIT). RIT оп­ ределяет поток, которому принадлежит окно­приемник, и помещает сообщение в соответствующую очередь сообщений этого потока. Заметьте, что асинхронные и синхронные сообщения помещаются непосредственно в соответствующую очередь сообщений потока. Циклы обработки сообщений При создании приложения программист VC++ должен написать цикл обра­ ботки сообщений, который имеет следующий вид:
23 // Цикл обработки сообщений окна. while( GetMessage(&msg, hwnd, 0, 0) ) { TranslateMessage(&msg); DispatchMessage(&msg); } Сообщения от других аппаратных средств Ввод с клавиатуры Ввод от мыши Системная очередь сообщений Поток необработанного ввода (RIT) О ч е р е д ь а с и н х р о н н ы х с о о б щ е н и й О ч е р е д ь с и н х р о н н ы х с о о б щ е н и й О ч е р е д ь м е ж п о т о ч н ы х с о о б щ е н и й В и р т у а л ь н а я о ч е р е д ь в в о д а Межпоточная Внутрипоточная Межпоточная (ptr на пустую MSG...) (заполняет структуру MSG) (ptr на заполненную MSG) Структура MSG Оконная процедура Оконная процедура Оконная процедура Поток О ч е р е д ь а с и н х р о н н ы х с о о б щ е н и й О ч е р е д ь с и н х р о н н ы х с о о б щ е н и й О ч е р е д ь м е ж п о т о ч н ы х с о о б щ е н и й В и р т у а л ь н а я о ч е р е д ь в в о д а Рис. 16.2 . Система сообщений Windows Система сообщений Windows
24 Сообщения Windows Функция GetMessage декларируется таким образом: LRESULT GetMessage( LPMSG lpMsg, // Адрес структуры MSG, принимающей сообщение. HWND hWnd, // Дескриптор окна, сообщения для которого проверяем. UINT wMsgFilterMin, // Первое сообщение из допустимого диапазона. UINT wMsgFilterMax // Последнее сообщение из допустимого // диапазона. ); Функция извлекает сообщение из очередей сообщений данного потока в том случае, если оно предназначено для окна с дескриптором hWnd и номер этого сооб­ щения лежит в допустимом диапазоне между wMsgFilterMin и wMsgFilterMax. Данные параметры определяют способ фильтрации сообщений. Например, если wMsgFilterMin имеет значение WM_KEYFIRST, а wMsgFilterMax – WM_ KEYLAST, то работа производится только с сообщениями от клавиатуры. Установка двух фильтрующих параметров в нуль снимает все вышеназванные ограничения и разрешает извлекать все сообщения. После присвоения параметру hwnd значения NULL будут извлекаться все сообщения для этого потока. Затем функция GetMessage заполняет структуру lpMsg информацией о данном сообщении. Указанная структура определяется так: typedef struct tagMSG { HWND hwnd; // Дескриптор окнаприемника сообщения. UINT message; // Номер сообщения. WPARAM wParam; // Параметр, зависящий от сообщения. LPARAM lParam; // Параметр, зависящий от сообщения. DWORD time; // Время отправки сообщения. POINT pt; // Положение курсора во время отправки сообщения. } MSG; По завершении работы функция GetMessage возвращает ненулевое значение. Результирующее значение, равное нулю, может появиться только при сообщении WM_QUIT. Таким образом, цикл while будет продолжаться до тех пор, пока не будет получено сообщение о завершении (quit). Если функц ия GetMessage возвращает ненулевое значение, то функция TranslateMessage в случае необходимости выполняет некоторое преобразова­ ние, относящееся к клавиатурному вводу, а функция DispatchMessage запро­ сит Windows выполнить оконную процедуру класса, передавая ей параметры из заполненной структуры с сообщением MSG: LONG DispatchMessage( CONST MSG *lpMsg // Указатель на структуру с сообщением MSG. ); Заметьте, что DispatchMessage не завершает свою работу до тех пор, пока не будет обработано сообщение и не закончит выполнение оконная процедура. Однако возможна ситуация, когда функция, принадлежащая оконной процеду­ ре, пошлет сообщение, которое приведет к тому, что оконная процедура будет вызвана снова. Например, если оконная процедура вызывает функцию UpdateWindow, эта
25 функция посылает окну сообщение WM_PAINT, вызывая таким образом оконную процедуру, в то время как она продолжает обрабатывать сообщение, связанное с вызовом UpdateWindow. В этом случае обработка первого сообщения при­ останавливается до тех пор, пока не завершится обработка второго сообщения (WM_PAINT). Следовательно, оконная процедура должна быть реентерабельной (то есть спо­ собной к обработке повторных вызовов во время обработки предыдущих). Кроме этого может потребоваться, чтобы процедура сохраняла свое текущее состояние и могла его восстановить, чтобы продолжать выполнение с того момента, когда была остановлена. Более пристальный взгляд на GetMessage Когда сообщение помещается в очередь сообщений потока, система устанавли­ вает в структуре потока THREADINFO один или несколько флагов пробуждения (wake flags). Таким образом поток выводится из состояния ожидания и начинает обрабатывать поступившее сообщение. Он может также вызвать API­функцию GetQueueStatus для проверки типа сообщений, ожидающих обработки: DWORD GetQueueStatus( UINT flags // Флаги statusочереди ); Код этой процедуры в VB выглядит следующим образом: Declare Function GetQueueStatus Lib "user32" ( _ ByVal fuFlags As Long _ ) As Long В табл. 16.2 показаны различные флаги пробуждения и их значение. Вероятно, вы удивляетесь тому, как функция GetMessage определяет, в какой из нескольких возможных непустых очередей находится нужное сообщение. Со­ гласно утверждению Рихтера, GetMessage последовательно проверяет условия, описанные ниже. Этот порядок проверки демонстрирует приоритет, назначаемый различным типам сообщений; также объясняется, почему, например, синхронные сообщения обрабатываются раньше асинхронных: 1. Если установлен флаг QS_SENDMESSAGE, извлекается следующее синхрон­ ное сообщение из очереди синхронных сообщений и вызывается соответс­ твующая оконная процедура. Это повторяется до тех пор, пока синхронных сообщений больше не остается. Затем данный флаг сбрасывается. Во время этих действий функция GetMessage не завершает свою работу, таким об­ разом, не выполняется код, следующий за вызовом GetMessage. 2. Если установлен флаг QS_POSTMESSAGE, извлекается следующее асинхрон­ ное сообщение из очереди асинхронных сообщений и заполняется структу­ ра MSG (параметр, передаваемый в GetMessage). Если это единственное асинхронное сообщение, то флаг QS_POSTMESSAGE сбрасывается. В любом случае функция GetMessage возвращает ненулевое значение (кроме зна­ чения –1, которое зарезервировано для индикации ошибки. Система сообщений Windows
2 Сообщения Windows 3. Если установлен флаг QS_QUIT, заполняется структура MSG и флаг сбрасы­ вается. GetMessage возвращает FALSE (0). 4. Если установлен флаг QS_INPUT, заполняется структура MSG. Состояние всех флагов, связанных с вводом (QS_KEY, QS_MOUSEMOVE и т.д .) сбрасы­ вается, если в очереди ввода больше нет сообщений соответствующего типа. GetMessage возвращает ненулевое значение (кроме значения –1). 5. Если установлен флаг QS_PAINT, заполняется структура MSG. GetMessage возвращает ненулевое значение (кроме значения –1). 6. Если установлен флаг QS_TIMER, заполняется структура MSG. Таймер сбрасы­ вается. GetMessage возвращает ненулевое значение (кроме значения –1). Таблица 16.2 . Флаги пробуждения Значение Описание QS_ALLEVENTS Ввод WM_TIMER, WM_PAINT, WM_HOTKEY или асинхронное сообщение находятся в очереди. QS_ALLINPUT В очереди имеется какоето сообщение QS_ALLPOSTMESSAGE Асинхронное сообщение (иное, чем те, что перечислены здесь) в очереди асинхронных сообщений QS_HOTKEY В очереди стоит сообщение WM_HOTKEY QS_INPUT В очереди ввода находится сообщение ввода QS_KEY В очереди ввода имеются сообщения WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP или WM_SYSKEYDOWN QS_MOUSE В очереди ввода стоят сообщения WM_MOUSEMOVE или сообщения, посланные от мыши QS_MOUSEBUTTON В очереди ввода находится сообщение, посланное от мыши. QS_MOUSEMOVE В очереди ввода содержится сообщение WM_MOUSEMOVE QS_PAINT В очереди имеется сообщение WM_PAINT QS_QUIT В очереди находится сообщение WM_QUIT QS_POSTMESSAGE Асинхронное сообщение (иное, чем те, что перечислены здесь) находится в очереди асинхронных сообщений QS_SENDMESSAGE Сообщение, отправленное другим потоком или приложением, находится в очереди синхронных или межпоточных сообщений QS_TIMER В очереди содержится сообщение WM_TIMER Обратите внимание, что сообщение WM_PAINT не обрабатывается до тех пор, пока не будут обработаны все синхронные, асинхронные сообщения и сообщения ввода. Кроме того, если накапливается несколько сообщений, адресованных одно­ му и тому же окну, то они объединяются системой в одно сообщение. Синхронные и асинхронные сообщения Приведенное выше описание показывает, что все синхронные сообще­ ния обрабатываются ранее любых асинхронных сообщений (или сообщений других типов). Есть еще одно важное отличие между функциями SendMessage и PostMessage.
2 Функция PostMessage завершает свою работу немедленно, возвращая булевское значение, указывающее на успешное завершение или сбой процесса отправки сообщения (posting), но вызывающий поток не получает никакой ин­ формации о результате отправки сообщения или даже о том, обработал ли при­ нимающий поток отправленное сообщение. (Он мог завершиться и до обработки сообщения.) В отличие от функции PostMessage, SendMessage не завершает свою рабо­ ту (следовательно, вызывающий поток находится в состоянии ожидания) до тех пор, пока не будет обработано сообщение. Таким образом, SendMessage может вернуть вызывающему потоку информацию о результате обработки сообщения. Например, сообщение LB_GETTEXT получает текст одного из элементов окна списка, которому отправлено сообщение. Очевидно, что посылать сообщение LB_ GETTEXT с помощью функции PostMessage не имеет смысла. В действительности вызывающий поток должен выполнять только одно дейс­ твие во время ожидания возвращаемого значения от SendMessage – обрабаты­ вать поступающие к нему синхронные сообщения. Иначе принимающий поток мог бы послать сообщение вызывающему потоку, и в этом случае оба потока находи­ лись бы в состоянии ожидания ответа от другого потока. В результате возникла бы тупиковая ситуация (deadlock). Установка тайм-аута Если у вызывающего потока нет времени ждать, пока принимающий поток обработает его сообщение, можно вместо функции SendMessage использовать функцию SendMessageTimeout. Она декларируется следующим образом: BOOL SendMessageTimeout( HWND hWnd, // Дескриптор окнаприемника. UINT Msg, // Отправляемое сообщение. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam, // Второй параметр сообщения. UINT fuFlags, // Как посылать сообщение. UINT uTimeout, // Продолжительность таймаута. LPDWORD lpdwResult // Возвращаемое значение для синхронного вызова. ); Первые четыре параметра те же самые, что и у SendMessage. Параметр fuFlags может быть комбинацией следующих значений:  SMTO_ABORTIFHUNG завершает работу немедленно, если принимающий процесс не отвечает на внешние запросы («завис»);  SMTO_BLOCK запрещает вызывающему потоку обработку любых других сообщений, до того как функция завершит свою работу;  SMTO_NORMAL не запрещает вызывающему потоку обрабатывать другие сообщения в процессе ожидания завершения работы функции. Это обычная установка. Параметр uTimeout представляет собой время ожидания в миллисекундах. Пос­ ледний параметр содержит возвращаемую информацию и зависит от сообщения. Синхронные и асинхронные сообщения
2 Сообщения Windows Уведомляющие сообщения У функции SendNotifyMessage такая же сигнатура, что и у SendMessage: BOOL SendNotifyMessage( HWND hWnd, // Дескриптор окнаприемника. UINT Msg, // Отправляемое сообщение. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam // Второй параметр сообщения. ); но ее поведение отличается в зависимости от того, где находится принимающее окно – в другом потоке или нет. Если принимающее окно принадлежит вызыва­ ющему потоку, то функция ведет себя точно так же, как и SendMessage. Однако если принимающее окно принадлежит другому потоку, функция завершает работу немедленно, не дожидаясь получения информации о результатах обработки сооб­ щения. Отличие от PostMessage состоит в том, что синхронное сообщение имеет приоритет выше, чем асинхронное. Основная задача функции SendNotifyMessage – отправка уведомляющих сообщений (notification message). Это сообщения, которые уведомляют адресата о некотором действии и не требуют от отправителя, чтобы тот дожидался ответа. Они являются самыми обычными, и Windows часто их использует. Например, сообщения WM_ACTIVATE, WM_DESTROY, WM_SIZE, WM_MOVE относятся к уведом­ ляющим. Кроме того, как упоминалось ранее, порожденные окна часто посылают уведомляющие сообщения своему родителю, информируя его о действиях, отно­ сящихся к его потомку. Пример отправки сообщений управляющему элементу Listbox Есть множество оснований использовать сообщения при создании программ, взаимодействующих с операционной системой на более низком уровне, чем обыч­ но позволяет VB. Одна из основных причин – это больший контроль за управля­ ющими элементами. Например, Visual Basic не предоставляет возможности ни устанавливать позиции табуляции в управляющем элементе «окно со списком» (listbox), ни добавлять к нему горизонтальную полосу прокрутки. Тем не менее обе эти задачи легко решаются при помощи функции SendMessage. Установка позиций табуляции Для установки позиций табуляции в окне со списком нужно просто послать предназначенное ему сообщение LB_SETTABSTOPS. Согласно документации, оно имеет следующие параметры:  wParam указывает количество устанавливаемых позиций табуляции;  lParam представляет собой указатель на первый элемент целочисленного массива (точнее, элементы типа VB Long), содержащего позиции табуляции
2 в единицах шаблона диалогового окна. Позиции табуляции должны быть упорядочены в возрастающем порядке. Функция SendMessage для этих сообщений возвращает True, если позиции табуляции установлены корректно. В документации также упоминается, что для того, чтобы использовать сообщение LB_SETTABSTOPS, окно списка должно быть создано со стилем LBS_USETABSTOPS. А в VB окна со списком ThunderListbox имеют именно такой стиль. В документации также утверждается, что если cTabs равно нулю и lpnTabs имеет значение NULL, у окна списка по умолчанию позиции табуляции будут на расстоянии двух единиц шаблона диалогового окна. Более того, если cTabs рав­ но единице, позиции табуляции, установленные по умолчанию, будут находить­ ся на расстоянии, определяемом lpnTabs. Если массив, на который ссылается lpnTabs, имеет больше одного значения, позиции табуляции будут установлены для каждого значения массива. Единицы шаблона диалогового окна, которые использует данное сообщение, яв­ ляются аппаратно­независимыми единицами измерения. Для перевода единиц шаб­ лона диалогового окна в пикселы можно использовать функцию MapDialogRect. Эти единицы составляют около одной четверти от средней ширины символа, и обычно этого достаточно и для прикидки, и для настройки. На рис. 16.3 в качестве примера представлен результат действия сообщения LB_SETTABSTOPS. Для его реализации вам потребуются декларации для Send Message и LB_SETTABSTOPS: Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As Long, _ ByVal lMsg As Long, _ wParam As Any, _ lParam As Any _ ) As Long Public Const LB_SETTABSTOPS = &H192 Рис. 16.3. Установка позиций табуляции в окне со списком Отправка сообщений элементу Listbox
300 Сообщения Windows В листинге 16.1 приведен исходный код, в котором посылается это сооб­ щение. Листинг 16.1. Использование SendMessage для установки позиций табуляции Public Function SetTabstopsExample() Dim i As Integer ' Устанавливаем позиции табуляции только в List2. Dim Tabstops(1 To 4) As Long Tabstops(1) = 4 * 10 ' Приблизительная ширина 10 символов. Tabstops(2) = Tabstops(1) + 4 * 8 Tabstops(3) = Tabstops(2) + 4 * 6 Tabstops(4) = Tabstops(3) + 4 * 4 SendMessage List2.hwnd, LB_SETTABSTOPS, 4, Tabstops(1) ' Заполняем окна со списком. Fori=1To4 List1.AddItem "Item1" & vbTab & "Item2" & _ vbTab & "Item3" & vbTab & "Item4" & vbTab & "Item5" List2.AddItem "Item1" & vbTab & "Item2" & _ vbTab & "Item3" & vbTab & "Item4" & vbTab & "Item5" Next End Function Установка горизонтальной протяженности В документации говорится, что сообщение LB_SETHORIZONTALEXTENT при­ ложение посылает для установки ширины (в пикселах), на которую окно со спис­ ком может прокручиваться в горизонтальном направлении (ширина прокрутки). Если ширина окна со списком меньше этого значения, то такую прокрутку эле­ ментов списка окна обеспечивает полоса горизонтальной прокрутки. Если ширина окна со списком равна этому значению или превышает его, горизонтальная полоса прокрутки становится невидимой. Параметр данного сообщения wParam указывает значение ширины прокрут­ ки в пикселах. В Windows 9x эта величина ограничена 16­разрядным значением, а lParam равен нулю. Возвращаемое значение отсутствует. Ниже представлена декларация для LB_SETHORIZONTALEXTENT: Public Const LB_SETHORIZONTALEXTENT = &H194 Следующий исходный код создает окно со списком, показанное на рис. 16.4: List1.AddItem "I would I could quit all offenses with as clear " & _ "excuse as well as I am doubtless I can purge " & _ "myself of many I am charg'd withal." SendMessage List1.hWnd, LB_SETHORIZONTALEXTENT, 500&, 0&
301 Извлечение данных управляющего элемента Listbox Время от времени я сталкивался с необходимостью извлечения данных из окна со списком или комбинированного окна другого приложения. Например, во время написания книг по программированию Microsoft Access, Word и Excel, а также при создании надстроек Visual Basic мне нужно было получать список свойств или ме­ тодов различных объектов из соответствующих объектных моделей. На рис. 16.5 по­ казана справочная система Visual Basic для Microsoft Word, с открытым диалоговым окном Topics Found (Найденные разделы) для свойств (properties) объекта Range. Требовалось извлечь список этих 65 свойств, для того чтобы изучать их более внимательно, чем прокручивая их в маленьком окошке. На рис. 16.5 показано так­ же приложение rpiControlExtractor с извлеченным списком свойств. Поскольку этот список находится в моем собственном окне, я могу спокойно с ним работать. (Возможно, это приложение следовало назвать Control Data Extractor, так как оно Рис. 16.4 . Установка горизонтальной протяженности окна со списком Рис. 16.5 . Control Extractor за работой Отправка сообщений элементу Listbox
302 Сообщения Windows извлекает не управляющие элементы, а данные из управляющих элементов, но я выбрал более короткое название.) Приложение Control Extractor может извлекать данные из управляющих элементов Listbox (окно со списком), Combobox (комбинированное окно), ListView (представление в виде списка). Listbox и Combobox созданы в старом стиле, так как существовали еще в 16­разрядной Windows, тогда как ListView – это новый 32­разрядный управляющий элемент. Вы увидите, что это имеет боль­ шое значение для извлечения данных из внешнего управляющего элемента (то есть находящегося в другом процессе). Нам придется отложить обсуждение программы, которая извлекает данные из управляющего элемента ListView другого процесса до главы 20. Процесс по­ лучения данных из комбинированных окон очень похож на аналогичный процесс для окон со списком, позже будет рассмотрен и он. (Исходный текст программы rpiControlExtract включает код для всех трех типов управляющих элементов.) Для того чтобы извлечь данные из управляющего элемента Listbox, нужно послать ему сообщение LB_GETTEXT. А чтобы послать это сообщение, следует при­ своить параметру сообщения wParam отсчитанный от нуля индекс извлекаемого элемента, а параметру lParam – адрес буфера памяти, в который помещается ука­ занный элемент. Именно буфер памяти является причиной некоторых проблем. В листинге 16.2 приведена программа, которая выполняется при нажатии пользователем командной кнопки Listbox (Список) группы Extract From (По­ лучить из). Подразумевается, что дескриптор данного управляющего элемента Listbox находится в глобальной переменной hControl. Листинг 16.2 . Извлечение содержания управляющего элемента Listbox Sub ExtractFromListBox() Dim cItems As Integer Dim i As Integer Dim sBuf As String Dim cBuf As Long Dim lResp As Long ' Получаем количество элементов из управляющего элемента. cItems = SendMessageByNum(hControl, LB_GETCOUNT, 0&, 0&) ' Выводим количество элементов в виде надписи. lblItems = "Items: " & cItems If cItems <= 0 Then Exit Sub ' Очистить предварительно окно со списком? If chkClearList.Value = 1 Then lstMain.Clear ' Помещаем элементы в окно со списком. Fori=0TocItems1 ' Получаем длину элемента. cBuf = SendMessageByString(hControl, LB_GETTEXTLEN, CLng(i), 0)
303 ' Выделяем буфер для хранения элемента списка. sBuf = String$(cBuf + 1, " ") ' Посылаем сообщение для извлечения элемента. lResp = SendMessageByString(hControl, LB_GETTEXT, CLng(i), sBuf) ' Добавляем элемент к локальному окну со списком. If lResp > 0 Then lstMain.AddItem Left$(sBuf, lResp) End If Next i lstMain.Refresh End Sub Маршаллинг между процессами Тема взаимосвязи между процессами является одной из наиболее важных для тех из нас, кто стремится стать хакером. Именно эта взаимосвязь позволяет проникать в посторонний процесс для того, чтобы посылать команды, отправлять и получать данные. Судя по описанию процедуры ExtractFromListbox, можно сказать, что сообщение LB_GETTEXTLEN проходит через границы процессов потому, что единс­ твенными передаваемыми данными является возвращаемое значение самой функции. Это значение возвращается через стек вызывающего процесса. В частности, такая ситуация не требует создания буфера памяти. С другой стороны, сообщению LB_GETTEXT необходим адрес буфера памяти. Вызывающий процесс может выделить память под буфер только из своей области памяти, а не из адресного пространства процесса, которому принадлежит управ­ ляющий элемент Listbox. Но оконная процедура другого процесса полагает, что адрес в параметре lParam относится к ее собственному адресному пространству (то есть к единственному адресному пространству, о котором она имеет инфор­ мацию), поэтому она будет пытаться поместить извлеченный элемент списка по указанному адресу в своем адресном пространстве. Все выглядит так, как будто ситуация должна завершиться нарушением доступа (access violation). В любом случае извлеченный элемент данных не может быть возвращен вызывающему про­ цессу. Тем не менее программа работает, а элемент из окна со списком помещается в буфер вызывающего процесса! Это объясняется тем, что Windows отслеживает некоторые сообщения, такие как LB_GETTEXT, и может перевести (marshall) данные (здесь элемент списка) из адресного пространства процесса, которому принадлежит окно со списком, в ад­ ресное пространство вызывающего процесса. Маршаллинг (marshalling) представ­ ляет собой упаковку данных и их пересылку через границы процессов. Подобное часто случается при OLE­автоматизации. Маршаллинг между процессами
304 Сообщения Windows Нет полной ясности в вопросе о том, к каким сообщениям Windows автома­ тически применяет маршаллинг, поэтому в каждом конкретном случае нужна экспериментальная проверка. Например, для буферов сообщений, посланных старорежимным 16­разрядным управляющим элементам, таким как окно со спис­ ком или комбинированное окно, маршаллинг осуществляется автоматически, для того чтобы поддерживать совместимость с 16­разрядной Windows, где никакого маршаллинга не было и в помине (он был просто не нужен). Однако Windows не выполняет автоматически маршаллинг данных, извлечен­ ных из нового 32­разрядного управляющего элемента ListView, так как в этом случае нет необходимости обеспечивать совместимость. В приложении Contol Extractor придется использовать другой подход. Нужно найти какой­то способ вы­ деления памяти в другом процессе и копирования данных в этот процесс и из него, что потребуется при создании программы rpiControlExtractor для извлечения дан­ ных из принадлежащего другому процессу управляющего элемента ListView. Выделение памяти другого процесса – это самая трудная часть задачи. Хотя в Windows NT данная процедура не так сложна, потому что там реализована фун­ кция VirtualAllocEx, которая специально предназначена для этой цели: LRVOID VirtualAllocEx( HANDLE hProcess, // Процесс, в котором выделяется память. LPVOID lpAddress, // Требуемый начальный адрес выделяемой // памяти. DWORD dwSize, // Размер выделяемой области в байтах. DWORD flAllocationType, // Тип выделения. DWORD flProtect // Тип защиты от доступа. ); К сожалению, данная функция отсутствует в Windows 9x. Придется преодо­ леть значительно большие трудности, чтобы выделять память таким способом, который будет работать в любой версии Windows. Как уже упоминалось, это рас­ сматривается в главе 20. Копирование данных между процессами После того как память в другом процессе выделена, можно обратиться к од­ ному из нескольких способов копирования данных между процессами. Кажется довольно курьезным тот факт, что Microsoft обеспечила простой способ копиро­ вания данных между процессами, но не предоставила столь же простого способа выделения памяти другого процесса для хранения этих данных. Давайте рассмот­ рим некоторые из методов копирования. Один из способов передавать данные между процессами состоит в том, чтобы использовать функции WriteProcessMemory и ReadProcessMemory. Ниже представлена декларация функции WriteProcessMemory: BOOL WriteProcessMemory( HANDLE hProcess, // Дескриптор процесса, в память которого // идет запись. LPVOID lpBaseAddress, // Начальный адрес записываемой памяти.
305 LPVOID lpBuffer, // Указатель на буфер для записываемых данных. DWORD nSize, // Количество записываемых байтов. LPDWORD lpNumberOfBytesWritten // Тип защиты от доступа. ); Заметьте, что дескриптор внешнего процесса должен иметь специальный доступ к этому внешнему процессу – PROCESS_VM_WRITE и PROCESS_VM_OPERATION. Такой дескриптор может быть получен с помощью функции OpenProcess, кото­ рая обсуждалась в главе 11. Декларация функции ReadProcessMemory выглядит так: BOOL ReadProcessMemory( HANDLE hProcess, // Дескриптор процесса, из памяти которого // идет чтение. LPCVOID lpBaseAddress, // Начальный адрес читаемой памяти. LPVOID lpBuffer, // Указатель на буфер для читаемых данных. DWORD nSize, // Количество читаемых байтов. LPDWORD lpNumberOfBytesRead // Адрес количества прочитанных байтов. ); В случае обращения к ReadProcessMemory дескриптор должен иметь тип доступа к внешнему процессу PROCESS_VM_READ. Другой способ передать данные внешнему процессу заключается в том, чтобы ис­ пользовать функцию SendMessage для отсылки сообщения WM_COPYDATA. В этом случае параметр wParam является дескриптором внешнего окна (окна во внешнем процессе), а параметр lParam – указателем на структуру COPYDATASTRUCT: typedef struct tagCOPYDATASTRUCT { DWORD dwData; DWORD cbData; PVOID lpData; } COPYDATASTRUCT; Параметр dwData может быть любым 32­разрядным значением, cbData – это количество посылаемых байтов; а lpData – указатель на буфер в вызывающем процессе, который содержит для передачи cbData байт. Ниже представлена корректная форма записи SendMessage: SendMessage(hTargetWindow, WM_COPYDATA, hSendingWindow, _ Address of COPYDATASTRUCT) Отправка этого сообщения приведет к тому, что Windows создаст буфер во внешнем процессе, поместит туда данные и присвоит lpData значение указателя на этот буфер. (Буфер, конечно же, не является внешним для оконной процедуры окна, которому посылается сообщение.) Вы познакомитесь с примером использования этого сообщения в главе 19, когда будет говориться о ловушках. А пока необходимо запомнить следующее:  данное сообщение нужно посылать только с помощью функции SendMessage, но не PostMessage. Причина в том, что Windows высвобождает память, выделенную под буфер во внешнем процессе, как только сообщение будет Копирование данных между процессами
30 Сообщения Windows обработано, а в процедуре PostMessage не предусмотрено ожидание окон­ чания процесса обработки сообщения;  данные в буфере (как и dwData) не должны содержать указателей по той простой причине, что не станет выполняться маршаллинг, следовательно, они будут недоступны для внешнего процесса;  принимающий процесс должен определять извлекаемые данные как дан­ ные «только для чтения». Последний параметр функции SendMessage, а именно указатель на копию структуры COPYDATASTRUCT во внешнем про­ цессе, действителен только на время обработки сообщения. Принимающий процесс не должен освобождать эту память. Кроме того, если ему требуется сохранить эти данные, он должен скопировать их в другое место. Состояние локального ввода В Win32 каждый поток в системе представляет собой своего рода виртуальный компьютер. Таким образом, любой поток должен считать, что клавиатура, мышь, монитор, другие аппаратные средства принадлежат только ему. Чтобы это положение выполнялось, каждому потоку предоставляется его собс­ твенное локальное состояние ввода (local input state) в виде части его структуры THREADINFO, как показано на рис. 16.1 . Локальное состояние ввода состоит из следующих данных, относящихся к фокусу ввода, клавиатуре и мыши:  текущее активное окно данного потока;  окно, которому принадлежит фокус ввода с клавиатуры (keyboard input focus), или просто фокус ввода (input focus);  текущее состояние клавиатуры (например, нажата ли клавиша Alt или вклю­ чен режим CAPSLOCK);  текущее состояние знака вставки (caret), растрового изображения, которое отмечает позицию точки вставки;  окно, которому в текущий момент принадлежит захват мыши (mouse capture), то есть окно, получающее сообщения от мыши. Это не обязательно то окно, которое находится под указателем мыши;  текущий курсор, растровое изображение, которое отмечает положение ука­ зателя мыши и его видимость. Важно понять, что представления активного окна и фокуса ввода существуют независимо для потока. Иными словами, каждый поток имеет окно, которое он рассматривает как активное, и окно, владеющее, как он считает, фокусом ввода. С другой стороны, пользователь представляет компьютер как единое целое, а не как совокупность отдельных виртуальных компьютеров. Для него в каждый момент времени только одно окно является активным и только одно окно имеет фокус ввода. Приоритетный поток В любой данный момент времени поток необработанного ввода направляет ввод с клавиатуры и от мыши в виртуальную очередь ввода одного из потоков.
30 Тот поток, который в данный момент времени принимает входные сообщения, называется приоритетным (foreground). Когда пользователь переключает прило­ жения, RIT перенаправляет вывод различным потокам, таким образом делая их по очереди приоритетными потоками. Можно использовать для переключения приоритетного потока API­функцию SetForegroundWindow. Давайте вспомним, что говорилось в главе 11 об изменении поведения фун­ кции SetForegroundWindow. В Windows 95 и Windows NT 4 данная функция делает приложение приоритетным. Однако в Windows 98 и Windows 2000 только главное окно приложения становится активным для этого потока и строка за­ головка начинает мигать. А функция rpiSetForegroundWindow разработана, чтобы восстановить исходную функциональность SetForegroundWindow. Ввод с клавиатуры Для каждого потока в любой момент времени существует максимум одно окно, которое будет обрабатывать сообщения от клавиатуры, если этот поток станет при­ оритетным. Это окно имеет фокус ввода для данного потока. Фокус ввода может быть изменен вызовом функции SetFocus. Дескриптор окна, которому прина­ длежит текущий фокус ввода, может быть получен с помощью вызова функции GetFocus. Следует еще раз подчеркнуть, что фокус ввода у каждого потока свой. Пример клавиатурного ввода будет рассматриваться позже в этой главе. Захват мыши Ввод данных от мыши более сложен, чем ввод с клавиатуры. Причина в том, что операция перетаскивания (drag) мышью начинается в одном месте экрана, а кончается в другом, и эти две точки вовсе не обязаны находиться над окнами одного и того же потока. Что касается фокуса мыши, то для каждого потока есть три его возможных состояния:  захват мыши отсутствует;  захват мыши на уровне потока;  захват мыши на уровне системы. В нормальных условиях (нет нажатых кнопок и некорректно работающих программ) ни одно окно не располагает захватом мыши. В этом случае любые сообщения от мыши помещаются в очередь ввода потока, владеющего тем окном, над которым находился курсор мыши в момент генерации данного сообщения, и обрабатываются затем указанным окном. Другое дело, каким образом обрабатывается сообщение. В VB для обработки сообщений от мыши потребуется ввести необходимый код в события MoseDown, MouseUp или MouseMove. В VC++ нужно поместить код в соответствующую оконную процедуру. Захват мыши может осуществляться на двух уровнях: на уровнях системы (syste­wide) и потока (thread­wide). В VB при щелчке по управляющему элементу ему передается захват мыши на уровне системы до того момента, когда вы отпускаете нажатую клавишу. Это Состояние локального ввода
30 Сообщения Windows объясняется тем, что щелчок по управляющему элементу может оказаться подго­ товительной операцией к перетаскиванию (dragging) указателя мыши (как при рисовании), и тогда исходное окно должно получать все сообщения мыши включи­ тельно до того момента, когда будет отпущена клавиша мыши, что служит призна­ ком завершения процедуры перетаскивания. В VC++ нужно запрашивать захват мыши путем вызова функции SetCapture в момент нажатия кнопки мыши. VB делает это за программиста. Функция SetCapture может также использоваться для передачи окну за­ хвата мыши, но только на уровне потока. Если мышь перемещается над окном другого процесса, то оно начнет получать сообщения от мыши. Но как только мышь возвратится к любому окну, принадлежащему вызвавшему SetCapture потоку, то все сообщения мыши снова будет получать окно, которому передан захват мыши. Чтобы сбросить режим захвата мыши для потока, нужно вызвать функцию ReleaseCapture. Активное окно и окно переднего плана Как вам уже известно, Windows устанавливает для своих окон отношения родитель­потомок (parent­child). Окна, у которых нет родителей, называются высокоуровневыми (top­level). О высокоуровневом окне каждого потока – роди­ теле окна, которому принадлежит фокус ввода с клавиатуры, – говорят, что оно является активным окном данного потока. Окно переднего плана (foreground window) представляет собой активное окно приоритетного потока, то самое окно, которое обычно имеет темно­синюю строку заголовка в качестве отличительного признака по сравнению с други­ ми окнами. К сожалению, в документации данный термин часто используется вместо термина «активное окно». Это касается, например, вкладки Appearance (Оформление) апплета Display (Экран), находящегося на панели Control Panel (Панель управления). Однако различать активное окно и окно переднего плана очень важно. Итак, функция SetFocus может применяться только в пределах данного по­ тока. Таким образом, ее нельзя использовать для переключения приложений, то есть для установки приложения переднего плана. В Windows 95 и Windows NT 4 этого можно добиться с помощью функции SetForegroundWindow, так как она изменяет окно переднего плана и, следовательно, перенаправляет ввод от аппа­ ратуры тому потоку, которому принадлежит это окно. Однако, как было сказано выше, ее поведение меняется в Windows 98 и Windows 2000. Эксперименты Лучшим способом исследования системы сообщений Windows и изучения того, как она управляет пользовательским вводом, являются эксперименты. Дан­ ный раздел включает три программы, с помощью которых вы увидите, что разные окна в одно и то же время могут иметь фокус ввода и захват мыши; определите, что каждый поток имеет свой собственный фокус ввода; исследуете различие между захватом мыши на уровнях потока и системы.
30 Эксперимент 1 Чтобы убедиться в том, что окна с фокусом ввода и захватом мыши могут быть разными, вы можете написать следующий простой проект VB. Создайте форму с одной командной кнопкой и двумя текстовыми полями. Добавьте код, приведен­ ный в листинге 16.3. Листинг 16.3. Окно с фокусом ввода и захватом мыши Option Explicit Private Declare Function SetCapture Lib "user32" (ByVal hwnd As Long) _ As Long Private Declare Function SetFocusAPI Lib "user32" Alias "SetFocus" _ ByVal hwnd As Long) As Long Private Declare Function GetActiveWindow Lib "user32" () As Long Private Sub Command1_Click() ' Устанавливаем фокус ввода для Text1. SetFocusAPI Text1.hwnd ' Устанавливаем захват мыши для Text2. SetCapture Text2.hwnd Debug.Print GetActiveWindow = Me.hwnd End Sub Private Sub Text2_MouseMove(Button As Integer, Shift As Integer, _ X As Single, Y As Single) Text2 = X End Sub Теперь запустите проект и щелкните по командной кнопке. Затем подви­ гайте мышью и одновременно наберите что­нибудь на клавиатуре. Вы долж­ ны увидеть изменяющиеся числа в поле Text2 (Текст2) и добавляемые в поле Text1 (Текст1) символы, которые вводятся с клавиатуры. Это показывает, что поле Text1 имеет фокус ввода, а Text2 – захват мыши (и то, и другое на уровне данного потока). Эксперимент 2 Выполните следующий эксперимент. Он демонстрирует то, что каждый поток имеет свой собственный фокус ввода. Создайте проект VB с формой, кото­ рая включает в себя одно текстовое поле и один управляющий элемент – таймер (см. рис. 16.6). Рис. 16.6 . Демонстрация фокуса ввода Состояние локального ввода
310 Сообщения Windows Установите свойство таймера Interval в значение 1000. Добавьте к форме код из листинга 16.4 . Листинг 16.4 . Программа, демонстрирующая, что каждый поток имеет свой собственный фокус ввода Option Explicit Private Declare Function SetFocusAPI Lib "user32" Alias "SetFocus" _ (ByVal hwnd As Long) As Long Private Declare Function GetActiveWindow Lib "user32" () As Long Private Sub Timer1_Timer() ' Устанавливаем фокус ввода для Text1. SetFocusAPI Text1.hwnd ' Отображаем дескриптор активного окна. Text1 = GetActiveWindow End Sub Теперь повторите это еще для одного проекта VB, переименовав заголовок формы в Form2, чтобы отличить ее от формы первого проекта. Выполните обе программы. Через одну секунду в обоих текстовых полях появится мерцающий курсор. Это показывает, что каждое текстовое поле имеет фокус ввода, но разных потоков. (Рис. 16.6 отображает ситуацию недостаточно корректно.) Более того, каждый поток имеет свое активное окно, так как оба текстовых поля отображают разные дескрипторы. Но как видно по темно­синей строке заголовка, только одна из двух форм может быть окном переднего плана. Эксперимент 3 Захват мыши демонстрирует проект с одной формой, как на рис. 16.7 . Помес­ тите на форму таймер и установите для свойства Interval значение 250. Полный исходный текст для данного проекта приведен в листинге 16.5 . Листинг 16.5 . Захват мыши на уровнях системы и потока Option Explicit Private Declare Function SetCapture Lib "user32"(ByVal hwnd As Long) _ As Long Private Declare Function GetCapture Lib "user32"() As Long Private Declare Function ReleaseCapture Lib "user32"() As Long Private Sub Command1_Click() ' Устанавливаем захват мыши для Text1. SetCapture Text1.hwnd End Sub Private Sub Command2_Click() ReleaseCapture End Sub
311 Private Sub Form_MouseMove(Button As Integer, Shift As Integer, _ X As Single, Y As Single) Text1=X+Y End Sub Private Sub Text1_MouseMove(Button As Integer, Shift As Integer, _ X As Single, Y As Single) Text1=X+Y End Sub Private Sub Timer1_Timer() txtCapture = GetCapture End Sub Если вы теперь будете перемещать мышь над формой, значение в поле Text1 (верхнее текстовое поле) будет меняться, указывая на то, что форма обрабатывает сообщения WM_MOUSEMOVE. Однако функция GetCapture выводит нуль, это оз­ начает, что ни одному окну не принадлежит захват мыши. Поэтому значение в Text1 не меняется, когда указатель мыши проходит над командной кнопкой, над одним из текстовых полей или когда перемеща­ ется за пределами формы. Это обычное (при отсутс­ твии захвата) функционирование мыши. Нажмите левую кнопку мыши в тот момент, ког­ да ее указатель находится над формой, и подвигайте указатель по экрану, особенно над окнами, принадле­ жащими другим приложениям. Заметьте, что текс­ товое поле Text2 отображает дескриптор формы, а в Text1 заносится значение X + Y независимо от того, где перемещается указатель. Следовательно, VB передал форме захват мыши на уровне системы. Наконец, щелкните по командной кнопке. Это приводит к тому, что в результа­ те вызова GetCapture текстовому полю Text1 передается захват мыши на уровне потока. Снова подвигайте указатель мыши по экрану, в том числе над окнами дру­ гих приложений. Пока указатель мыши находится над любым из окон текущего проекта VB (потока), значения в Text1 меняются, но как только указатель мыши переходит в область окон других приложений, изменения в Text1 прекращаются, так как на них действие захвата мыши на уровне потока не распространяется. Рис. 16.7. Демонстрация захвата мыши Состояние локального ввода
Глава 17. Классы окон и процесс создания окна Вы видели, как операционная система взаимодействует с окнами, используя со­ общения. В этой главе подробно рассматривается сущность самих окон. Давайте поговорим о классах окон и о том, как окно реально создается. Данный процесс должен понимать каждый программист VC++. А вместо программиста VB всем этим занимается Visual Basic. И все же профессионалы должны иметь некоторое представление об этих вопросах, для того чтобы программировать Windows API. Чтобы получить более глубокое представление о том, что такое окно и как оно работает, следует кратко рассмотреть выполнение программы на VC++, которая создает класс окна и окно, базирующееся на этом классе. Не стремитесь понять каждую строку программы – наша задача сформировать общее представление. Основные этапы создания окна в VC++ выглядят следующим образом (хотя немного изменен порядок): 1. Определить класс окна. 2. Зарегистрировать класс. 3. Подготовить оконную процедуру для данного класса. 4. Создать окно на основе данного класса. 5. Подготовить цикл обработки сообщений для созданного окна. Классы окон Каждое окно является экземпляром (instance) класса окна (window class). В та­ ком смысле окна являются объектно­ориентированными. Создать класс окна не сложно. Начнем с объявления переменной типа WNDCLASS. Это структура со сле­ дующим определением: typedef struct WNDCLASS { UINT style; // Стиль окна. WNDPROC lpfnWndProc; // Оконная процедура. int cbClsExtra; int cbWndExtra; HANDLE hInstance; // Экземпляр дескриптора процесса. HICON hIcon; // Значок окна. HCURSOR hCursor; // Курсор мыши окна. HBRUSH hbrBackground; // Цвет фона окна. LPCTSTR lpszMenuName; // Меню класса окна. LPCTSTR lpszClassName; // Имя класса. };
313 Данная структура определяет свойства класса окна, и, следовательно, каждого окна, которое основано на этом классе. В частности, в ней есть члены, которые задают значок, курсор мыши, цвет фона и меню класса. Последняя составляющая нужна для того, чтобы дать классу имя. Фактически наиболее важными членами этой структуры являются имя класса и оконная процедура. Заметьте, что свойства любого окна можно изменять или дополнять. Свойства класса окна действуют только в качестве начальной установки. Ниже представлен фрагмент реальной программы VC++: WNDCLASS wndclass; // Определяем класс окна. wndclass.style = CS_HREDRAW | CS_VREDRAW wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = NULL; wndclass.cbWndExtra = NULL; wndclass.hInstance = hInstance; wndclass.hIcon = NULL; wndclass.hCursor = NULL; wndclass.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = TEXT("rpiClass1"); Член style определяется как объединение (логическое OR) двух значений. (Вертикальная черта является оператором OR в VC++.) Стиль CS_HREDRAW требу­ ет от Windows перерисовать окно, если изменялась его ширина, аналогично дейс­ твует стиль CS_VREDRAW. Таким образом, все окна, созданные на основе данного класса, будут иметь указанные свойства. Следующий шаг – зарегистрировать класс в Windows. Это можно сделать с помощью функции RegisterClass, которая принимает указатель на структуру WNDCLASS регистрируемого класса (& означает операцию взятия адреса): // Регистрируем класс. RegisterClass(&wndclass); Предопределенные классы окон Windows определяет несколько классов, которые можно использовать для создания окон:  Button (кнопка);  Combobox (комбинированное окно);  Edit, Textbox (текстовое поле);  Listbox (окно со списком);  MDIclient и окно клиента MDI (приложение с многодокументным интер­ фейсом);  RichEdit, версия 1.0 (текстовое поле с расширенными возможностями);  RichEdit_Class, версия 2.0 (текстовое поле с расширенными возможнос­ тями);  Scrollbar (полоса прокрутки); Предопределенные классы окон
314 Классы окон и процесс создания окна  Static (метка, прямоугольник или линия, используемые для надписей, обрамления рамкой или разделения других управляющих элементов). Уп­ равляющие элементы Static не принимают входных данных и не выводят результатов, из­за чего и называются статическими. Оконная процедура класса окна Ранее уже говорилось о том, что сообщения обрабатываются оконной процеду­ рой класса окна. Стоит подчеркнуть, что оконная процедура принадлежит классу окна, а не конкретным окнам. И поэтому все окна, созданные на основе некоторого класса, используют одну и ту же оконную процедуру. Далее приводится пример оконной процедуры, которая отслеживает только два типа сообщений: сообщение WM_DESTROY, которое посылается окну при его уничтожении, и сообщение WM_SIZE, отправляемое окну после того, как Windows изменит его размеры (возможно в ответ на действия мышью). // Оконная процедура для данного класса. LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { // Временная строка. char temp[50]; switch(iMsg) // Похоже на конструкцию VB Select Case. { // Обрабатываем все сообщения об изменении размера. case WM_SIZE: _ itoa( (lParam & 0x0000FFFF) , temp, 10 ); SetWindowText(hwnd, temp); return 0; // Обрабатываем сообщение об уничтожении. case WM_DESTROY: // Генерируем сообщение quit. PostQuitMessage(0); return 0; } // Просим Windows вызвать оконную процедуру по умолчанию. return DefWindowProc(hwnd, iMsg, wParam, lParam); } Если поступает сообщение WM_DESTROY, оконная процедура посылает сооб­ щение WM_QUIT, завершая работу потока, создавшего это окно (и, соответствен­ но, приложения, если оно состоит из одного потока). Если приходит сообщение WM_SIZE, она считывает ширину клиентской области окна (это часть окна, не включающая заголовок или рамки), которая возвращается в 16­ти младших битах параметра lParam, и помещает ее в заголовок окна. Следует подчеркнуть, что программисты редко вызывают оконную процедуру напрямую. Оконная процедура представляет собой функцию обратного вызова и
315 вызывается Windows для того, чтобы передать информацию о сообщении (де­ скриптор окна, ID сообщения, wParam и lParam) конкретному приложению для обработки. Обратите внимание, что последнее действие приведенной в примере оконной процедуры – это вызов оконной процедуры по умолчанию, обеспечивающей обра­ ботку по умолчанию всех тех сообщений, которые не задействованы первой окон­ ной процедурой (в данном случае всех сообщений, за исключением WM_DESTROY и WM_SIZE. Оператор VC++ return завершает выполнение оконной процедуры). Конечно, оконные процедуры большинства приложений будут значительно более сложными и, возможно, включать код, выполнение которого зависит от того, какое из окон принимает сообщение (это определяет параметр hwnd). В действительнос­ ти для большинства приложений Windows самое главное действие происходит в оконной процедуре. Создание окна После определения и регистрации класса на его основе можно создать окно. Это осуществляется с помощью функции CreateWindow: HWND CreateWindow( LPCTSTR lpszClassName, // Указатель на зарегистрированное имя // класса. LPCTSTR lpWindowName, // Указатель на имя окна. DWORD dwStyle, // Стиль окна. int x, // Положение окна по горизонтали. int y, // Положение окна по вертикали. int nWidth, // Ширина окна. int nHeight, // Высота окна. HWND hWndParent, // Дескриптор родительского окна или окна // владельца. HWND hMenu, // Дескриптор меню или идентификатор окна // потомка. HANDLE hInstance, // Дескриптор экземпляра приложения. LPVOID lpParam // Указатель на данные для создания окна. ); Обратите внимание, что параметры этой функции очень напоминают парамет­ ры структуры WNDCLASS. Функция CreateWindow требует указания имени класса для создаваемого окна и устанавливает различные его свойства, например начальное положение. Имя окна используется в качестве заголовка тех окон, у которых он есть (напри­ мер, окон приложений и командных кнопок). Функция CreateWindow возвращает дескриптор вновь созданного окна: // Создаем окно. hwnd = CreateWindow( wndclass.lpszClassName, // Имя класса окна. TEXT("rpi.Window"), // Имя окна. WS_OVERLAPPEDWINDOW | WS_VSCROLL, // Стиль окна. Создание окна
31 Классы окон и процесс создания окна CW_USEDEFAULT, // Исходное положение окна слева. CW_USEDEFAULT, // Исходное положение окна сверху. CW_USEDEFAULT, // Исходная ширина окна. CW_USEDEFAULT, // Исходная высота окна. NULL, // Дескриптор родительского окна. NULL, // Дескриптор меню окна. hInstance, // Дескриптор экземпляра процесса. NULL, // Параметры создания окна. ); Стили окон Как вам известно, каждое окно имеет стиль, являющийся комбинацией из сти­ ля, который определен для данного класса окна, и установок параметра dwStyle функции CreateWindow. Параметр dwStyle может быть комбинацией несколь­ ких констант стиля, причем как общего характера, так и специальных, приме­ няемых к конкретным типам окон, таким как командная кнопка. Далее следует несколько примеров констант стиля общего характера:  WS_BORDER создает окно с рамкой в виде тонкой линии;  WS_CAPTION формирует окно со строкой заголовка (включает стиль WS_ BORDER);  WS_CHILD создает окно­потомок;  WS_DLGFRAME формирует окно со стилем рамки, обычно использующемся в диалоговых окнах;  WS_HSCROLL создает окно с полосой горизонтальной прокрутки;  WS_MAXIMIZE формирует окно, изначально развернутое на весь экран;  WS_MAXIMIZEBOX создает окно с кнопкой Развернуть;  WS_SYSMENU формирует окно с оконным меню в строке заголовка. Кроме того, каждый тип предопределенного класса имеет свои стили. Напри­ мер, большинство стилей кнопок приведены далее:  стили, используемые при создании флажка (checkbox): – BS_AUTO3STATE. Трехпозиционный флажок, изменяющий свое состояние при выделении пользователем; – BS_CHECKBOX. Флажок с текстом;  стили создания переключателя (radio button, option button): – BS_RADIOBUTTON. Маленький кружок с текстом (радиокнопка); – BS_AUTORADIOBUTTON. Переключатель, который Windows автоматичес­ ки переводит в состояние «установлен» (checked) и так же автоматически переводит все остальные переключатели группы в состояние «сброшен» (unchecked);  стили создания командной кнопки (command button, push button): – BS_DEFPUSHBUTTON. Командная (нажимная) кнопка, которая аналогична командной кнопке стиля BS_PUSHBUTTON, но является кнопкой по умол­ чанию (может быть выбрана нажатием клавиши Enter); – BS_PUSHBUTTON. Командная кнопка;
31  стили, используемые при размещении текста: – BS_LEFTTEXT. Размещает текст с левой стороны переключателя или флажка; – BS_BOTTOM. Размещает текст в нижней части прямоугольной кнопки; – BS_CENTER. Центрирует текст прямоугольной кнопки по горизонтали; – BS_MULTILINE. Переносит текст кнопки на несколько строк; – BS_VCENTER. Центрирует текст прямоугольной кнопки по вертикали;  другие стили: – BS_GROUPBOX. Прямоугольник, в котором могут быть сгруппированы другие управляющие элементы; – BS_OWNERDRAW. Пользовательская кнопка (управляющий элемент, вид которого определяет программист); – BS_BITMAP. Кнопка в виде растровой картинки (bitmap); – BS_ICON. Кнопка в виде значка (icon); – BS_TEXT. Кнопка в виде текста. Обратите внимание на следующее: то, что производит впечатление разных управляющих элементов – командные кнопки, флажки и переключатели – в дейс­ твительности одни и те же кнопки с разными стилями. Изменение стиля окна Функцию SetWindowLong можно использовать для задания стиля окна после того, как оно создано. Бывает так, что после создания кнопки одни стили могут быть результативно изменены, другие – нет (результат или отсутствует, или вы­ зывает аварийное завершение). Только на конкретном примере можно убедиться, что изменение стиля будет работать. Далее приведено несколько тестовых примеров. Нужно только помес­ тить на форму командную кнопку и два текстовых поля: Dim lStyle As Long ' Надпись командной кнопки, выровненная по нижнему краю. lStyle = GetWindowLong(Command1.hwnd, GWL_STYLE) SetWindowLong Command1.hwnd, GWL_STYLE, lStyle Or BS_BOTTOM ' Текстовое поле переводит все входные данные в нижний регистр. lStyle = GetWindowLong(Text1.hwnd, GWL_STYLE) SetWindowLong Text1.hwnd, GWL_STYLE, lStyle Or ES_LOWERCASE ' Текстовое поле принимает только цифры. lStyle = GetWindowLong(Text2.hwnd, GWL_STYLE) SetWindowLong Text2.hwnd, GWL_STYLE, lStyle Or ES_NUMBER Главное, на что надо обратить внимание в этих примерах – для того чтобы внести необходимые изменения, нужно сначала определить текущий стиль. Было бы ошибкой просто установить какой­нибудь стиль окна, скажем, BS_BOTTOM, поскольку это отменило бы все другие установки. Управляющие элементы Windows и VB
31 Классы окон и процесс создания окна Управляющие элементы Windows и VB Управляющими элементами Visual Basic являются окна. У прежних управляю­ щих элементов были имена классов, начинающиеся со слова Thunder, поскольку это рабочее кодовое название Visual Basic 1.0 . Обратите внимание, что некоторые управляющие элементы времени проектирования отличаются от соответствую­ щих элементов времени выполнения, и, следовательно, могут иметь разные имена классов. Имена классов управляющих элементов времени выполнения в качестве основы используют имена классов времени проектирования, но добавляют ссылку на версию VB. Например, имя класса управляющего элемента Listbox времени проектирования – ThunderListbox, в то время как имя класса управляющего элемента Listbox времени выполнения в VB5 – ThunderRT5Listbox, а VB6 – ThunderRT6Listbox. В табл. 17.1 приведены имена классов времени проектиро­ вания для некоторых распространенных управляющих элементов VB. Таблица 17.1. Управляющие элементы и имена классов времени проектирования Управляющий элемент Имя класса Check ThunderCheckBox Combo ThunderComboBox Command ThunderCommandButton Dir ThunderDirListBox Drive ThunderDriveListBox File ThunderFileListBox Form ThunderForm Frame ThunderFrame Label ThunderLabel List ThunderListBox MDIForm ThunderMDIForm Option ThunderOptionButton Picture ThunderPictureBox Scroll (Horiz) ThunderHScrollBar Scroll (Vert) ThunderVScrollBar Text ThunderTextBox Timer ThunderTimer TabStrip TabStrip20WndClass Toolbar msvb_lib_toolbar ProgressBar ProgressBar20WndClass StatusBar StatusBar20WndClass TreeView TreeView20WndClass ListView ListView20WndClass ImageList ImageList20WndClass Slider Slider20WndClass
31 Пример слежения за окнами Приложение rpiSpyWin, полный исходный код которого находится в архиве примеров, – это небольшая утилита для получения информации о конкретном окне. На рис. 17.1 показано главное (и единственное) окно данной программы. Она извлекает дескриптор, имя класса, заголовок, стили, положение, идентификаторы окна, процесса и потока для любого отображаемого окна. Идентификатор окна, или ID, – это число, передаваемое функции CreateWindow и идентифицирующее окно­потомок среди его братских окон. ID порой бывает полезен при вызове API­ функций, поэтому я включил его в выходные данные. Рис. 17.1. Окно утилиты слежения за окнами Утилита использует преимущества (на системном уровне), которые создает Windows при перетаскивании окна мышью. Значит, для того чтобы начать работу rpiSpyWin, просто нажмите левую кнопку мыши, установив курсор над красным прямоугольником, и начинайте двигать мышь. Когда найдете интересующее вас окно, всего лишь отпустите кнопку. Если при этом на экране остаются следы, про­ ведите указателем мыши над красным прямоугольником еще раз. Почти все программные действия связаны с событием MouseMove поля, на котором изображен красный прямоугольник. Private Sub picSpy_MouseMove(Button As Integer, Shift As Integer, _ x As Single, y As Single) Dim xValue As Long, yValue As Long Dim pt As POINTAPI ' Преобразуем X и Y в экранные координаты (пикселы). ' X и Y измеряются в твипах относительно верхнего левого угла ' перетаскиваемого окна. ' Top и Left измеряются в твипах относительно клиентской области формы. xValue = (x + picSpy.Left) \ Screen.TwipsPerPixelX yValue = (y + picSpy.Top) \ Screen.TwipsPerPixelY pt.x = xValue pt.y = yValue ClientToScreen Me.hwnd, pt txtX="X = "&pt.x&"Y = "&pt.y 'txtY = pt.y Пример слежения за окнами
320 Классы окон и процесс создания окна ' Получаем дескриптор окна по положению мыши. hCurrent = WindowFromPoint(pt.x, pt.y) If hCurrent <> hPrevious Then ' Изменяем окно. txtHandle = "&H" & Hex$(hCurrent) & " (" & hCurrent & ")" ' Получаем имя класса. txtClass = GetClass(hCurrent) ' Получаем заголовок. txtCaption = GetCaption(hCurrent) ' Получаем стиль. txtStyle = "&H" & Hex$(GetWindowLong(hCurrent, GWL_STYLE)) ' Получаем расширенный стиль. txtEXStyle = "&H" & Hex$(GetWindowLong(hCurrent, GWL_EXSTYLE)) ' Получаем ID окна. txtID = GetWindowLong(hCurrent, GWL_ID) ' Получаем прямоугольник. lretSpy = GetWindowRect(hCurrent, rectCurrent) ' Инвертируем рамку предыдущего прямоугольника. ' Верхняя граница. rectTemp = rectPrev rectTemp.Bottom = rectPrev.Top + PEN_WIDTH InvertRect hDCScreen, rectTemp ' Нижняя граница. rectTemp = rectPrev rectTemp.Top = rectPrev.Bottom  PEN _WIDTH InvertRect hDCScreen, rectTemp ' Левая граница. rectTemp = rectPrev rectTemp.Right = rectPrev.Left + PEN_WIDTH InvertRect hDCScreen, rectTemp ' Правая граница. rectTemp = rectPrev rectTemp.Left = rectPrev.Right  PEN _WIDTH InvertRect hDCScreen, rectTemp ' Инвертируем рамку нового прямоугольника. ' Верхняя граница. rectTemp = rectCurrent rectTemp.Bottom = rectCurrent.Top + PEN_WIDTH InvertRect hDCScreen, rectTemp ' Нижняя граница. rectTemp = rectCurrent rectTemp.Top = rectCurrent.Bottom  PEN _WIDTH
321 InvertRect hDCScreen, rectTemp ' Левая граница. rectTemp = rectCurrent rectTemp.Right = rectCurrent.Left + PEN_WIDTH InvertRect hDCScreen, rectTemp ' Правая граница. rectTemp = rectCurrent rectTemp.Left = rectCurrent.Right  PEN _WIDTH InvertRect hDCScreen, rectTemp ' Обновляем предыдущие окно и прямоугольник. hPrevious = hCurrent rectPrev = rectCurrent End If End Sub Первое, что необходимо сделать – это преобразовать входящие координа­ ты мыши, которые измеряются относительно верхнего левого угла изображения прямоугольника, в координаты относительно верхнего левого угла клиентской области основной формы. Кроме того, твипы1 (twips) должны быть преобразованы в пикселы, которые использует большинство API­функций. Затем следует пере­ вести клиентские координаты в экранные. По экранным координатам в пикселах с помощью функции WindowFromPoint можно получить дескриптор окна, находящегося под указателем мыши. Если он изменился с момента последнего перемещения мыши, извлекаются и помещаются в текстовые поля имя класса окна, заголовок и другие данные. Функция GetClass является, по сути, всего лишь контейнером для API­функции GetClassName: Function GetClass(lhwnd As Long) As String ' Возвращает имя класса окна lhwnd. Dim lret As Long Dim sClassName As String GetClass = "Не могу получить имя класса." aClassName = String$(256, 0) lret = GetClassName(lhwnd, sClassName, 257) Iflret=0Then Exit Function Else GetClass = Left$(sClassName, lret) End If End Function 1 Твип – единица длины, приблизительно составляющая 1/1440 дюйма. – Прим. науч. ред. Пример слежения за окнами
322 Классы окон и процесс создания окна Функция GetCaption использует API­функции GetWindowTextLength и GetWindowText: Function GetCaption(lhwnd As Long) As String Dim sText As String Dim lCaption As Long Dim hnd As Long lCaption = GetWindowTextLength(lhwnd) ' Выделяем буфер строки. sText = String$(lCaption + 2, 0) lCaption = GetWindowText(lhwnd, sText, lCaption + 1) ' include NULL GetCaption = Left$(sText, lCaption) End Function Для извлечения стиля используется функция GetWindowLong. Функция GetWindowRect определяет размеры окна. В частности, она запол­ няет структуру rect: Type rect Left As Long Top As Long Right As Long Bottom As Long End Type Самая интересная часть приложения – это использование API­функции InvertRect, которая инвертирует пикселы заданного прямоугольника, выполняя операцию логического отрицания для каждого пиксела. Таким образом, следующая инверсия возвращает пикселы в их исходное состояние. Когда дескриптор окна изменяется, процедура возвращает предыдущий прямоугольник в исходное состо­ яние и инвертирует новый прямоугольник, который окружает новое окно. Обратите внимание, что InvertRect преобразует все пикселы внутри прямоугольника, по­ этому нужно было сделать четыре маленьких прямоугольника для представления всех сторон окна, находящегося под указателем мыши. Кстати, можно применять InvertRect в программе rpiPEInfo для слежения за импортируемыми функциями коммерческой утилиты захвата экрана. Следует упомянуть еще одно обстоятельство. Окну rpiWinSpy присвоен при­ оритет переднего плана с помощью API­функции SetWindowPos. Это означает, что оно будет располагаться поверх всех остальных окон (не считая других окон переднего плана) даже в том случае, когда окно теряет фокус.
Глава 18. Модификация класса окна В следующих нескольких главах будут обсуждаться более сложные темы, вклю­ чая модификацию классов окон, установку ловушек Windows и доступ к памяти внешнего процесса посредством внедрения DLL. В этой главе рассматривается модификация классов. Модификация класса окна Вы уже знаете, что API­функция SetWindowLong может использоваться для изменения стиля окна. С ее помощью можно также изменять оконную процедуру любого окна. Она декларируется таким образом: LONG SetWindowLong( HWND hWnd, // Дескриптор окна. int nIndex, // Индекс устанавливаемого значения. LONG dwNewLong // Новое значение. ); Так это выглядит в VB: Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" ( _ ByVal hwnd As Long, _ ByVal nIndex As Long, _ ByVal dwNewLong As Long _ ) As Long Можно изменить оконную процедуру, установив параметр nIndex в одно из следующих значений:  GWL_WNDPROC определяет новый адрес оконной процедуры;  DWL_DLGPROC устанавливает новый адрес оконной процедуры диалогового окна. Каждое диалоговое окно имеет диалоговую процедуру, а не оконную. Однако эти две процедуры похожи. Обратите внимание, что возвращаемое функцией SetWindowLong значение представляет собой адрес (дескриптор) предыдущей оконной или диалоговой процедуры. Это важно. Используя данную функцию, можно заменить оконную процедуру конкрет­ ного окна процедурой, которую вы сами напишете. Но не существует способа узнать, какие сообщения обрабатывала исходная оконная процедура. Поэтому очень важно вызвать исходную процедуру после того, как ваша процедура закон­ чит свою работу.
324 Модификация класса окна Функция CallWindowProc создана с учетом этих соображений: LRESULT CallWindowProc( WNDPROC lpPrevWndFunc, // Указатель на предыдущую процедуру. HWND hWnd, // Дескриптор окна данного класса. UINT Msg, // Сообщение. WPARAM wParam, // Первый параметр сообщения. LPARAM lParam // Второй параметр сообщения. ); Функция передает сообщение оконной процедуре, указанной в аргументе lpPrevWindowProc. Этому параметру должно быть присвоено значение, воз­ вращаемое функцией SetWindowLong, для того чтобы сообщение передавалось исходной оконной процедуре класса, ассоциированного с этим окном. Процесс замены оконной процедуры другой и передача необработанных сооб­ щений исходной процедуре называется модификацией класса (subclassing) окна. Важно подчеркнуть, что новая оконная процедура должна вызывать исходную для обработки всех тех сообщений, которые она сама не обрабатывает. Обратите особое внимание на то, что SetWindowLong оказывает влияние только на окно, дескриптор которого размещен в параметре hWnd. Для модифи­ кации всего класса можно использовать функцию SetClassLong: DWORD SetClassLong( HWND hWnd, // Дескриптор окна данного класса. int nIndex, // Индекс изменяемого значения. LONG dwNewLong // Новое значение. ); и присвоить параметру nIndex значение GCL_WNDPROC. В этом случае все окна вызывающего данную функцию процесса, которые создаются после вызова фун­ кции SetClassLong, будут модифицированы. Надстройка класса Существует концепция, называемая надстройкой класса (superclassing). Данное название относится к процедуре создания нового класса, который обрабатывает не­ которые сообщения, а затем вызывает оконную процедуру другого класса. Новый класс называется надклассом, или суперклассом1, исходного класса. Это немного похоже на модификацию класса, однако разница в том, что при модификации новый класс не создается, а только модифицируется существующий. Пример модификации класса VB Checkbox Осуществить модификацию класса в Visual Basic не трудно, но нужно действо­ вать очень осторожно, так как любой неверный шаг приведет к хорошо знакомой общей ошибке защиты (GPF). 1 Данная терминология отличается от классической терминологии, принятой в С++ или Java, где суперклассом называется базовый (или порождающий) класс, а производный класс (потомок) назы­ вается подклассом. То есть подкласс (потомок) расширяет свой суперкласс (порождающий). – Прим. науч. ред.
325 Рассмотрим следующий пример модификации уп­ равляющего элемента VB – флажка (сheckbox). Как известно, щелчки мышью по флажку обычно переклю­ чают его состояния между «установлен», значение равно единице, и «неустановлен», значение равно нулю. При этом флажок не переводится в состояние «недоступен», значение равно двум. Однако можно очень просто модифицировать флажок так, чтобы при помощи мыши переключать его по всем трем состоя­ ниям – от «установлен» через «недоступен» до «не­ установлен». На рис. 18.1 . показано главное окно программы rpiSubClass (ее исходный код находится в архиве при­ меров). Нажатие кнопки Subclass Checkbox (Модифици­ ровать флажок) приводит к выполнению следующей процедуры: Sub SubClass() ' Модифицируем флажок. hPrevWndProc = SetWindowLong(Check1.hwnd, GWL_WNDPROC, AddressOf _ WindowProc) ' Для модификации всего класса. ''hPrevWndProc = SetClassLong(Check1.hwnd, GCL_WNDPROC, AddressOf _ WindowProc) If hPrevWndProc <> 0 Then bIsSubclassed = True lblStatus = "Subclassed" End If End Sub Нажатие кнопки Remove Subclassing (Отменить модификацию) приводит к выполнению процедуры Remove: Sub Remove() Dim lret As Long ' Отменяем модификацию при необходимости. If bIsSubclassed Then lret = SetWindowLong(Check1.hwnd, GWL_WNDPROC, hPrevWndProc) ' Для отмены модификации всего класса. Рис. 18.1. Окно программы rpiSubClass Пример модификации класса VB Checkbox
32 Модификация класса окна ''lret = SetClassLong(Check1.hwnd, GCL_WNDPROC, hPrevWndProc) bIsSubclassed = False lblStatus = "Not Subclassed" End If End Sub Ниже представлен код оконной процедуры: Public Function WindowProc(ByVal hwnd As Long, ByVal iMsg As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long Select Case iMsg ' Обрабатываем нажатие левой клавиши мыши и выходим. Case WM_LBUTTONDOWN ' Просматриваем коллекцию управляющих элементов, ' проверяя, принадлежит ли данный дескриптор флажку. For Each ctl In frmSubClass.Controls If TypeName(ctl) = "CheckBox" Then If ctl.hwnd = hwnd Then ' Изменяем значение. If ctl.Value <= 1 Then ctl.Value = ctl.Value + 1 Else ctl.Value = 0 End If ' Обработка по умолчанию отсутствует. Exit Function End If End If Next End Select ' Вызываем исходную оконную процедуру. WindowProc = CallWindowProc(hPrevWndProc, hwnd, iMsg, wParam, lParam) End Function Эта оконная процедура обрабатывает сообщение WM_LBUTTONDOWN, проверяя, является ли управляющий элемент с дескриптором hwnd флажком. Здесь нужно действовать очень осторожно, потому что некоторые управляющие элементы, такие как метка (label), не имеют дескриптора и могут вызвать исключительную ситуацию при попытке проверить свойство hWnd. Запустите программу и щелкните несколько раз по флажку Check1 (Флажок 1). Затем нажмите кнопку Subclass Checkbox и щелкните по Check1 еще несколько раз. Затем нажмите кнопку Remove Subclassing. Обратите внимание, что при использовании функции SetWindowLong модифи­ кация затрагивает только флажок Check1, поскольку дескриптор именно этого уп­
32 равляющего элемента размещен в параметре hWnd. На флажок Check2 (Флажок 2) это никак не влияет. Теперь закомментируйте оба вызова функции SetWindowLong и снимите комментарий с вызовов функции SetClassLong. Если вы теперь запустите про­ грамму и попробуете пощелкать по обоим флажкам, то заметите, что модифика­ ция не действует ни на один из них. Это объясняется тем, что эти управляющие элементы существовали до вызова SetClassLong. Однако модификация будет работать со флажком, созданном нажатием кнопки Create New Checkbox (Соз­ дать новый флажок). Следует еще раз обратить ваше внимание на то, что при модификации целых классов нужно проявлять большую осторожность. Пример модификации класса VB Checkbox
Глава 19. Ловушки Windows Ловушка (hook) – это способ наблюдения за различными участками потока со­ общений Windows. Операционная система определяет множество разных типов ловушек, расположение некоторых из них показано на рис. 19.1 (вместе с моди­ фикацией класса). Модификация (subclassing) Аппаратные сообщения Системная очередь сообщений Очереди сообщений потока Поток необработанного ввода (RIT) О ч е р е д ь а с и н х р о н н ы х с о о б щ е н и й О ч е р е д ь с и н х р о н н ы х с о о б щ е н и й О ч е р е д ь м е ж п о т о ч н ы х с о о б щ е н и й В и р т у а л ь н а я о ч е р е д ь в в о д а Межпоточная Внутрипоточная Оконная процедура Оконная процедура Рис. 19.1. Ловушки Windows
32 Принцип, который используют ловушки, очень похож на принцип модифика­ ции класса. Если сообщение проходит по участку системы сообщений Windows с установленной ловушкой, система вызывает процедуру ловушки (hook procedure), которую вам нужно реализовать. Глобальные ловушки и ловушки потока Ловушка может быть установлена на уровне потока (thread­specific), в этом случае она перехватывает сообщения (заданного типа), передаваемые только дан­ ному потоку. Она может быть глобальной (на уровне всей системы), в этом случае она перехватывает все сообщения заданного типа на всем системном пространстве. Ловушки некоторых видов можно устанавливать как для отдельных потоков, так и глобально, в то время как другие виды ловушек (например, ловушки записи и воспроизведения пользовательского ввода) обязательно должны быть глобаль­ ными. Между ловушками на уровне потока и глобальными ловушками разница до­ вольно существенная. Прежде всего, глобальные ловушки снижают общую произ­ водительность системы и обычно используются в целях отладки. Более значимо то, что должна существовать возможность вызова процедуры глобальной ловушки из любого потока системы, который получает сообщение соответствующего типа. Следовательно, указанная процедура должна быть раз­ мещена в модуле, доступном всем потокам системы, то есть DLL. Всякий раз, когда поток одного процесса устанавливает ловушку на поток другого процесса, речь идет о взаимосвязи между процессами. Представьте, что потоку A процесса A требуется установить ловушку мыши на поток B процесса B. Для этого поток A должен вызвать функцию SetWindowsHookEx, передав ей идентификатор потока B. Однако, когда поток B получает сообщение от мыши, вызывается процедура ловушки потока B. Здесь возникает две проблемы: как процессу A поместить процедуру ловушки в адресное пространство процесса B, и как, если первое сделано, получить резуль­ таты обработки процедурой ловушки, установленной в процессе B? Ответ на первый вопрос заключается в том, что Windows в процессе вызова потоком A функции SetWindowsHookEx сама заботится о месте ловушки в ад­ ресном пространстве. Но для этого процедура ловушки должна находиться в DLL, тогда система сможет внедрить или отобразить эту DLL в адресное пространство процесса B. (Более подробно внедрение DLL будет обсуждаться в главе 20.) К со­ жалению, это обстоятельство выводит глобальные ловушки за сферу применения Visual Basic, так как VB не позволяет создавать традиционные DLL. Таким образом, глобальная ловушка может потенциально способствовать внедрению DLL в адресное пространство каждого из существующих процессов. Если во всех таких случаях библиотека может быть по умолчанию загружена по ее базовому адресу, то отпадает необходимость передавать дополнительную физи­ ческую память каждому новому виртуальному экземпляру DLL. В документации нет ясности относительно того, когда эти библиотеки бу­ дут удалены из оперативной памяти. Понятно одно, что процесс не сможет вы­ Глобальные ловушки и ловушки потока
330 Ловушки Windows звать API­функцию FreeLibrary для выгрузки DLL, о которой ему практичес­ ки ничего неизвестно. Он даже не располагает информацией о том, загружена DLL или нет. Согласно определению Рихтера, когда поток вызывает функцию UnhookWindows HookEx, система проходит по внутреннему списку процессов, в которые она внедряла данную DLL, и осуществляет декремент счетчика блоки­ ровок DLL. При обнулении счетчика библиотека автоматически выгружается из адресного пространства процесса. Некоторые эксперименты это подтверждают. Внедренные DLL, если в них больше нет необходимости, сверхъестественным образом исчезают. Тем не менее при использовании глобальных ловушек следует проявлять осмотрительность. Теперь обсудим общие принципы организации и функционирования ловушек Windows и реализуем ловушку на уровне потока VB. Потом с помощью написан­ ной на VC++ простой DLL реализуем глобальную ловушку. Установка ловушки Ловушку Windows устанавливают с помощью функции SetWindowsHookEx: HHOOK SetWindowsHookEx( int idHook, // Тип устанавливаемой ловушки. HOOKPROC lpfn, // Адрес процедуры ловушки. HINSTANCE hMod, // Дескриптор экземпляра приложения. DWORD dwThreadId // Идентификатор потока для установки ловушки ); В VB это выглядит так: Declare Function SetWindowsHookEx Lib "user32" Alias _ "SetWindowsHookExA" ( _ ByVal idHook As Long, _ ByVal lpfn As Long, _ ByVal hmod As Long, _ ByVal dwThreadId As Long _ ) As Long В случае успеха функция SetWindowsHookEx возвращает дескриптор вновь созданной ловушки, параметр idHook задает ее тип. Если ловушка предназна­ чена для использования на уровне потока, то параметр dwThreadId является идентификатором целевого потока. Если ловушка определялась для глобального применения, то dwThreadId должен быть установлен в NULL (0). С другой стороны, если ловушка предназначена для внешнего потока или явля­ ется глобальной, то lpfn – адрес процедуры ловушки в DLL, а hMod – дескриптор экземпляра данной DLL, то есть ее базовый адрес. Эти значения (lpfn и hMod) относятся, конечно, к копии DLL в вызывающем процессе. Однако Windows будет преобразовывать их в соответствующие значения для каждого процесса, который вынужден (под действием ловушки) загрузить данную DLL. Иными словами, базовый адрес DLL и процедуры ловушки в вызывающем процессе в совокупнос­ ти будут определять смещение процедуры ловушки внутри библиотеки. Таким образом, смещение (offset) процедуры ловушки равно адресу процедуры ловушки минус базовый адрес DLL.
331 Это смещение может быть использовано для определения адреса процедуры ловушки независимо от положения DLL в пространстве данного процесса. Если приложение больше не нуждается в ловушке, оно вызывает функцию UnhookWindowsHookEx: BOOL UnhookWindowsHookEx( HHOOK hhk // Дескриптор удаляемой процедуры ловушки. ); Процедуры ловушек Процедура ловушки имеет следующую форму: LRESULT CALLBACK HookProc( int nCode, WPARAM wParam, LPARAM lParam ); где nCode код ловушки, который зависит от ее типа. Другие параметры так же за­ висят от типа ловушки, но обычно содержат информацию о сообщении. Например, wParam может быть номером сообщения, а lParam – указателем на дополнитель­ ную информацию о сообщении. Типы ловушек В Windows реализовано множество типов ловушек (как показано на рис. 19.1). Заметьте, что некоторым ловушкам разрешается изменять сообщение, в то время как другие могут только анализировать его. Ниже представлены некоторые из наиболее распространенных типов:  WH_CALLWNDPROC. Windows вызывает процедуру ловушки WH_CALLWNDPROC до передачи сообщения, сгенерированного вызовом функции SendMessage, оконной процедуре­адресату. Обратите внимание, что этой ловушке разре­ шается анализировать перехваченное сообщение, но не изменять его;  WH_CALLWNDPROCREТ. Операционная система вызывает процедуру ловуш­ ки WH_CALLWNDPROCRET после того, как оконная процедура обработает сообщение, сгенерированное SendMessage. Эта ловушка действует только в пределах процесса, который создает сообщение. Она также предоставляет информацию о результатах обработки сообщения оконной процедурой;  WH_GETMESSAGE. Windows вызывает процедуру ловушки WH_GETMESSAGE, когда функция GetMessage извлекает сообщение из очереди сообщений потока, но до его передачи принимающей оконной процедуре. Ловушке этого типа разрешается изменять перехваченные сообщения;  WH_JOURNALRECORD и WH_JOURNALPLAYBACK. Windows вызывает процеду­ ру ловушки WH_JOURNALRECORD, когда сообщение удаляется из системной очереди сообщений. Таким образом, эта ловушка отслеживает только сообще­ ния от аппаратуры (мышь и клавиатура). Обычно она используется совместно с ловушкой WH_JOURNALPLAYBACK для записи и последующего воспроизве­ Типы ловушек
332 Ловушки Windows дения пользовательского ввода. Ловушка WH_JOURNALRECORD является гло­ бальной, ее нельзя использовать для отдельных потоков. Процедура ловушки WH_JOURNALRECORD не должна изменять перехваченные сообщения;  WH_KEYBOARD. Windows вызывает процедуру ловушки WH_KEYBOARD, когда приложение вызывает GetMessage (или PeekMessage) и извлеченное сообщение исходит от клавиатуры (WM_KEYUP или WM_KEYDOWN);  WH_MOUSE. Windows вызывает процедуру ловушки WH_MOUSE, когда прило­ жение вызывает GetMessage (или PeekMessage) и извлеченное сообще­ ние исходит от мыши;  WH_SHELL. Windows вызывает процедуру ловушки WH_SHELL, когда при­ ложение оболочки Windows начинает переходить в активное состояние и когда создается или уничтожается высокоуровневое окно. Цепочки ловушек Важно знать, что в любой момент времени в системе могут присутствовать несколько ловушек заданного типа. Например, несколько приложений могут ус­ тановить ловушки WH_MOUSE. По этой причине Windows поддерживает цепочки ловушек (hook chain) отдельно для каждого типа. Цепочка ловушек представляет собой список указателей на процедуры ловушек данного типа. Когда поступает сообщение, связанное с конкретным типом ловушки, Windows передает его первой (созданной раньше всех остальных) процедуре ловушки в це­ почке ловушек данного типа. Дальнейшие действия определяются той процедурой, которая обрабатывает сообщение. Однако, как отмечалось ранее, некоторые ловушки могут изменять или удалять сообщение, в то время как другим это не разрешается. Если процедуре ловушки требуется передать сообщение по цепочке ловушек, она вызывает функцию CallNextHookEx: LRESULT CallNextHookEx( HHOOK hhk, // Дескриптор текущей ловушки (возвращается // SetWindowsHookEx). int nCode, // Код ловушки, передаваемый процедуре ловушки. WPARAM wParam, // Значение, передаваемое процедуре ловушки. LPARAM lParam // Значение, передаваемое процедуре ловушки. ); Как видите, эта функция просто передает информацию о сообщении следую­ щей в цепочке процедуре ловушки. Пример локальной ловушки Реализовать локальную ловушку несложно. На рис. 19.2 показано главное окно демонстрационной утилиты rpiLocalHook. Ниже представлен полный исходный код, принадлежащий данной форме: Option Explicit Sub EnableHook() ghHook = SetWindowsHookEx(WH_MOUSE, AddressOf MouseProc, 0&, _ App.
333 ThreadID) End Sub Sub DisableHook() UnhookWindowsHookEx ghHook End Sub Private Sub cmdDisable_Click() DisableHook ghHook = 0 lblIsHooked = "Not Hooked" lblIsHooked.ForeColor = &H0 End Sub Private Sub cmdEnable_Click() EnableHook lblIsHooked = "Hooked" lblIsHooked.ForeColor = &HFF End Sub Private Sub cmdExit_Click() Unload Me End Sub Private Sub Form_Unload(Cancel As Integer) If ghHook <> 0 Then DisableHook End Sub Все действие происходит в процедуре EnableHook, где находится вызов фун­ кции SetWindowsHookEx: ghHook = SetWindowsHookEx(WH_MOUSE, AddressOf MouseProc, 0&, _ App.ThreadID) Вспомогательный стандартный модуль содержит процедуру ловушки мыши. Полный исходный код этого модуля приведен в листинге 19.1 . Листинг 19.1. Установка SetWindowsHookEx Option Explicit Public ghHook As Long Dim mhs As MOUSEHOOKSTRUCT Dim sText As String Public Const WH_MOUSE = 7 Type POINTAPI x As Long y As Long End Type Type MOUSEHOOKSTRUCT Рис. 19.2 . Пример локальной ловушки мыши Пример локальной ловушки
334 Ловушки Windows pt As POINTAPI hwnd As Long wHitTestCode As Long dwExtraInfo As Long End Type Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" ( _ ByVal idHook As Long, _ ByVal lpfn As Long, _ ByVal hmod As Long, _ ByVal dwThreadId As Long _ ) As Long Declare Function CallNextHookEx Lib "user32" ( _ ByVal ghHook As Long, _ ByVal ncode As Long, _ ByVal wParam As Integer, _ ByVal lParam As Long _ ) As Long Declare Function UnhookWindowsHookEx Lib "user32" ( _ ByVal ghHook As Long _ ) As Long Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ Destination As Any, _ Source As Any, _ ByVal Length As Long) '  Public Function MouseProc(ByVal ncode As Long, ByVal wParam As Long, _ ByVal lParam As Long) As Long ' Документация требует это сделать. If ncode < 0 Then ' Передаем сообщение по цепочке и выходим. MouseProc = CallNextHookEx(ghHook, ncode, wParam, lParam) Exit Function End If ' Получаем MOUSEHOOKSTRUCT, на которую указывает lParam. CopyMemory mhs.pt.x, ByVal lParam, LenB(mhs) ' Заполняем текстовое поле данными сообщения. sText = "MsgID: " & wParam sText = sText & vbCrLf & "For window: " & Hex$(mhs.hwnd) sText = sText & vbCrLf & "X: " & mhs.pt.x & "Y: " & mhs.pt.y ''sText = sText & vbCrLf & "Hit test code: " & mhs.wHitTestCode sText = sText & vbCrLf & "Hit test: " & GetConstant(mhs.wHitTestCode) frmMain.txtMsg.Text = sText
335 ' Переходим к следующей ловушке. MouseProc = CallNextHookEx(ghHook, ncode, wParam, lParam) End Function '  Function GetConstant(vValue As Variant) As String ' Функция возвращает имя константы по заданному значению. Dim sName As String Select Case vValue Case 18 sName = "HTBORDER" Case 15 sName = "HTBOTTOM" Case 16 sName = "HTBOTTOMLEFT" Case 17 sName = "HTBOTTOMRIGHT" Case 2 sName = "HTCAPTION" Case 1 sName = "HTCLIENT" Case 2 sName = "HTERROR" Case 4 sName = "HTGROWBOX" Case 6 sName = "HTHSCROLL" Case 10 sName = "HTLEFT" Case 9 sName = "HTMAXBUTTON" Case 5 sName = "HTMENU" Case 8 sName = "HTMINBUTTON" Case 0 sName = "HTNOWHERE" Case 11 sName = "HTRIGHT" Case 3 sName = "HTSYSMENU" Case 12 sName = "HTTOP" Case 13 sName = "HTTOPLEFT" Case 14 Пример локальной ловушки
33 Ловушки Windows sName = "HTTOPRIGHT" Case 1 sName = "HTTRANSPARENT" Case 7 sName = "HTVSCROLL" End Select GetConstant = sName End Function Обратите внимание, что lParam указывает на структуру MOUSEHOOKSTRUCT, которая содержит информацию о сообщении. Интересные данные представляет dwHitTestCode, определяя местонахождение указателя мыши (на рамке окна, на полосе прокрутки, на меню и т.д .) в тот момент, когда было сгенерировано сообщение. Более подробные сведения об этом смотрите в документации MSDN, касающейся сообщения WM_NCHITTEST. Пример глобальной ловушки Как упоминалось ранее, для установки глобальной ловушки необходимо сде­ лать процедуру ловушки доступной всем потокам, то есть поместить ее в DLL. Так как в VB нельзя разрабатывать традиционные DLL, то для создания библиотеки придется использовать другую программную среду, например, VC++. Приложение rpiGlobalHook состоит из двух частей. Глобальная ловушка реализована в виде двух моду­ лей – модуля формы с именем frmRpiHook и стандарт­ ного модуля basRpiHook. Форма frmRpiHook показана на рис. 19.3 . Благодаря особенностям своей разработки при загрузке она остается невидимой. Таким образом, она функционирует подобно модулю класса, но, кроме того, предоставляет окно (саму форму), класс которого может быть модифицирован. Эти два модуля могут быть добавлены к любому проекту VB для реализации глобальной ловушки мыши. Приложение rpiGlobalHook включает также свое­ го рода установочный проект (installing project), состоящий из формы, показан­ ной на рис. 19.4, frmMain и стандартного модуля basMain. Напомним, что представленная здесь про­ грамма только демонстрирует способ создания глобальной ловушки и не подходит для разработки продукта коммерческого уровня. Такого рода про­ дукт можно было бы создать в виде управляю­ щего элемента ActiveX. Кроме того, этот простой пример не включает кода обработки ошибок. На рис. 19.5 . показана принципиальная структура проекта в целом. Рис. 19.3. Форма глобальной ловушки Рис. 19.4 . Пример глобальной ловушки мыши
33 Далее следует описание шагов, изображенных на рис. 19.5: 1. Когда пользователь нажимает кнопку Enable Hook (Установить ловуш­ ку), установочный проект загружает форму frmRpiHook, изображенную на рис. 19.3 . 2. Событие Load формы frmRpiHook модифицирует свой собственный класс (subclass), вызывая функцию SetWindowLong и передавая ей адрес окон­ ной процедуры из basRpiHook. 3. Установочная программа вызывает процедуру SetGlobalHook, которая реализована в форме frmRpiHook. 4. Эта функция вызывает DLL­функцию rpiSetGlobalMouseHook, пере­ давая ей дескриптор формы (свой собственный дескриптор) и идентифи­ каторы обрабатываемых сообщений. Обратите внимание, что эту DLL вам необходимо поместить в каталог \Windows\System\ на своем компьютере или изменить оператор Lib в соответствующей декларации исходного кода до того, как эта программа начнет выполняться. 5. DLL­функция rpiSetGlobalMouseHook устанавливает глобальную ло­ вушку мыши, вызывая функцию SetWindowsHookEx и подключая, таким образом, процедуру ловушки мыши из DLL. 6. Когда генерируется сообщение мыши, система внедряет DLL в пространс­ тво целевого процесса, и данное сообщение передается процедуре мыши в целевом процессе. Если ID сообщения согласуется со значением аргумента lMsgID, процедура мыши посылает сообщение WM_COPYDATA модифици­ рованной оконной процедуре формы frmRpiHook. 7. Оконная процедура вызывает функцию rpiMouseHookProc, которая оп­ ределена пользователем в установочном проекте. Обратите внимание, что шаги 1–5 и 7 относятся к процессу установки прило­ жения, а шаг 6 касается процесса, который принимает сообщения мыши. Но для сообщения WM_COPYDATA Windows осуществляет маршаллинг (передачу) данных из целевого процесса в установочный. А теперь рассмотрим сам исходный код. 2 7 4 1 3 5 frmRpiHook Form_Load() Subclass Me frmHookForm SetGlobalHook Вызов rpiSetGLobalMouseHook_ (Me.hwnd,IMsgID) Установка VBпроекта Загрузка frmRpiHook Вызов SetGlobalHook rpiSetGLobalMouseHook (Me.hwnd,IMsgID) Вызов SetWindowsHookEx() basRpiHook ATA, ...) (процедура ловушки для мыши) rpiGlobalHook.dll Вызов rpiMouseHookProc Модификация оконной процедуры rpiMouseHookProc 6 Вызов SendMessage (hwnd.WM _COPYDATA, . . .) Рис. 19.5 . Проект глобальной ловушки Пример глобальной ловушки
33 Ловушки Windows Модуль frmRpiHook Исходный код формы frmRpiHook записывается таким образом: Option Explicit Private mhHook As Long Private mhPrevWndProc As Long '  Public Property Get hHook() As Long hHook = mhHook End Property '  Public Property Get hPrevWndProc() As Long hPrevWndProc = mhPrevWndProc End Property '  Public Function SetGlobalHook(ByVal lMsgID As Long) As Long ' Вызывает DLL для установки свойства hHook. ' Возвращает дескриптор ловушки (0 указывает на ошибку). mhHook = rpiSetGlobalMouseHook(Me.hwnd, lMsgID) SetGlobalHook = mhHook End Function '  Public Function FreeGlobalHook() As Boolean FreeGlobalHook = (rpiFreeGlobalMouseHook <> 0) End Function '  Private Sub Form_Load() ' Модифицирует форму. mhPrevWndProc = SetWindowLong(Me.hwnd, GWL_WNDPROC, AddressOf _ WindowProc) End Sub '  Private Sub Form_Unload(Cancel As Integer) ' Отменяем модификацию класса. SetWindowLong Me.hwnd, GWL_WNDPROC, mhPrevWndProc ' При выгрузке формы проверяем наличие действующей ловушки ' и снимаем ее. If hHook <> 0 Then FreeGlobalHook Set frmRpiHook = Nothing End Sub
33 Форма обладает двумя свойствами:  hHook – дескриптор ловушки (если она установлена). Предназначен только для чтения;  hPrevWndProc – дескриптор исходной оконной процедуры формы. Пред­ назначен только для чтения. У нее также есть два метода:  SetGlobalHook вызывает DLL для установки ловушки сообщений;  FreeGlobalHook вызывает DLL для снятия ловушки. Код метода SetGlobalHook представлен ниже: Public Function SetGlobalHook(ByVal lMsgID As Long) As Long ' Вызывает DLL для установки свойства hHook. ' Возвращает дескриптор ловушки (0 указывает на ошибку). mhHook = rpiSetGlobalMouseHook(Me.hwnd, lMsgID) SetGlobalHook = mhHook End Function Этот метод вызывает функцию rpiSetGlobalMouseHook из рассматриваемой DLL, передавая ей дескриптор модифицированной формы (данной формы) и иден­ тификаторы тех сообщений, которые требуется обработать. Подобным же образом метод FreeGlobalHook вызывает DLL­функцию rpiFreeGlobalMouseHook. Несколько позже эти DLL­функции будут рассмотрены более подробно. Модуль basRpiHook Сопутствующий стандартный модуль basRpiHook содержит необходимые де­ кларации вместе с оконной процедурой для модифицированной формы. Оконную процедуру необходимо размещать в стандартном модуле, поскольку оператор VB AddressOf требует, чтобы его аргументы находились именно там. Если бы не это, можно было весь проект поместить в модуле формы, что гораздо удобнее. Исходный код оконной процедуры записывается так: Public Function WindowProc(ByVal hwnd As Long, ByVal iMsg As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long ' Оконная процедура модифицированной формы. If iMsg = WM_COPYDATA Then ' lParam содержит указатель на структуру COPYDATASTRUCT. ' Вывести ее немедленно. ' Вопервых, копируем COPYDATASTRUCT. CopyMemory cds.dwData, ByVal lParam, LenB(cds) ' Затем копируем MOUSEHOOKSTRUCT. CopyMemory mhs.pt.x, ByVal cds.lpData, LenB(mhs) ' Вызываем пользовательскую процедуру ловушки мыши. rpiMouseHookProc mhs.hwnd, cds.dwData, mhs.pt.x, _ mhs.pt.y, mhs.wHitTestCode, mhs.dwExtraInfo Пример глобальной ловушки
340 Ловушки Windows ' Не передаем это сообщение форме. Exit Function End If ' Вызываем исходную оконную процедуру. WindowProc = CallWindowProc(frmRpiHook.hPrevWndProc, _ hwnd, iMsg, wParam, lParam) End Function Когда глобальная ловушка мыши установлена, DLL отслеживает все сообщения мыши. Для каждого такого сообщения DLL посылает сообщение WM_COPYDATA оконной процедуре WindowProc, которая обрабатывает входящее сообщение, вызывает пользовательскую процедуру rpiMouseHookProc (в установочном проекте), и уничтожает его. Все остальные сообщения передаются оконной про­ цедуре формы по умолчанию. Параметры WindowProc содержат следующие данные:  hwnd – дескриптор модифицированной формы;  iMsg – номер сообщения, который всегда содержит WM_COPYDATA;  WParam – дескриптор посылающего (sending) окна. В данном случае содер­ жит нуль, так как посылающее окно отсутствует (сообщения направляет DLL);  LParam – указатель на переменную типа COPYDATASTRUCT, для которой осуществляется маршаллинг из целевого процесса посредством вызова SendMessage с использованием WM_COPYDATA. Далее представлена структура COPYDATASTRUCT: Type COPYDATASTRUCT dwData As Long ' ID перехватываемого сообщения мыши. cbData As Long ' Счетчик байтов, на который указывает lpData. ' Его значение равно 20. lpData As Long ' Указатель на переменную типа MOUSEHOOKSTRUCT. End Type И структура MOUSEHOOKSTRUCT: Type MOUSEHOOKSTRUCT pt As POINTAPI ' Структура POINT с координатами мыши. hwnd As Long ' Дескриптор окна, получающего сообщение мыши. wHitTestCode As Long ' Описывает положение мыши. dwExtraInfo As Long ' Дополнительная информация о сообщении, ' обычно нуль. End Type Наконец, DLL предполагает, что реализованная пользователем процедура ло­ вушки мыши имеет следующий вид: Public Sub rpiMouseHookProc(ByVal hwnd As Long, _ ByVal iMsg As Long, _ ByVal x As Long, _
341 ByVal Y As Long, _ ByVal wHitTestCode As Long, _ ByVal dwExtraInfo As Long) Библиотека rpiGlobalHook.dll Теперь поговорим о DLL. Как упоминалось ранее, она пишется на языке VC++. В действительности это не очень сложно. Во­первых, сюда входит процедура rpiSetGlobalHook, которая просто вызывает SetWindowHookEx. Кроме того, она сохраняет дескриптор ловушки, дескриптор модифицированной формы и идентификаторы обрабатываемых со­ общений в совместно используемых переменных, то есть в переменных, которые совместно используются всеми процессами, загрузившими данную DLL. int WINAPI rpiSetGlobalMouseHook(int hSubclassedForm, int iMsgID) { // Устанавливает ловушку мыши и возвращает ее дескриптор // или нуль при неудаче. // Ввод – дескриптор модифицированной формы и ID сообщения для ловушки. // Устанавливает совместно используемую переменную ghSubclassedForm, // которая содержит дескриптор модифицированной формы, msgID // и переменную ghHook. // Если ловушка уже установлена, не следует ее устанавливать еще раз. if (!ghHook == 0) return 0; ghHook = SetWindowsHookEx( WH_MOUSE, (HOOKPROC)MouseProc, hDLLInst, 0); // Сохраняем дескриптор модифицированной формы. ghSubclassedForm = (HANDLE) hSubclassedForm; // Сохраняем ID сообщения. lMsgID = iMsgID; return (int)ghHook; } Функция rpiFreeGlobalMouseHook записывается еще проще: int WINAPI rpiFreeGlobalMouseHook() { HRESULT hr; // Освобождаем ловушку hh. При неудаче возвращаем нуль. hr = UnhookWindowsHookEx(ghHook); ghHook = 0; return hr; } Пример глобальной ловушки
342 Ловушки Windows Наконец, давайте рассмотрим процедуру ловушки мыши. Как упоминалось ранее, эта процедура перехватывает данные сообщений мыши и отправляет их модифицированной форме проекта VB с помощью сообщения WM_COPYDATA, для которого Windows осуществляет автоматический маршаллинг. Затем она вызыва­ ет процедуру следующей в цепочке ловушки. LRESULT WINAPI MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { // Если nCode < 0, то не обрабатываем сообщение. if (nCode < 0) return CallNextHookEx(ghHook, nCode, wParam, lParam); // Проверяем наличие фильтра для ID сообщения. if ( (lMsgID == 0) || (lMsgID & wParam) ) // || логическое OR, & // поразрядное AND. { // Подготавливаем данные для копирования в сообщение. copydata.dwData = wParam; // Здесь ID сообщения. copydata.lpData = (MOUSEHOOKSTRUCT*)lParam; copydata.cbData = sizeof(MOUSEHOOKSTRUCT); // Посылаем сообщение. if (!ghSubclassedForm == 0) { SendMessage(ghSubclassedForm, VM_COPYDATA, NULL, (LPARAM) &(copydata.dwData) ); } } } return CallNextHookEx(ghHook, nCode, wParam, lParam); } Установочное приложение Здесь приводится исходный код формы установочного процесса, изображен­ ной на рис. 19.3: Option Explicit Dim hMouseHook As Long Const WM_LBUTTONDOWN = &H201 Const WM_RBUTTONDOWN = &H204 '  Private Sub cmdEnable_Click() ' Загружаем форму ловушки, но не отображаем ее. Load frmRpiHook ' Устанавливаем ловушку. ''frmRpiHook.SetGlobalHook WM_LBUTTONDOWN + WM_RBUTTONDOWN ' Для всех сообщений мыши. frmRpiHook.SetGlobalHook 0
343 ' Сохраняем дескриптор ловушки для последующего ее снятия. hMouseHook = frmRpiHook.hHook lblIsHooked = "Hooked" lblIsHooked.ForeColor = &HFF End Sub '  Private Sub cmdDisable_Click() frmRpiHook.FreeGlobalHook hMouseHook = 0 lblIsHooked = "Not Hooked" lblIsHooked.ForeColor = &H0 End Sub '  Private Sub cmdExit_Click() If hMouseHook <> 0 Then frmRpiHook.FreeGlobalHook Unload Me End Sub '  Private Sub Form_Unload(Cancel As Integer) ' Это отменяет модификацию класса формы и снимает ловушку. Unload frmRpiHook End Sub Пользовательский исходный код для обработки сообщений находится в стан­ дартном модуле установочного приложения. Эта процедура производит с инфор­ мацией от мыши те же действия, что и в примере с локальной ловушкой: Public Sub rpiMouseHookProc(ByVal hwnd As Long, ByVal iMsg As Long, _ ByVal x As Long, ByVal y As Long, ByVal wHitTestCode As Long, _ ByVal dwExtraInfo As Long) Dim sText As String ' Заполняем текстовое поле данными сообщения. sText = "MsgID: " & iMsg sText = sText & vbCrLf & "For window: " & Hex$(hwnd) sText=sText&vbCrLf&"X:"&x&"Y:"&y sText = sText & vbCrLf & "Hit test: " & GetConstant(wHitTestCode) frmMain.txtMsg.Text = sText frmMain.txtMsg = sText End Sub В отличие от примера с локальной ловушкой теперь можно перемещать мышь над любыми окнами системы и перехватывать генерируемые ею сообщения. Пример глобальной ловушки
Глава 20. Внедрение DLL и доступ к внешнему процессу В примере с глобальной ловушкой в предыдущей главе показана процедура внед­ рения (injection) DLL. Напомним вкратце, что после установки глобальной ло­ вушки система внедряет (отображает, загружает) DLL, содержащую процедуру ловушки, в адресное пространство того процесса, который получает перехватыва­ емые сообщения, тем самым делая процедуру ловушки доступной для отслежи­ ваемого потока. Последствия процедуры отображения DLL довольно интересны и выходят за первоначально определенные рамки. В частности, DLL может содержать код помимо процедуры ловушки. Это значит, что программист может заставить любой процесс в системе выполнять действия, которые он укажет. Далее эта особенность будет использована для создания очень интересного приложения, основная задача которого состоит в выделении памяти внешнего процесса, то есть чужой памяти (foreign memory). Однако создание такого прило­ жения преследует более далекую цель: с его помощью можно заставить внешний процесс выполнять любой код, который вы включите во внедряемую DLL. Поэ­ тому я назвал эту библиотеку rpiAccessProcess.dll. После обсуждения приложения будут рассмотрены примеры использования выделенной во внешнем процессе памяти. Доступ к внешнему процессу. Граф отслеживаемых потоков Библиотека rpiAccessProcess.dll, содержащаяся в архиве примеров, экспорти­ рует следующие функции:  rpiVirtualAlloc выделяет заданный объем памяти во внешнем процессе;  rpiVirtualFree освобождает память, выделенную вызовом функции rpiVirtualAlloc;  rpiVirtualWrite записывает заданное количество байтов в память вне­ шнего процесса;  rpiVirtualRead читает заданное количество байтов из памяти внешнего процесса;  rpiSetForegroundWindow выводит приложение на передний план. Со­ здана для замещения Win32­функции SetForegroundWindow в Windows 98/2000. Эта функция упоминалась в главе 11.
345 Чтобы понять, как работают эти функции и как спроектировано приложе­ ние, нужно представить каждый поток, который выдает или получает запрос на выделение памяти, в виде вершины (node) графа, который за неимением более подходящего названия я назвал графом отслеживаемых потоков (hooked­thread graph). Поток представляется его вершиной, если для него установлена ловушка и соответствующая DLL загружена в адресное пространство процесса­владельца данного потока. На рис. 20.1 показаны три отслеживаемых потока (они могут относиться к одному или к разным процессам). Узел для Thread2 Index: 2 ProcID: 84 ThreadID: 183 hHook: 5244326 hDialog: 657016 OutDeg: 0 InDeg: 3 Узел для Thread0 Index: 0 ProcID: 162 ThreadID: 196 hHook: 138347756 hDialog: 13960316 OutDeg: 3 InDeg: 1 Узел для Thread1 Index: 1 ProcID: 155 ThreadID: 167 hHook: 853596 hDialog: 1640072 OutDeg: 2 InDeg: 1 Адрес 22020096 Размер: 4096 Размер: 4096 Адрес 21954560 Р а з м е р : 4 0 9 6 А д р е с 2 1 9 5 4 5 6 0 Р а з м е р : 4 0 9 6 А д р е с 2 2 0 2 0 0 9 6 Р а з м е р : 4 0 9 6 А д р е с 2 2 0 8 5 6 3 2 Рис. 20.1. Граф потока Каждая вершина представляет собой структуру в DLL, определенную следу­ ющим образом: struct rpiNODE { DWORD dwThreadID; HHOOK hHook; HWND hDialog; int iOutDeg; int iInDeg; } Значение dwThreadID есть ID потока, представленного данной вершиной. Оно однозначно идентифицирует саму вершину. Член структуры hHook является дескриптором ловушки. С каждой вершиной связано скрытое диалоговое окно, дескриптор которого хранится в hDialog. Член структуры iOutDeg указывает количество дуг, выходящих из данной вершины, а iInDeg – количество дуг, вхо­ дящих в нее. Информация о вершинах графа отслеживаемых потоков хранится в массиве, называемом таблицей вершин (nodes table), в совместно используемом разделе Граф отслеживаемых потоков
34 Внедрение DLL и доступ к внешнему процессу DLL. Это означает, что все потоки, обращающиеся к данной библиотеке, имеют доступ к указанной таблице. Ранее уже говорилось, что переменная в DLL может быть локальной, доступной внутри процедуры; глобальной, доступной одному экземпляру DLL, совместно используемой, доступной всем экземплярам данной DLL. В случае использования локальных и глобальных переменных каждый эк­ земпляр DLL получает отдельную копию такой переменной, поэтому если один из экземпляров DLL изменяет ее значение, то на копии, содержащиеся в других экземплярах, это не влияет. Как видно из рис. 20.1, вершины графа могут соединять направленные ребра (directed edges), или дуги (arrows). Ребро от вершины Node1 к Node2 представ­ ляет успешный запрос на выделение памяти, произведенный из вершины Node1 (Thread1) к вершине Node2 (Thread2). Возле каждого ребра записан размер и ба­ зовый адрес выделенной памяти. В DLL ребро, так же как и вершина, представляет собой структуру: struct rpiEDGE { DWORD dwOutThreadID; // ThreadID узла, из которого выходит дуга. DWORD dwInThreadID; // ThreadID узла, в который дуга входит. DWORD dwSize; // Размер области выделения. LPVOID lpAddress; // Базовый адрес выделения. } Количеством ребер, выходящих из вершины, определяется, сколько раз дан­ ный поток успешно получал запрошенную им у любых других потоков системы память. Это число называется степенью выхода (out degree) вершины. Количес­ тво ребер, входящих в вершину, равно количеству запросов на выделение памя­ ти, удовлетворенных данным потоком. Это число называется степенью входа (in degree) вершины. Как и вершины, ребра хранятся в массиве, называемом таблицей ребер (edges table), который находится в совместно используемом разделе DLL. Модель успешных запросов на выделение памяти, представленная в виде гра­ фа отслеживаемых потоков, значительно облегчает разработку программы. С ее помощью можно описать принципы работы создаваемого приложения. Функция rpiVirtualAlloc Функция rpiVirtualAlloc определяется следующим образом: LPVOID WINAPI rpiVirtualAlloc( DWORD dwThreadID, DWORD dwSize, int *piResultCode ); или в VB: Public Declare Function rpiVirtualAlloc Lib "rpiAccessProcess.DLL" ( _ ByVal dwThreadID As Long, _ ByVal dwSize As Long, _ Optional ByRef lResultCode As Long = 0 _ ) As Long
34 Последний параметр является необязательным. Если не требуется проверять результирующий код (который указывает на тип ошибки), то этот аргумент можно опустить. Теперь предположим, что приложение VB вызывает функцию rpiVirtualAlloc. Идентификатор потока, возможно, был получен по дескриптору окна с помощью функций FindWindow и GetWindowThreadProcessID: Dim lResultCode As Long Dim hWindow As Long Dim ThreadID As Long Dim lAddress As Long hWindow = FindWindow(vbNullString, WindowTitle) ThreadID = GetWindowThreadProcessId(hWindow, 0&) lAddress = rpiVirtualAlloc(ThreadID, 4096&, lResultCode) При выполнении этого кода произойдет следующее: 1. Если вершины для целевого потока не существует, то она создается. Это делается с помощью вызова внутренней процедуры DLL CreateNode, ко­ торая работает следующим образом: – осуществляется поиск пустой строки таблицы вершин. Таблица вершин не упакована, и поэтому неиспользуемые элементы могут находиться в разных местах. Кроме того, размер таблицы фиксирован в DLL и состав­ ляет 100 вершин. Этого должно быть вполне достаточно; – ловушка WH_CALLWNDPROC устанавливается на целевой поток; – теперь можно заполнять все члены структуры rpiNODE за исключением hDialog; – функция CreateNode посылает безобидное сообщение WM_NULL целевому потоку. Это заставляет Windows внедрить DLL в целевой процесс. Затем вызывается процедура ловушки CallWndProc для обработки сообщения WM_NULL. Процедура ловушки проверяет равенство значения hDialog нулю, чтобы убедиться, что диалоговое окно еще не создавалось. Затем для создания диалогового окна она вызывает функцию CreateDialog. Поскольку это все происходит в процедуре ловушки, то диалоговое окно создается в целевом процессе. В нижней части рис. 20.2 показаны три таких диалоговых окна. Они должны быть скрытыми и, следовательно, не содержать текстовых полей, но в этом приложении они специально показываются, а текстовые поля заполняются данными вершины; – когда обработка сообщения WM_NULL завершается, функция CreateNode проверяет совместно используемую таблицу вершин, чтобы убедиться, что hDialog вершины имеет ненулевое значение. Это свидетельствует об успешном создании диалогового окна. Затем CreateNode возвращает управление rpiVirtualAlloc. 2. Данная процедура повторяется для вызывающего потока. Иными словами, если вершины вызывающего потока не существует, то она тоже создается с помощью вызова функции CreateNode. Граф отслеживаемых потоков
34 Внедрение DLL и доступ к внешнему процессу 3. К этому моменту созданы вершины и для вызывающего, и для целевого по­ токов. Следующий шаг заключается в формировании ребра и выполняется таким образом: – устанавливаются значения dwInThreadID, dwOutThreadID и размер области памяти dwSize; – осуществляется запрос на выделение памяти путем посылки сообщения MSG_VIRTUAL_ALLOC (определено как WM_APP + 1) диалоговому окну целевого процесса. Дескриптор диалогового окна является значением hDialog вершины целевого потока. Заметьте, что Windows резервирует значения WM_APP до величины &HBFFF для использования приложени­ ями, таким образом, эти значения не будут пересекаться с встроенными сообщениями Windows, посылаемыми диалоговому окну. – диалоговая процедура (особая форма оконной процедуры для обработки сообщений диалогового окна) обрабатывает это сообщение, вызывая API­ функцию VirtualAlloc и заполняя ее возвращаемым значением совмес­ тно используемый член ребра lpAddress. Важно то, что диалоговая про­ цедура работает в пространстве целевого процесса, поэтому вызов функции VirtualAlloc на самом деле выделяет память целевого процесса. 4. Сразу после создания ребра инкрементируются соответствующие значения iInDeg и iOutDeg начальной и конечной вершин, завершая, тем самым, его формирование. На рис. 20.1 изображен граф с тремя вершинами и пятью ребрами. Рис. 20.2 . Демонстрация приложения rpiAccessProcess
34 Функция rpiVirtualFree Функция rpiVirtualFree проще, чем rpiVirtualAlloc. Ее объявление выглядит так: int WINAPI rpiVirtualFree( DWORD dwThreadID, LPVOID lpAddress ); В VB она объявляется следующим образом: Public Declare Function rpiVirtualFree Lib "rpiAccessProcess.DLL" ( _ ByVal dwThreadID As Long, _ ByVal lpAddress As Long) As Long Знать адрес и идентификатор потока необходимо для однозначной иденти­ фикации ребра графа отслеживаемых потоков, то есть для успешного запроса на выделение памяти, поскольку две таких области памяти не могут иметь одинако­ вый адрес в одном и том же процессе. Приложение не будет создавать два ребра с одинаковым значением lpAddress, но с разными потоками в одном процессе, поскольку второй вызов VirtualAlloc закончится ошибкой. Функция rpiVirtualFree работает таким образом: 1. Получает индекс входящей и выходящей вершин ребра. 2. Посылает сообщение MSG_VIRTUAL_FREE диалоговому окну целевого по­ тока. Диалоговая процедура окна обрабатывает сообщение, вызывая API­ функцию VirtualFree (в пространстве целевого процесса) и затем очи­ щает совместно используемый член структуры ребра lpAddress. 3. Функция rpiVirtualFree может затем удалить ребро, то есть структуру rpiEDGE. 4. Осуществляет декремент соответствующих степеней входа и выхода. 5. Если у какой­то из вершин степени входа и выхода равны нулю, такая вершина удаляется. Для этого сначала закрывается ее диалоговое окно, а затем снимается ловушка с соответствующего потока вызовом функции UnhookWindowsHookEx. Так функционирует модель графа отслеживаемых потоков. Как только граф создан, ребра начинают действовать как своего рода каналы между процессами. Для того чтобы целевой поток выполнил некоторую программу по запросу вызы­ вающего потока, все, что нужно сделать – это создать новое сообщение и добавить в диалоговую процедуру программу его обработки, которую должен выполнить другой поток. Данная программа начнет выполняться сразу после отправки со­ общения. Тестирование функций выделения памяти На рис. 20.2 показаны результаты выполнения приложения VB, которое можно использовать для тестирования функций rpiVirtualAlloc и rpiVirtualFree. Его исходный код содержится в архиве примеров на сайте www.dmkpress.ru. Это приложение реализует способ вызова функций DLL rpiVirtualAlloc Граф отслеживаемых потоков
350 Внедрение DLL и доступ к внешнему процессу и rpiVirtualFree из двух других экземпляров данной библиотеки, слегка мо­ дифицированных для изменения заголовка окна. Давайте подробнее рассмотрим его работу. Запустите три варианта указанного приложения – rpiHookApp1.exe, rpiHookApp2.exe и rpiHookApp3.exe, которые находятся в архиве примеров. На рис. 20.2 показаны их окна, а также диалоговые окна соответствующей вершины, созданные DLL после нескольких вызовов функ­ ции rpiVirtualAlloc. Это является отражением графа с рис. 20.1 . Не забудьте, что вы должны освободить все выделенные области памяти, перед тем как закрыть любое из этих приложений, если не желаете получить сообщение об общей ошибке защиты. Выделение памяти внешнего процесса Как упоминалось выше, DLL rpiAccessProcess в настоящее время сориен­ тирована на выделение внешней памяти. Для этого предназначены функции rpiVirtualRead и rpiVirtualWrite. Они довольно просты, особенно если сравнивать их с функциями выделения памяти. Функция rpiVirtualWrite Так записывается объявление функции rpiVirtualWrite: int WINAPI rpiVirtualWrite( DWORD dwThreadID, LPVOID lpSourceAddress, LPVOID lpTargetAddress, DWORD nSize ); В VB функция выглядит так: Public Declare Function rpiVirtualWrite Lib "rpiAccessProcess.dll" ( _ ByVal dwThreadID As Long, _ ByVal lpSourceAddress As Long, _ ByVal lpTargetAddress As Long, _ ByVal dwSize As Long) As Long Данная функция сначала проверяет, что ребро, определяемое значениями dwThreadID и lpAddress, действительно выходит из вершины вызывающего потока, так как нельзя допускать, чтобы неизвестный поток производил запись в память, выделенную другим потоком. В некотором смысле запись в память похожа на негарантированную передачу данных, или, говоря более образно, на хождение по тонкой кромке льда. После подобной проверки rpiVirtualWrite вызывает API­функцию WriteProcess Memory, о которой говорилось в главе 16. Но сначала она вызывает GetWindowThreadProcessId и получает ID процесса по дескриптору диалого­ вого окна целевого процесса. Потом она вызывает OpenHandle для получения де­ скриптора процесса, который требуется передать функции WriteProcessMemory.
351 Функция rpiVirtualRead Таким образом записывается объявление функции rpiVirtualRead: int WINAPI rpiVirtualRead( DWORD dwThreadID, LPVOID lpSourceAddress, LPVOID lpTargetAddress, DWORD nSize ); или в VB: Public Declare Function rpiVirtualRead Lib "rpiAccessProcess.dll" ( _ ByVal dwThreadID As Long, _ ByVal lpSourceAddress As Long, _ ByVal lpTargetAddress As Long, _ ByVal dwSize As Long) As Long Как и функция rpiVirtualWrite, данная функция сначала проверяет, что ребро выходит из вершины вызывающего процесса. Чтение – это в некотором смысле возврат данных по той же тонкой кромке. Затем она вызывает API­функ­ цию ReadProcessMemory. Пример извлечения данных управляющего элемента другого процесса Вы уже видели, что извлечь данные из окна со списком или комбинированного окна внешнего процесса нетрудно, так как Windows осуществляет автоматический маршаллинг этих данных через границы процессов. Однако подобное действие не­ применимо к 32­разрядным управляющим элементам, таким как ListView (спи­ сок). В данном случае маршаллинг необходимо выполнять самим. Для создания приложения rpiControlExtractor (см. главу 16), работаю­ щего с управляющим элементом ListView, можно использовать функцию выде­ ления памяти rpiAccessProcess, формирующую буфер в другом процессе. Но сначала нужно рассмотреть управляющий элемент ListView. Каждый пункт списка управляющего элемента ListView представлен струк­ турой LVITEM: typedef struct LVITEM { UINT mask; int iItem; int iSubItem; UINT stat UINT stateMask; LPTSTR pszText; int cchTextMax; int iImage; Пример извлечения данных из другого процесса
352 Внедрение DLL и доступ к внешнему процессу LPARAM lParam; #if (_WIN32_IE >= 0x0300) int iIndent; #endif }; Составляющие данной структуры таковы:  mask – набор флагов, указывающих, какие из членов структуры содержат корректные данные, а какие нуждаются в установке (в зависимости от вызы­ ваемой функции). Например, значение LVIF_TEXT определяет, содержит ли составляющая pszText корректные данные или нуждается в установке;  item – индекс интересующего пункта элемента ListView, отсчитываемый от нуля;  subitem – индекс интересующего подпункта элемента ListView, отсчиты­ ваемый от единицы. Чтобы сослаться на пункт, а не на подпункт, это значение надо установить в нуль. Подпункты будут рассматриваться несколько позже;  pszText – адрес буфера, содержащего текст пункта или подпункта (строка, завершающаяся нулем);  cchTextMax – размер буфера, на который ссылается pszText. В документации по ListView довольно много несоответствий, особенно в том, что касается пунктов и подпунктов. Однако кое­что описано верно. Когда управляющий элемент ListView находится в режиме вывода данных (report mode), он может отображать один или несколько столбцов. Каждый стол­ бец, кроме первого, соответствует подпункту (subitem) ListView. Первый стол­ бец соответствует пункту (item) ListView. Таким образом, первый столбец содержит текст (метку, надпись) или значок самого пункта, а следующие столбцы содержат текст подпунктов. Следователь­ но, номер подпунктов на единицу меньше номера столбцов. Подпункты нельзя удалять или добавлять, ориентируясь на их имя. Вместо этого удаляются или добавляются столбцы. Для извлечения текста пункта или подпункта нужно заполнить соответству­ ющие члены структуры LVITEM и послать управляющему элементу ListView сообщение LVM_GETITEMA. Исходный код представлен ниже: uLVItem.mask = LVIF_TEXT ' Указывает на то, что текст затребован. uLVItem.iItem = lItem ' Индекс пункта. uLVItem.iSubItem = lSubItem ' Индекс подпункта или нуль. uLVItem.cchTextMax = 255 ' Размер буфера. uLVItem.pszText = lpAddress ' Адрес буфера для текста. ' Посылаем сообщение. lResp = SendMessage(hListView, LVM_GETITEMA, lItem, lpAddress) Основная проблема, конечно, заключается в том, что эта структура должна быть помещена в адресное пространство другого процесса. Учтите, что речь идет и о самой структуре, и о буфере для извлекаемого текста.
353 Это делается следующим образом: 1. Во­первых, установите два буфера в пространстве внешнего процесса – бу­ фер объемом 40 байт для переменной LVITEM: ' Выделяем буфер в памяти другого процесса для 40 байт LVItem. lForeignLVItemAddr = rpiVirtualAlloc(lThreadID, 40&) и буфер объемом 256 байт для текста пункта (или подпункта): ' Выделяем буфер в памяти другого процесса для текста пункта. lForeignTextBufferAddr = rpiVirtualAlloc(lThreadID, 256&) 2. Заполните те члены локальной копии переменной LVITEM, которые требу­ ется, используя адрес буфера текста в другом процессе: uLocalLVItem.mask = LVIF_TEXT uLocalLVItem.iItem = lItem uLocalLVItem.iSubItem = lSubItem uLocalLVItem.cchTextMax = 255 uLocalLVItem.pszText = lForeignTextBufferAddr 3. Локальная переменная LVITEM копируется в адресное пространство вне­ шнего процесса: ' Копируем локальную переменную uLocalLVItem в пространство другого ' процесса. lResp = rpiVirtualWrite(lThreadID, VarPtr(uLocalLVItem.mask), _ lForeignLVItemAddr, 40&) 4. Теперь можно послать сообщение LVM_GETITEMA соответствующему уп­ равляющему элементу в другом процессе: ' Посылаем сообщение. lResp = SendMessageByNum(lhwnd, LVM_GETITEMA, lItem, _ lForeignLVItemAddr) 5. Это приводит к заполнению структуры LVITEM и текстового буфера во вне­ шнем процессе. Текстовый буфер можно скопировать обратно в локальный процесс: ' Копируем данные обратно из пространство другого процесса. lResp = rpiVirtualRead(lThreadID, lForeignTextBufferAddr, _ VarPtr(bLocalTextBuffer(0)), 256) 6. Наконец, не забудьте освободить буферы внешнего процесса: ' Освобождаем память другого процесса. lret = rpiVirtualFree(lThreadID, lForeignTextBufferAddr) lret = rpiVirtualFree(lThreadID, lForeignLVItemAddr) В основном это все, что требуется. Полный исходный код находится в архиве примеров на сайте издательства «ДМК Пресс». Пример исправления системы помощи VB Если говорить честно, то я думаю (во всяком случае сейчас), что новая гипер­ текстовая система помощи Microsoft, введенная в Visual Studio 6, оставляет желать Пример исправления системы помощи VB
354 Внедрение DLL и доступ к внешнему процессу лучшего. В функциональности система, безусловно, является шагом назад. К счас­ тью, имеющиеся недостатки можно устранить, если Microsoft сочтет это нужным. Поспешу добавить, что новая система имеет и одно очень важное преимущест­ во для опытных программистов VB, так как открывает доступ ко всей документа­ ции по Visual Studio. Лично я использую документацию VC++ или SDK гораздо чаще, чем документацию VB6, даже когда программирую в VB. Тем не менее разработчики могли бы получше потрудиться над реализацией этой справочной системы до того, как выпускать ее в свет. Из­за этого я даже не стал устанавливать последние версии библиотеки MSDN. Хочется рассказать о двух наиболее непростительных оплошностях – одна из них настолько поразительна, что я не поверил своим глазам, когда впервые ее обнаружил. Это также касается одной из самых загадочных историй из числа тех, которые происходили со мной при работе на компьютере. На рис. 20.3 наглядно представлены те проблемы, о которых сказано выше. Во­первых, как видите, что диалоговое окно Topics Found (Найденные разделы) занимает всего лишь микроскопическую часть экрана. Почему Microsoft не сделает диалоговые окна, которые изменяли бы свой размер в зависимости от разрешения? Я не могу найти ни одной причины того, чтобы этот список нельзя было отобра­ жать на всю высоту экрана. Рис. 20.3. Система помощи VB6
355 Вторая проблема заключается в том, что упомянутый список разделов не упо­ рядочен по алфавиту. По крайней мере, он не был отсортирован, когда я решил указать на данную оплошность разработчиков. Чтобы убедиться в этом, запустите справочную систему VB6 и выберите на вкладке индекса пункт, относящийся к управляющему элементу Listbox. Затем щелкните по ссылке Properties (Свойства) и посмотрите список Topics Found. Я разработал приложение, специально предназначенное для того, чтобы ре­ шить эту проблему. На рис. 20.4 показан результат его выполнения. Рис. 20.4 . Окно приложения rpiFixVB6Help Приложение rpiFixVB6Help работает так. Допустим, что диалоговое окно Topics Found отображается на экране. Для запуска приложения rpiFixVB6Help предназначена комбинация клавиш Ctrl-Alt-Shift-X . Приложение сразу же про­ сматривает текущие окна системы, чтобы найти высокоуровневое окно Topics Found с помощью функции FindWindow: hTopics = FindWindow(vbNullString, "Topics Found") Затем приложение получает дескриптор окна­потомка управляющего элемен­ та ListView, которое называется List1: ' Ищем окнапотомки для "List1". hListView = FindWindowEx(hTopics, 0, vbNullString, "List1") Пример исправления системы помощи VB
35 Внедрение DLL и доступ к внешнему процессу Теперь точно так же, как в программе rpiControlExtractor, приложение за­ полняет свое окно со списком (свойство Sorted которого установлено в True) названиями разделов. При двойном щелчке по пункту этого окна со списком при­ ложение ищет управляющий элемент ListView для данного пункта, используя тот же, что и ранее, обмен между процессами и отправляя следующее сообщение: lResp = SendMessageByNum(hListView, LVM_FINDITEMA, 1&, _ lForeignLVFindInfoAddr) Затем выбирается указанный пункт управляющего элемента ListView и отображается на экране после отправки соответствующих сообщений: ' Посылаем сообщение для выбора элемента установкой его состояния. lResp = SendMessageByNum(hListView, LVM_SETITEMSTATE, lItem, _ lForeignLVItem) ' Убеждаемся, что элемент отображается. lResp = SendMessageByNum(hListView, LVM_ENSUREVISIBLE, lItem, 0&) Затем приложение завершает свою работу. Есть еще одна история, связанная с этой ситуацией. Как только я запустил справочную систему VB6 для того, чтобы получить снимок экрана для рис. 20.4, список разделов оказался упорядоченным по алфавиту! Мне не ясно, как это про­ изошло, но все исправилось само собой. Единственное предположение, которое можно сделать, заключается в том, что недавно на этом компьютере устанавлива­ лось приложение, разработанное не компанией Microsoft. Возможно, это приложе­ ние перезаписало какие­то системные файлы новыми файлами, в которых ошибка исправлена, или старыми, в которых ее еще не было? Кто знает. В любом случае, сначала я подумал, что мне все это померещилось, но потом я успокоился, увидев, что на моем портативном компьютере проблема осталась не­ разрешенной. Чтобы убедиться, что с портативным компьютером все в порядке и что Microsoft в курсе дела, я позвонил в службу технической поддержки Microsoft. После того как я провел особу из службы технической поддержки через все эта­ пы получения списка Topics Found, показанного на рис. 20.4, она заявила, что ее список также не упорядочен по алфавиту, но не выглядела при этом особенно озабоченной и посоветовала мне обратиться в службу Microsoft, составляющую список пожеланий пользователей. Что бы там ни было, если данный список у вас не отсортирован или вы пред­ почитаете иметь список, развернутый по всей высоте экрана, то можете восполь­ зоваться приложением rpiFixVBHelp.
Глава 21. Растровые изображения Глава 22. Обзор контекстов устройств Глава 23. Типы контекстов устройств Глава 24. Координатные системы контекстов устройств Глава 25. Шрифты Часть IV Windows GDI
Глава 21. Растровые изображения До сих пор более или менее подробно описывались низкоуровневые концепции, такие как процессы, потоки, память, PE­файлы и оконные сообщения. В этой главе начинается обзор графики и текста, что гораздо ближе к уровню пользователя, чем ранее обсуждаемый материал. Графические функции Windows собраны в библиотеке интерфейса графи­ ческих устройств (Graphical Device Interface – GDI). Основные функции GDI реализованы в библиотеке GDI32.DLL. Прямоугольники Вам уже встречалась структура RECT. Давайте повторим ее декларацию: typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT; В GDI интенсивно используются прямоугольники и структура RECT. В этой библиотеке реализованы также разнообразные функции для операций с прямо­ угольниками:  CopyRect создает копию прямоугольника;  EqualRect возвращает True, если два прямоугольника равны;  InflateRect расширяет прямоугольник путем увеличения его линейных размеров, то есть изменения значений соответствующих полей структуры RECT;  IntersectRect определяет пересечение двух прямоугольников;  IsRectEmpty возвращает True, если площадь прямоугольника равна нулю;  OffsetRect перемещает прямоугольник;  PtInRect возвращает True, если заданная точка лежит внутри заданного прямоугольника;  SetRect устанавливает члены структуры RECT;  SetRectEmpty обнуляет члены структуры RECT;  SubtractRect вычитает один прямоугольник из другого при условии, что разность также является прямоугольником;  UnionRect возвращает наименьший прямоугольник, включающий два за­ данных прямоугольника, что является не объединением (union), а скорее выпуклой оболочкой (convex hull) двух прямоугольников.
35 Некоторые из этих функций потребуется использовать в примере, который вам предстоит выполнить. Растры Термин растр (bitmap), или растровое изображение (bitmap image), отно­ сится к графическому изображению, которое состоит из прямоугольного масси­ ва элементов изображения (picture elements), или пикселов. (Хотя прозрачные (transparent) пикселы могут создавать впечатление, что массив не является пря­ моугольным.) Каждый пиксел растра соответствует пикселу на отображающем устройстве, под которым здесь будет подразумеваться экран монитора. К сожале­ нию, многие программы вносят несанкционированные изменения в растры, для того чтобы печатать изображения. Поэтому часто пиксел растра не соответствует одиночному пикселу принтера или точке, печатаемой принтером. А если бы поль­ зователь мог легко осуществить контроль над отображением пикселов растра в точки, печатаемые принтером... впрочем, я отвлекся. Термин «файл растрового изображения» Windows (эти файлы имеют расши­ рение .bmp) относится к специальному набору форматов файлов, которые опера­ ционная система использует для визуализации растровых изображений на отоб­ ражающем устройстве. Каждый пиксел растрового изображения соответствует некоторому количеству битов файла растрового изображения, как показано на рис. 21.1 . Количество битов на один цвет в файле растрового изображения называется насыщенностью цвета (color depth) и зависит от количества цветов, поддержива­ емых данным растровым изображением. Например, монохромное изображение требует только одного бита на пиксел, в то время как для растрового изображения с 256 цветами необходимо 8 бит на пиксел (насыщенность цвета равна 8). Растро­ вое изображение с насыщенностью цвета n поддерживает 2n цветов. Значения пикселов в файле растрового изображения могут относиться или не относиться к интенсивности самих цветов. Для описания цвета требуется три байта, Массив пикселов 8разрядное значение пиксела в файле растровой картинки Пиксел на растровом изображении Рис. 21.1. Растровые изображения и файлы растровых изображений Растры
30 Растровые изображения по одному на каждый основной цвет – красный (red), зеленый (green), голубой (blue). Поэтому часто бывает так, что каждое значение пиксела задает смещение (offset) в таблице цветности, которая также находится в файле растрового изображения. Microsoft несколько раз за прошедшие годы совершенствовала формат файла BMP, поэтому в настоящее время существует несколько форматов файлов растро­ вых изображений. На файлы первоначального формата BMP ссылаются как на аппаратно-зависимые растровые изображения (Device­Dependent Bitmap – DDB). Этот формат сейчас повсеместно признается устаревшим, хотя файлы такого типа по­прежнему существуют в большом количестве. Все следующие версии формата файлов BMP считаются аппаратно-независимыми растровыми изображениями (Device­Independent Bitmap – DIB). К сожалению, термин «растровое изображение» часто используется и для обозначения этих типов файлов1. Для ясности в книге термин «файл растрового изображения» (bitmap file) применяется при ссылках на файл, а «растровое изобра­ жение» (bitmap image) при ссылках на само графическое изображение. Все запутывается еще больше, если добавить, что при сохранении файла растро­ вого изображения Windows удаляет его заголовок, создавая, таким образом, отобра­ жение растрового изображения Windows в памяти (Windows bitmap memory image). В итоге растровыми изображениями могут называться графическое рас­ тровое изображение, файл растрового изображения или отображение растрового изображения в памяти. Строки развертки Для визуализации растрового изображения на дисплее пикселы (триады точек) на внутренней поверхности экрана дисплея сканируются по строкам электронны­ ми пушками. Поэтому каждая строка массива пикселов файла растрового изоб­ ражения (см. рис. 21.1) соответствует некоторой части одной из строк развертки дисплея. Иногда сами строки файла называют строками развертки. (Подробнее об аппаратной растровой развертке можно узнать из моей книги Understanding Personal Computer Hardware, опубликованной Springer­Verlag, Нью­Йорк.) Растровые изображения можно разделить на две категории, в зависимости от того, в прямом или в обратном направлении строки пикселов были записаны в файл растрового изображения при сканировании. В нисходящем (top­down) файле растрового изображения первая строка пикселов соответствует верхней строке растрового изображения. Это означает, что начало растрового массива, то есть элемент массива пикселов (0,0), представляет верхний левый угол растрового изображения. В восходящем (bottom­up) файле первая строка пикселов соответс­ твует последней (нижней) строке. Следовательно, начало представляет нижний левый угол растрового изображения. Проще говоря, нисходящий файл воспроизводит растровое изображение свер­ ху вниз, в то время как восходящий – снизу вверх. 1 По аналогии можно сказать, что документ – это текст определенной структуры, распечатанный на бумаге, а то, что мы подготавливаем в текстовом редакторе – это файл документа, или электронный документ. – Прим. науч. ред.
31 Аппаратно-независимые растровые изображения Основная проблема совместимости, связанная с растровыми изображениями, заключается в визуализации цвета. Первоначальный аппаратно­зависимый фор­ мат растровых изображений, который применялся в Windows 3.0 и более ранних версиях, использует очень простой способ для представления цвета. Значение каждого пиксела в массиве данных изображения является индексом таблицы цвет­ ности, которая поддерживается самой Windows. В этом случае таблица цветности не входит в состав файла растрового изображения. Таким образом, если растровое изображение создается на одном компьютере, а воспроизводится на другом, с дру­ гой таблицей цветности, то исходные цвета могут быть изменены. Для решения данной проблемы был разработан формат аппаратно­независи­ мых растровых изображений. Файл DIB содержит всю необходимую цветовую информацию, используемую растровым изображением, делая его переносимым. Такой эффект объясняется следующим. Обычно цвета в растровом изобра­ жении представляются на основе цветовой модели (color model) RGB, в которой каждый цвет описывается трехбайтовым (24 бита) числом – по 8 бит для задания яркости красного, зеленого и голубого цветов соответственно. Эти цвета являются основными (primary) цветами модели RGB. Например, цвет &HFFFFFF указыва­ ет на максимальную яркость каждого цвета, и, следовательно, задает белый цвет, &H0 – черный, а &H00FF00 – зеленый цвет с максимальной яркостью. Вы можете сами это проверить при помощи управляющего элемента VB Picturebox (рамка с изображением), выполнив следующий код: Picture1.BackColor = RGB(0, &HFF, 0) Между прочим, существуют и другие цветовые модели. Например, основными цветами цветовой модели CMYK является голубой (Cyan), желтый (Yellow), ярко­ красный (Magenta) и черный (Black). Цветовая модель RGB используется мони­ торами, а цветовая модель CМYK – цветными принтерами. Это одна из причин несоответствия цвета на экране монитора и на распечатке принтера. Представление цвета каждого пиксела в файле DIB зависит от того, как много цветов поддерживает данное растровое изображение. Обычно растровые изобра­ жения имеют следующие значения насыщенности цвета:  1 бит: 2 цвета (монохромное изображение);  4 бита: 16 цветов;  8 бит: 256 цветов;  16 бит: 65536 цветов, высококачественное цветовоспроизведение (high color);  24 бита: 16777216 цветов, реалистичное цветовоспроизведение (true color). Существуют также растровые изображения с насыщенностью цвета 32 бита. В действительности они требуют 24 битов для представления основных RGB­ цветов. Дополнительный байт используется под альфа-канал (alpha channel), ко­ торый содержит информацию о степени прозрачности (opacity) каждого пиксела. Растровые изображения с насыщенностью цвета 1, 4 или 8 всегда использу­ ют таблицу цветности (color table), которая называется также цветовой палит- рой (color palette). Это таблица трехбайтовых цветовых значений RGB, размер которой равен количеству поддерживаемых цветов. Каждое значение пиксела Растры
32 Растровые изображения в массиве данных о растровом изображении – это всего лишь индекс соответ­ ствующей таблицы цветности. Таблица показана на рис. 21.2 . Проблемы возникают с 16­разрядными цветными изображениями, не попада­ ющими в диапазон между теми изображениями, которые обязаны использовать таблицу цветности (насыщенность 1, 4 и 8), и теми, которым таблица цветности не нужна (насыщенность 24 и 32). Для операций с 16­разрядными цветными изоб­ ражениями используется множество схем, но обычно все они попадают в одну из двух категорий. Либо значение пиксела является смещением в таблице цветности, либо значение пиксела маскирует три составляющих – по одной на каждый ос­ новной цвет. Конечно, каждая из составляющих меньше, чем 8 разрядов, которые потребовались бы для представления всех возможных яркостей одного цвета. Например, в самой простой цветовой маске 5 бит на каждый цвет: Xrrrrrgggggbbbbb где самый старший бит x не используется, а 5 младших бит определяют яркость голубого цвета. Обратите внимание, что при сохранении этого 16­разрядного сло­ ва байты записываются, начиная с младших разрядов (прямой порядок), так что в памяти слово появляется в виде gggbbbbb xrrrrrgg. Важно отметить, что даже 24­разрядные и 32­разрядные файлы цветных рас­ тровых изображений иногда содержат таблицы цветности. Это происходит из­за того, что может возникнуть необходимость визуализации изображений на уст­ ройствах, которые не поддерживают всю палитру цветов изображения. Визуа­ лизация 24­разрядного цветного изображения на мониторе, который отображает только 256 цветов, требует использования таблицы цветности или каких­то других средств для замены каждого из потенциально возможных 16 миллионов цветов одним из 256. Формат файла BMP развивается, по крайней мере, на протяжении четы­ рех версий, и для Windows 2000 реализована новая версия. Впрочем, не будем углубляться в обсуждение форматов, так как это не является основным в теме о Win32 GDI. Цвет 0 Цвет 255 Значение пиксела Таблица цветов для 8разрядной цветной растровой картинки Рис. 21.2 . Таблица цветности
33 Функции для работы с растровыми изображениями Win32 API предоставляет множество функций для создания растровых изоб­ ражений и работы с ними. И прежде чем приниматься за свою очередную про­ грамму – редактор растровых изображений – вы, конечно, захотите поближе с ними познакомиться. Однако давайте ограничимся только двумя функциями, связанными с растровыми изображениями – LoadImage и BitBlt. Функция BitBlt BitBlt представляет собой одну из часто используемых функций Windows GDI. Название BitBlt является сокращением от Bitmap Block Transfer (перенос блоков растровых изображений), из чего следует, что назначение BitBlt – пере­ мещать графические данные. Впрочем, можно также задать способ взаимодейс­ твия перемещаемых данных и данных, на которые осуществляется перемещение. Синтаксис функции BitBlt таков: BOOL BitBlt( HDC hdcDest, // Дескриптор контекста устройства копии. int nXDest, // Xкоордината верхнего левого угла прямоугольника // копии. int nYDest, // Yкоордината верхнего левого угла прямоугольника // копии. int nWidth, // Ширина прямоугольникакопии. int nHeight, // Высота прямоугольникакопии. HDC hdcSrc, // Дескриптор контекста устройства источника. int nXSrc, // Xкоордината верхнего левого угла исходного // прямоугольника. int nYSrc, // Yкоордината верхнего левого угла исходного // прямоугольника. DWORD dwRop // Код растровой операции. ); Кроме всего прочего, этой функции требуются дескрипторы контекста уст­ ройства источника данных и их приемника. Контексты устройств будут подробно описаны в главе 22. Здесь же достаточно сказать, что контекст устройства пре­ доставляет средства для использования функций рисования GDI на конкретных устройствах (которыми могут быть дисплей, принтер, окно или даже участок памяти). Вам, может быть, известно, что большинство управляющих элементов VB (включая формы) имеют свойство hDC, возвращающее дескриптор контекста устройства. Оно использовано в примерах программ данной главы. Единственным параметром, требующим некоторых пояснений, является код растровой операции dwRop. В табл. 21.1 приведены некоторые из ее возможных значений. Операторы AND, OR и NOT в этой таблице являются поразрядными опе­ раторами: 1100 AND 1010 = 1000 1100 OR 1010 = 1110 1100 XOR 1010 = 0110 NOT10=01 Работа с растровыми изображениями
34 Растровые изображения Таблица 21.1. Некоторые коды растровых операций Код Описание BLACKNESS Заполняет копию прямоугольника, используя цвет, имеющий индекс 0 в физической палитре, по умолчанию – черный цвет DSTINVERT Инвертирует копию прямоугольника NOTSRCCOPY Копия = NOT Источник NOTSRCERASE Копия = NOT (Источник OR Копия) SRCAND Копия = Источник AND Копия SRCCOPY Копирует (накладывает) исходный прямоугольник в целевой. Исходные пикселы целевого прямоугольника теряются SRCERASE Копия = Источник AND (NOT Копия) SRCINVERT Копия = Источник XOR Копия SRCPAINT Копия = Источник OR Копия WHITENESS Заполняет копию прямоугольника, используя цвет, имеющий индекс 1 в физической палитре, по умолчанию – белый цвет Пример перемещения игральных карт Прежде чем привести пример использования функции BitBlt, мне хотелось бы рассказать о моем знакомстве с этой функцией, поскольку отчасти это имеет отношение к рассматриваемой теме. Примерно в 1994 году стало доступным первое программное обеспечение распознавания слитной речи для персонального компьютера. В то время я разрабатывал большую базу данных из нескольких тысяч избранных книг в области математики для информационного бюллетеня, который я издавал. Каждой книге требовалось назначать до трех тем из более чем сотни входящих в предметную классификацию. Первые несколько тематических разделов по­ казаны в табл. 21.2 . Таблица 21.2 . Тематическая классификация Код Тема АА Абстрактная алгебра АГЕО Алгебраическая геометрия АЛГ Алгоритмы ПМ Прикладная математика АППРОК Теория аппроксимации АРИФ Арифметика АТОП Алгебраическая топология Мысль о том, что это может быть сделано с помощью речевого ввода, меня просто восторгала, поскольку альтернатива – ввод этих данных с клавиатуры – была отвратительной. Я хотел, чтобы можно было вводить такие слова, как сле­ дующая книга, алгоритмы, следующая тема, прикладная математика, следующая тема, аппроксимация, следующая книга.
35 Как оказалось, речевой ввод работал довольно хорошо – гораздо лучше, чем я предполагал, что и побудило меня заинтересоваться распознаванием речи. Демонстрационная программа, которая поставлялась с программой распозна­ вания речи, была обычной небольшой видеоигрой в покер, и мне пришло в голову, что компания могла бы лучше продемонстрировать свою программу, добавив ре­ чевой ввод к игре «Солитер», которая поставляется вместе с Windows. К сожалению, в той конкретной версии не было возможности добавить рече­ вой ввод, так как она не принимала ввод с клавиатуры. И я принялся писать клон игры на Visual Basic, где было бы возможно управление с клавиатуры командами типа JH.QS, что означает Put the Jack of Hearts on the Queen of Spades (положить валета червей на даму пик). Это упростило бы добавление речевого ввода к про­ грамме. Задача неожиданно оказалась довольно сложной. Во­первых, я начал с размещения на форме VB 52 рамок с изображением (picture box), но продвинулся не очень далеко, так как VB сообщил, что ресурсы исчерпаны. Тогда я попробовал вместо этого использовать управляющий элемент «изображение» (image), но в этом случае нельзя было избежать сильного мерцания при перемещении этих управляющих элементов. (Там были и другие проблемы, но теперь я уже не помню какие.) Единственным решением, которое не требовало больших затрат ресурсов и не приводило к заметным мерцаниям при перемещении карт, было использование функции BitBlt. Идея заключалась в том, чтобы добавить отдельную форму с единственной рамкой с изображением (picture box), называющейся Deck (колода карт). В эту рамку я поместил колоду из 52 карт, как показано на рис. 21.3 . Когда же карту нуж­ но было изобразить в каком­то месте игрового пространства, я просто использовал BitBlt для копирования карты из рамки в основную форму. В архиве программ содержится небольшой проект rpiBitBlt с примером ис­ пользования BitBlt. Программа показывает на главной форме frmTable слу­ чайно выбранную карту из небольшой колоды из 20 карт. Можно использовать Рис. 21.3. Карты для программы «Солитер» Работа с растровыми изображениями
3 Растровые изображения мышь для перетаскивания карт в пределах главно­ го окна, не затрагивая другие карты. На рис. 21.4 и 21.5 изображены примеры расположения карт до и после перемещения. Большая часть действий здесь происходит в со­ бытии MouseMove. Когда карта перемещается, вы, во­первых, опре­ деляете открывающиеся в результате этого прямо­ угольники (UnRect1 и UnRect2). На рис. 21.6 пока­ заны открывшиеся при сдвиге карты вниз и вправо прямоугольники. Их положение, конечно, будет зависеть от направления движения карты. Ниже представлена часть программы, которая соответствует рис. 21.6: ElseIf CardLocation(iCurrentCard).Left > CardPrevRect.Left And _ CardLocation(iCurrentCard).Top > CardPrevRect.Top Then ' Двигаем вправо и вниз. UnRect1.Top = CardPrevRect.Top UnRect1.Bottom = CardLocation(iCurrentCard).Top UnRect1.Left = CardPrevRect.Left UnRect1.Right = CardPrevRect.Right UnRect2.Top = CardLocation(iCurrentCard).Top Рис. 21.6 . Перемещение карты Рис. 21.5 . Расположение карт после перемещения Рис. 21.4 . Расположение карт до перемещения
3 UnRect2.Bottom = CardPrevRect.Bottom UnRect2.Left = CardPrevRect.Left UnRect2.Right = CardLocation(iCurrentCard).Left Как только прямоугольники UnRect1 и UnRect2 определены, вы заполняете их цветом игровой поверхности (формы), используя функцию GDI FillRect: FillRect Me.hdc, UnRect1, COLOR_BTNFACE + 1 FillRect Me.hdc, UnRect2, COLOR_BTNFACE + 1 Если вам непонятно, откуда здесь +1, обратитесь к документации, где говорит­ ся, что при задании значения цвета для параметра hbr цвет должен быть одним из стандартных системных цветов (к выбранному цвету должно быть добавлено +1). Далее копируете перемещаемую карту на ее новое место с помощью функции BitBlt: ' Помещаем текущую карту на новое место. i = CardIndex(iCurrentCard) BitBlt Me.hdc, _ CardLocation(iCurrentCard).Left, _ CardLocation(iCurrentCard).Top, _ CARD_WIDTH, _ CARD_HEIGHT, _ frmCards.Deck.hdc, _ DECK_X _SPACING * (i Mod DECK_COL_COUNT), _ DECK_Y _SPACING * (i \ DECK_COL_COUNT), _ SRCCOPY Конечно, открывшиеся прямоугольники могут пересекаться с некоторыми другими картами. Проверить это можно при помощи функции IntersectRect: ' Получаем прямоугольник карты. SetRect Card, CardLocation(i).Left, CardLocation(i).Top, _ CardLocation(i).Left + CARD_WIDTH, CardLocation(i).Top + _ CARD_HEIGHT ' Получаем пересечение карты с Unrect1. IntersectRect FixupRect, UnRect1, Card ' Если пересечение есть, перерисовываем FixupRect. If IsRectEmpty(FixupRect) = 0 Then BitBlt Me.hdc, _ FixupRect.Left, _ FixupRect.Top, _ FixupRect.Right  FixupRect.Left, _ FixupRect.Bottom  FixupRect.Top, _ frmCards.Deck.hdc, _ DECK_X _SPACING * (CardIndex(i) Mod DECK_COL_COUNT) + _ (FixupRect.Left  CardLocation(i).Left), _ DECK_Y _SPACING * Int(CardIndex(i) / DECK_COL_COUNT) + _ (FixupRect.Top  CardLocation(i).Top), _ SRCCOPY End If Работа с растровыми изображениями
3 Растровые изображения Далее показан полный исходный код для события MouseMove: Private Sub Form_MouseMove(Button As Integer, Shift As Integer,_ x As Single, y As Single) Dim i As Integer Dim UnRect1 As RECT Dim UnRect2 As RECT Dim FixupRect As RECT Dim Card As RECT If Not bProcessMouseMove Then Exit Sub ' Корректируем положение карты. CardLocation(iCurrentCard).Left = x  iCardOffsetX CardLocation(iCurrentCard).Top = y  iCardOffsetY CardLocation(iCurrentCard).Bottom = CardLocation(iCurrentCard).Top + _ CARD_HEIGHT CardLocation(iCurrentCard).Right = CardLocation(iCurrentCard).Left + _ CARD_WIDTH ' Получаем неперекрытые прямоугольники. If CardLocation(iCurrentCard).Left = CardPrevRect.Left Then ' Перемещение по вертикали. SubtractRect UnRect1, CardPrevRect, CardLocation(iCurrentCard) SetRectEmpty UnRect2 ElseIf CardLocation(iCurrentCard).Top = CardPrevRect.Top Then ' Перемещение по горизонтали. SetRectEmpty UnRect1 SubtractRect UnRect2, CardPrevRect, CardLocation(iCurrentCard) ElseIf CardLocation(iCurrentCard).Left > CardPrevRect.Left And _ CardLocation(iCurrentCard).Top > CardPrevRect.Top Then ' Передвигаем вправо и вниз. UnRect1.Top = CardPrevRect.Top UnRect1.Bottom = CardLocation(iCurrentCard).Top UnRect1.Left = CardPrevRect.Left UnRect1.Right = CardPrevRect.Right UnRect2.Top = CardLocation(iCurrentCard).Top UnRect2.Bottom = CardPrevRect.Bottom UnRect2.Left = CardPrevRect.Left UnRect2.Right = CardLocation(iCurrentCard).Left ElseIf CardLocation(iCurrentCard).Left > CardPrevRect.Left And _ CardLocation(iCurrentCard).Top < CardPrevRect.Top Then ' Передвигаем вправо и вверх. UnRect1.Top = CardLocation(iCurrentCard).Bottom
3 UnRect1.Bottom = CardPrevRect.Bottom UnRect1.Left = CardPrevRect.Left UnRect1.Right = CardPrevRect.Right UnRect2.Top = CardPrevRect.Top UnRect2.Bottom = CardLocation(iCurrentCard).Bottom UnRect2.Left = CardPrevRect.Left UnRect2.Right = CardLocation(iCurrentCard).Left ElseIf CardLocation(iCurrentCard).Left < CardPrevRect.Left And _ CardLocation(iCurrentCard).Top > CardPrevRect.Top Then ' Передвигаем влево и вниз. UnRect1.Top = CardPrevRect.Top UnRect1.Bottom = CardLocation(iCurrentCard).Top UnRect1.Left = CardPrevRect.Left UnRect1.Right = CardPrevRect.Right UnRect2.Top = CardLocation(iCurrentCard).Top UnRect2.Bottom = CardPrevRect.Bottom UnRect2.Left = CardLocation(iCurrentCard).Right UnRect2.Right = CardPrevRect.Right ElseIf CardLocation(iCurrentCard).Left < CardPrevRect.Left And _ CardLocation(iCurrentCard).Top < CardPrevRect.Top Then ' Передвигаем влево и вверх. UnRect1.Top = CardLocation(iCurrentCard).Bottom UnRect1.Bottom = CardPrevRect.Bottom UnRect1.Left = CardPrevRect.Left UnRect1.Right = CardPrevRect.Right UnRect2.Top = CardPrevRect.Top UnRect2.Bottom = CardLocation(iCurrentCard).Bottom UnRect2.Left = CardLocation(iCurrentCard).Right UnRect2.Right = CardPrevRect.Right End If ' Закрашиваем неперекрытые прямоугольники в соответствии с таблицей ' цветности. FillRect Me.hdc, UnRect1, COLOR_BTNFACE + 1 FillRect Me.hdc, UnRect2, COLOR_BTNFACE + 1 ' Помещаем текущую карту на новое место. i = CardIndex(iCurrentCard) BitBlt Me.hdc, _ CardLocation(iCurrentCard).Left, _ CardLocation(iCurrentCard).Top, _ CARD_WIDTH, _ CARD_HEIGHT, _ frmCards.Deck.hdc, _ Работа с растровыми изображениями
30 Растровые изображения DECK_X _SPACING * (i Mod DECK_COL_COUNT), _ DECK_Y _SPACING * (i \ DECK_COL_COUNT), _ SRCCOPY ' Неперекрытые прямоугольники пересекают какиенибудь карты? ' Если да, перерисовываем карты. For i = 0 To DECK_CARD_COUNT  1 ' Пропускаем перемещаемую карту. If i = iCurrentCard Then GoTo NotThisCard ' Получаем прямоугольник карты. SetRect Card, CardLocation(i).Left, CardLocation(i).Top, _ CardLocation(i).Left + CARD_WIDTH, CardLocation(i).Top + CARD_HEIGHT ' Получаем пересечение карты с Unrect1. IntersectRect FixupRect, UnRect1, Card ' Если пересечение не пусто, перерисовываем FixupRect. If IsRectEmpty(FixupRect) = 0 Then BitBlt Me.hdc, _ FixupRect.Left, _ FixupRect.Top, _ FixupRect.Right  FixupRect.Left, _ FixupRect.Bottom  FixupRect.Top, _ frmCards.Deck.hdc, _ DECK_X _SPACING * (CardIndex(i) Mod DECK_COL_COUNT) + _ (FixupRect.Left  CardLocation(i).Left), _ DECK_Y _SPACING * Int(CardIndex(i) / DECK_COL_COUNT) + _ (FixupRect.Top  CardLocation(i).Top), _ SRCCOPY End If ' Получаем пересечение карты с Unrect2. IntersectRect FixupRect, UnRect2, Card ' Если пересечение не пусто, перерисовываем FixupRect. If IsRectEmpty(FixupRect) = 0 Then BitBlt Me.hdc, _ FixupRect.Left, _ FixupRect.Top, _ FixupRect.Right  FixupRect.Left, _ FixupRect.Bottom  FixupRect.Top, _ frmCards.Deck.hdc, _ DECK_X _SPACING * (CardIndex(i) Mod DECK_COL_COUNT) + _ (FixupRect.Left  CardLocation(i).Left), _ DECK_Y _SPACING * Int(CardIndex(i) / DECK_COL_COUNT) + _ (FixupRect.Top  CardLocation(i).Top), _ SRCCOPY End If
31 NotThisCard: Next ' Сохраняем до следующего раза. CardPrevRect = CardLocation(iCurrentCard) End Sub Использование растровых изображений в меню Если кто­нибудь спросит вас, зачем может понадобит­ ся программировать Win32 API на Visual Basic, вы всегда можете привести в качестве хорошего довода следующий пример. Здесь растровая картинка помещается в меню VB, как показано на рис. 21.7 . Ключом к решению этой задачи является функция ModifyMenu: BOOL ModifyMenu( HMENU hMnu, // Дескриптор меню. UINT uPosition, // Модифицируемый пункт меню. UINT uFlags, // Флаги пункта меню. UINT uIDNewItem, // Идентификатор пункта меню или дескриптор // выпадающего меню. LPCTSTR lpNewItem // Содержание пункта меню. ); Предполагается, что если параметр uFlags имеет значение MF_BITMAP, то параметр lpNewItem должен содержать дескриптор растрового изображения. Соответственно, объявление функции ModifyMenu в VB выглядит следующим образом (заметьте, что последний параметр объявлен с типом long): Declare Function ModifyMenu Lib «user32» Alias «ModifyMenuA» ( _ ByVal hMenu As Long, _ ByVal nPosition As Long, _ ByVal wFlags As Long, _ ByVal wIDNewItem As Long, _ ByVal lpNewitem As Long ) As Long Для получения дескриптора растрового изображения использована функция LoadImage: HANDLE LoadImage( HINSTANCE hinst, // Дескриптор экземпляра, содержащего изображение. LPCTSTR lpszName, // Имя идентификатора изображения. UINT uType, // Тип изображения. int cxDesired, // Заказанная ширина. int cyDesired, // Заказанная высота. UINT fuLoad // Флаги загрузки. ); Рис. 21.7 . Растровая картинка в меню Использование растровых изображений в меню
32 Растровые изображения Установив hInst в NULL, а uLoad – в LR_LOADFROMFILE, вы можете помес­ тить полное имя (путь и имя) файла растрового изображения в lpszName. Полный исходный код для выполнения этой работы представлен ниже: Public Sub MenuBitmap() Dim hMenu As Long Dim hSubMenu As Long Dim lMenuID As Long Dim hBitmap As Long Dim hImage As Long ' Получаем дескриптор меню верхнего уровня (top menu). hMenu = GetMenu(Me.hwnd) ' Имеем корректный дескриптор меню? If IsMenu(hMenu) = 0 Then MsgBox "Некорректный дескриптор меню", vbInformation Exit Sub End If ' Получаем дескриптор подменю 0 (меню File). hSubMenu = GetSubMenu(hMenu, 0) ' Имеем корректный дескриптор подменю? If IsMenu(hSubMenu) = 0 Then MsgBox "Некорректный дескриптор подменю", vbInformation Exit Sub End If ' Нужен ID пункта меню (пункт 1 – это следующий пункт). lMenuID = GetMenuItemID(hSubMenu, 1) ' Загружаем растровую картинку. hImage = LoadImage(0, "d:\bkapi\0code\atten.bmp", IMAGE_BITMAP, 0, 0, _ LR_LOADFROMFILE) ' Присоединяем картинку к меню. ModifyMenu hSubMenu, 1, MF_BITMAP Or MF_BYPOSITION, lMenuID, hImage End Sub Обратите внимание, что в прилагаемом архиве отсутствует файл atten.bmp, поэтому в своих экспериментах вы можете использовать любой bmp­файл соответ­ ствующих размеров.
Глава 22. Обзор контекстов устройств Ключевой концепцией GDI является контекст устройства (device context). Кон­ текст устройства использован Windows для того, чтобы сделать процедуру ри­ сования (под которой здесь подразумевается отображение и графики, и текста) настолько аппаратно­независимой, насколько это возможно. Контексты устройств применяются для воспроизведения графических объектов не только на физичес­ ких устройствах, таких как дисплей или принтер, но также в отдельном окне или даже на растровом изображении, которое хранится в памяти. Установлен следующий порядок действий при использовании контекста ус­ тройства: 1. Получить контекст устройства путем создания нового контекста или ис­ пользования существующего. В Windows есть набор готовых к применению контекстов устройств. Для рисования в окне или на экране можно исполь­ зовать функции GDI GetDC или GetWindowDC. Для работы с принтером требуется функция CreateDC. Свойство hDC в Visual Basic возвращает де­ скриптор контекста устройства, о чем будет говориться позже в данной главе. 2. Установить атрибуты контекста устройства и графические объекты, кото­ рые могут включать чертежное перо, кисть для рисования, шрифт и т.д . 3. Применить методы рисования к контексту устройства, включая вывод текста. 4. Удалить (используя DeleteDC) или освободить (при помощи ReleaseDC) данный контекст устройства. Несколько позже эти шаги будут рассматриваться более подробно. Здесь же важно подчеркнуть, что контекст устройства – это один из способов достижения аппаратной независимости. В частности, программа рисования, созданная для одного контекста устройства, например экрана, может быть затем переделана для работы с другими контекстами устройств. Нужно только изменить значение аргу­ мента функций рисования, который задает контекст устройства. Следовательно, можно использовать одну и ту же программу как для рисования на экране, так и для печати. Функция Ellipse, например, предназначена для рисования эллипсов. У нее пять параметров. Четыре из них задают ограничивающий прямоугольник, ко­ торый охватывает эллипс, и один – контекст устройства, на котором рисуется эллипс. Таким образом, изменяя один аргумент, можно рисовать один и тот же эллипс в разных контекстах. (Это станет понятнее, после того как вы выполните несколько конкретных примеров.)
34 Обзор контекстов устройств Как Windows управляет рисованием окна Чтобы разобраться в Windows GDI, нужно уяснить, как Windows определяет, когда следует нарисовать (или перерисовать) часть окна. И здесь не обойтись без понятия область. Словарь областей Область (region) – это объект, который состоит из объединения одного или нескольких прямоугольников, многоугольников или эллипсов. Области имеют дескрипторы и могут заполняться, группироваться, перемещаться, инвертиро­ ваться и т.д . В качестве примера рассмотрим эллиптическую область, создав ее с помощью соответствующей функции, которая возвращает дескриптор созданной области. HRGN CreateEllipticRgn( int nLeftRect, // Xкоордината верхнего левого угла // рабочего прямоугольника. int nTopRect, // Yкоордината верхнего левого угла // рабочего прямоугольника. int nRightRect, // Xкоордината нижнего правого угла // рабочего прямоугольника. int nBottomRect // Yкоордината нижнего правого угла // рабочего прямоугольника. ); Кстати, создание эллиптической области не следует путать с рисованием эл­ липса в окне, осуществляющегося с помощью функции Ellipse. Созданный таким образом объект представляет собой фигуру (shape) эллипса, а не эллипти­ ческую область. Области объединяются при помощи функции CombineRgn: int CombineRgn( HRGN hrgnDest, // Дескриптор области назначения. HRGN hrgnSrc1, // Дескриптор исходной области. HRGN hrgnSrc2, // Дескриптор исходной области. int fnCombineMode // Режим объединения областей. ); Параметр fnCombineMode может принимать одно из следующих значений, которые уточняют режим объединения:  RGN_AND создает пересечение (общая часть) исходных областей;  RGN_COPY копирует исходную область в целевую;  RGN_DIFF определяет ту часть hrgnSrc1, которая не принадлежит hrgnSrc2. В математике это называется разностью двух множеств;  RGN_OR объединяет исходные области;  RGN_XOR образует все те части объединения исходных областей, которые не принадлежат этим областям одновременно. В теории множеств это называ­ ется строгой дизъюнкцией (исключающее ИЛИ) двух областей.
35 Помимо того, что области можно объединять, их можно заливать каким­либо цветом, используя функции FillRgn или PaintRgn, инвертировать при помо­ щи InvertRgn, группировать, применяя FrameRgn, и перемещать при помощи OffsetRgn. Область, требующая перерисовки Область окна, требующая перерисовки, (update region) представляет собой такую область, которая в данный момент содержит устаревшие или неверные данные, и поэтому требует перерисовки. Видимая область Видимой областью (visible region) является, как правило, та область, которая видима для пользователя. Однако стили WS_CLIPCHILDREN и WS_CLIPSIBLINGS влияют на то, как она трактуется. Если окно имеет стиль WS_CLIPCHILDREN, то видимая область не включает в себя никаких окон­потомков. Если окно имеет стиль WS_CLIPSIBLINGS, то видимая часть окна не содержит частей, которые закрыты «братскими» (sibling) окнами. Область отсечения Область отсечения (clipping region) представляет собой подобласть клиент­ ской области окна, в которой в данный момент разрешено рисование. Иначе гово­ ря, если начать рисовать в окне, то Windows будет урезать рисунок таким образом, чтобы он не выходил за пределы области отсечения. Когда приложение путем вызова одной из GDI функций – BeginPaint, GetDC или GetDCEx – получает контекст устройства дисплея, система устанавливает в качестве области отсечения для этого контекста область пересечения видимой об­ ласти и области, требующей перерисовки. Таким образом, единственной частью окна, в которой разрешено рисование, является та часть, изменения которой видны. Область окна Данный термин появляется в документации Microsoft также в следующем контексте: область окна определяет участок внутри окна, в котором операционная система разрешает отображение. Система не отображает те части окна, которые лежат за пределами области. Это не совсем то же самое, что обозначается областью отсечения, поскольку область окна, по­видимому, не ограничена клиентской областью. Во всяком случае, данные термины могут путаться в документации, поэтому нужно обратить на это внимание. Функции, влияющие на области окна API GDI имеет множество функций, которые влияют на различные области окон (помимо функций объединения, заполнения, инвертирования и перемеще­ ния областей, обсуждавшихся ранее). GetUpdateRect и GetUpdateRgn Функция GetUpdateRect возвращает координаты наименьшего прямоуголь­ ника, полностью включающего область заданного окна, требующую перерисовки. Как Windows управляет рисованием окна
3 Обзор контекстов устройств Функция GetUpdateRgn возвращает саму область окна, нуждающуюся в перери­ совке, копируя ее в указанный пространственный объект. GetWindowRgn и SetWindowRgn Функция GetWindowRgn извлекает область окна (делает ее копию). Функция SetWindowRgn устанавливает область окна. Функцию SetWindowRgn можно использовать для создания непрямоуголь­ ных окон, в частности, для придания видимой части окна непрямоугольной формы. Это можно очень просто проверить, создав проект VB с двумя формами – Form1 и Form2. Затем свяжите следующий код с кнопкой на Form1 (конечно, потребуется также добавить соответствующие декларации): ' Загружаем form2 и изменяем область окна. Dim hrgn As Long hrgn = CreateEllipticRgn(0, 0, 100, 200) Form2.Show SetWindowRgn Form2.hWnd, hrgn, 1 InvalidateRect и ValidateRect Функция InvalidateRect переводит прямоугольник в недействительное (invalid) состояние, добавляя его к области окна, требующей перерисовки. Фун­ кция ValidateRect выводит прямоугольник из недействительного состояния, удаляя его из области окна, нуждающейся в перерисовке. InvalidateRgn и ValidateRgn Функция InvalidateRgn переводит область в недействительное состояние, добавляя ее к области окна, требующей перерисовки. Функция ValidateRgn выводит область из недействительного состояния, удаляя ее из области окна, нуждающейся в перерисовке. RedrawWindow Функцию RedrawWindow можно использовать для выполнения разных опе­ раций, включая перевод данной области в недействительное или действительное состояния. UpdateWindow Функция UpdateWindow обновляет клиентскую область окна, посылая ему сообщение WM_PAINT. Область, требующая перерисовки, и сообщения WM_PAINT Как вам уже известно, область окна, требующая перерисовки, представляет собой область, которая в данный момент содержит неверные данные и поэтому нуждается в перерисовке. Безусловно, эта область постоянно изменяется. На­ пример, если пользователь при передвижении окна частично закрывает то окно, о котором идет речь, то Windows добавит перекрытую часть окна к области, тре­ бующей перерисовки. Если окно сворачивается, то все данные окна устаревают, и оно добавляется к области, требующей перерисовки.
3 При совершении любых действий, затрагивающих содержимое окна (пер­ вичном его создании, свертывании, изменении размеров или при перекрывании другим окном) прежнее содержимое окна не сохраняется, так как сохранение содержимого всех таких окон потребовало бы слишком большого количества ресурсов. Вместо этого Windows переводит затронутую часть окна в недействи­ тельное состояние и добавляет ее к области, требующей перерисовки. Затем система посылает сообщение WM_PAINT оконной процедуре данного окна. Вспомните, как GetMessage управляет сообщениями WM_PAINT. Данные сообщения имеют очень низкий приоритет и их обходят как синхронные, так и асинхронные сообщения. Заметьте, что включать в оконную процедуру код, выполняющий все необ­ ходимые действия, которые связаны с перерисовкой соответствующих областей окна, должен программист. Область, требующая перерисовки, может быть опре­ делена посредством вызова функции GetUpdateRgn. Даже если Visual Basic выполняет эту работу за вас, стоит рассмотреть этот вопрос более внимательно. Обычно программист начинает код, обрабатывающий сообщение WM_PAINT, с вызова функции BeginPaint. Она получает контекст устройства, который не­ обходим для вызова различных API­функций рисования. Windows устанавливает в качестве области отсечения (clipping region) пересечение видимой области и области, требующей перерисовки. Затем система записывает в структуру PAINTSTRUCT по­ ложение области окна, нуждающейся в перерисовке. Затем функция BeginPaint устанавливает статус этой области в NULL, то есть переводит ее из недействи­ тельного состояния в действительное, так что дальнейшая генерация сообщений WM_PAINT прекращается. Неклиентская область Как и следовало ожидать, Windows не вмешивается в перерисовку клиентской области окна, поскольку ей ничего не известно о содержимом этой области. Одна­ ко сказанное не касается неклиентской части окна. Данную часть перерисовывает функция DefWindowProc, которая устанавливается по умолчанию и вызывает­ ся в конце оконной процедуры. Если приложение тем не менее готово занять­ ся этой рутинной работой, оно может самостоятельно обрабатывать сообщения WM_NCPAINT и WM_ERASEBKGND. Контексты устройств Контекст устройства (Device Context – DC) представляет собой объект, исполь­ зуемый для выполнения процедуры рисования (и графики, и текста) на целевом ус­ тройстве. Целевые устройства могут быть виртуальными (virtual), такими как блок памяти или окно, или физическими (physical), такими как экран или принтер. У контекстов устройств есть атрибуты, например, цвет фона. Некоторые атри­ буты являются графическими объектами. Графические объекты GDI перечислены ниже:  растровое изображение;  кисть;  палитра; Контексты устройств
3 Обзор контекстов устройств  шрифт;  путь (path);  перо;  область. Процесс привязки графического объекта к контексту устройства прост: 1. Создать графический объект с помощью API­функции, такой как CreatePen, CreateBrushIndirect или CreateBitmap, или использовать стандарт­ ный (stock) объект. (Windows поддерживает наборы стандартных перьев и кистей.) Свойства графического объекта задаются при его создании. На­ пример, функция CreatePen имеет параметры, которые определяют ши­ рину и цвет пера. 2. Выбрать созданный объект в контексте устройства с помощью функции SelectObject точно так, как какое­либо перо для рисования. Можно использовать сочетание функции GetCurrentObject и GetObject для получения информации о выбранном в текущий момент объекте заданного типа в контексте устройства. Функция GetCurrentObject извлекает дескриптор выбранного объекта, а функция GetObject заполняет структуру информацией об объекте, дескриптор которого был получен функцией GetCurrentObject. Заметьте, что для изменения атрибутов, используемых в настоящий момент контекстом устройства, таких как цвет пера, потребуется создать и выбрать новое перо в данном контексте устройства или использовать другое стандартное перо, удалив предыдущее. В Windows 2000 реализованы функции, подобные SetDCPenColor, которые могут непосредственно изменять цвет выбранного в текущий момент пера, снимая, таким образом, часть работы с программиста. Использование контекста устройства Порядок действий при работе с контекстом устройства описывался в самом начале главы. На рис. 22.1 показан контекст устройства. Шрифт Область отсечения Палитра Растр Перо Кисть Путь Режим отображения, режим заливки много угольника, точка отсчета позиции наблюдения и т.д. GetDC(hWnd) Окно Контекст устройства Рис. 22 .1. Контекст устройства, выбранный для некоторого окна
3 Следующая далее процедура рисует красный эллипс с черным заполнением внутри вокруг командной кнопки. Так как графический вывод осуществляется в слой, расположенный ниже управляющего элемента, командная кнопка отобра­ жается поверх нарисованного эллипса. Sub EllipseIt() Dim hPen As Long Dim hBrush As Long Dim hDCForm As Long Dim r As RECT Dim iDC As Long ' Получаем контекст устройства для формы. hDCForm = Form1.hdc ' Сохраняем его для последующего восстановления. iDC = SaveDC(hDCForm) ' Задаем черный цвет кисти (заливки). hBrush = GetStockObject(BLACK_BRUSH) SelectObject hDCForm, hBrush ' А для пера – красный цвет. hPen = CreatePen(PS_SOLID, 3, &HFF) SelectObject hDCForm, hPen ' Получаем координаты прямоугольника, окружающего кнопку. r.Left = Command1.Left \ Screen.TwipsPerPixelX r.Top = Command1.Top \ Screen.TwipsPerPixelY r.Right = (Command1.Left + Command1.Width) \ Screen.TwipsPerPixelX r.Bottom = (Command1.Top + Command1.Height) \ Screen.TwipsPerPixelY ' Рисуем прямоугольник. Ellipse hDCForm, r.Left  50, r.Top  50, r.Right + 50, r.Bottom + 50 ' Обновляем. Me.Refresh ' Удаляем перо. DeleteObject hPen ' Восстанавливаем контекст устройства (DC). RestoreDC hDCForm, iDC End Sub Свойства, устанавливаемые по умолчанию Чтобы вы получили некоторое представление о типах атрибутов, принадлежа­ щих контексту устройства, в табл. 22.1 показаны значения, устанавливаемые по Контексты устройств
30 Обзор контекстов устройств умолчанию для вновь создаваемого контекста устройства. Несколько позже будут рассмотрены некоторые из этих атрибутов. Таблица 22.1. Значения атрибутов контекста устройства, устанавливаемого по умолчанию Атрибут Значение, устанавливаемое по умолчанию Цвет фона Цвет фона, установленный в Панели управления (Control Panel) Режим фона OPAQUE Растровое изображение Отсутствует Кисть WHITE_BRUSH (стандартная кисть) Начало координат кисти (0,0) Область отсечения Все окно Палитра DEFAULT_PALETTE Текущая позиция пера (0,0) Начало координат устройства Верхний левый угол окна или клиентской области Режим рисования R2_COPYPEN Шрифт SYSTEM_FONT Расстояние между символами 0 Режим отображения MM_TEXT Перо BLACK_PEN (стандартное перо) Режим заливки многоугольника ALTERNATE Режим растяжения (stretch) BLACKONWHITE Цвет текста Цвет текста, установленный в Панели управления Протяженность области вывода (1,1) Начало координат области вывода (0,0) Протяженность окна (1,1) Начало координат окна (0,0) Режимы контекста устройства Контексты устройства имеют режимы (mode), которые влияют на то, как вы­ полняются некоторые операции. Перечень режимов приведен в табл. 22.2 . Таблица 22.2 . Режимы контекста устройства Режим Описание Set/Get Режим фона Определяет, как цвета фона смешиваются SetBkMode, с существующими цветами окна или экрана GetBkMode при выполнении операций с растровым изображением и текстом Режим рисования Определяет, как цвета фона смешиваются SetROP2, с существующими цветами окна или экрана GetROP2Mode при выполнении операций с пером, кистью, растровым изображением и текстом
31 Таблица 22.2 . Режимы контекста устройства (окончание) Режим Описание Set/Get Режим отображения Определяет, как вывод графики отображается SetMapMode, из логических координат в координаты GetMapMode устройства Режим заливки Определяет, как шаблон кисти используется SetPolyFillMode, многоугольника для заливки внутренней части составных GetPolyFillMode областей Режим растяжения Определяет, как цвета растрового SetStretchBltMode, изображения смешиваются с существующими GetStretchBltMode цветами окна или экрана при уменьшении его масштаба Контексты устройства в Visual Basic Формы и рамки с изображениями в Visual Basic обладают свойством hDC, которое возвращает частный (private) контекст устройства указанных объектов (данный термин разъясняется в главе 23). Можно также получить дескриптор кон­ текста устройства этих объектов с помощью функции GetDC. Как вы увидите, де­ скрипторы, возвращаемые свойством hDC и функцией GetDC, могут относиться как к одному и тому же контексту (DC), так и к разным контекстам. Это зависит от установок свойства AutoRedraw. Обратите внимание, что необходимо с помощью функции SaveDC сохранить состояние контекста устройства VB, полученного посредством свойства hDC, и затем восстановить это состояние с помощью RestoreDC после завершения работы с контекстом, как это было сделано в предыдущем примере. Свойство AutoRedraw Установка свойства AutoRedraw определяет, будет ли Windows при необхо­ димости автоматически перерисовывать графическое изображение, когда изме­ няются размеры окна вывода или оно перекрывается другими окнами, а затем снова открывается. В отличие от окон вывода графики окна­потомки, такие как управляющие элементы, всегда автоматически перерисовываются. Графика, об­ новляющаяся автоматически, называется постоянной (persistent). Для того чтобы Windows могла поддерживать постоянную графику, вывод изображений должен где­то храниться на время работы приложения. Это храни­ лище называется постоянным растром (persistent bitmap) и находится в памяти. Важно отметить, что формы и рамки с изображениями могут иметь два, свя­ занных с ними, отображения растра в память. Одним их них является изображение фона (background bitmap), которое используется для «очистки» окна. Оно хранит­ ся в памяти, если было установлено свойство Picture формы или управляющего элемента. Когда AutoRedraw принимает значение True, возникает также и посто­ янное растровое изображение, которое принимает вывод графики. Таким образом, установка этого свойства оказывает глубокое воздействие на режим отображения. Давайте рассмотрим его более подробно. Контексты устройств
32 Обзор контекстов устройств Свойство hDC, функция GetDC и вывод графики Если AutoRedraw установлено в True, значит, создано постоянное растровое изображение. В этом случае свойство hDC ссылается на его контекст устройства. Если AutoRedraw имеет значение False, то постоянного растрового изображе­ ния не существует и свойство hDC ссылается на контекст устройства того окна, в которое осуществляется вывод. В любом случае возвращаемое значение функ­ ции GetDC – это дескриптор контекста устройства целевого окна (API ничего не известно о постоянных растровых изображениях VB). Из этого следует, что API­функции рисования всегда будут направлять свой вывод целевому окну, в то время как функции рисования VB посылают свой вы­ вод тому объекту, на который указывает свойство hDC и который может быть и целевым окном, и постоянным растровым изображением. Подпрограмма HDCExample, приведенная ниже, иллюстрирует этот факт. Во­первых, здесь устанавливается AutoRedraw и затем исполь­ зуется свойство hDC рамки с изображением pic для выбора белой кисти. Затем рисуется прямо­ угольник (см. рис. 22.2), закрашенный белым цветом. Обратите внимание, что следует вызы­ вать метод Refresh для рамки с изображением, для того чтобы VB перенес вывод с постоянного растра на само изображение в рамке. Далее вы сбрасываете AutoRedraw и рисуе­ те еще два прямоугольника – один при помощи свойства hDC, другой с использованием дескрип­ тора контекста устройства, полученного от фун­ кции GetDC. И в том, и в другом случае прямо­ угольники не закрашены белой кистью (см. рис. 22.2), поскольку применялся контекст устройства рамки с изображением, а не постоянного растрового изображения. Private Sub HDCExample() Dim hDCPic As Long ' Включаем AutoRedraw. pic.AutoRedraw = True Debug.Print "hDC:" & pic.hdc ' Задаем для кисти белый цвет. SelectObject pic.hdc, GetStockObject(WHITE_BRUSH) ' Пририсовываем прямоугольник постоянному растровому изображению. SelectObject pic.hdc, 0, 0, 100, 100 ' Это нужно. pic.Refresh Рис. 22 .2 . AutoRedraw и GetDC
33 ' Выключаем AutoRedraw. pic.AutoRedraw = False ' Получаем контекст устройства для окна. hDCPic = GetDC(pic.hwnd) Debug.Print "GetDC:" & hDCPic ' Рисуем прямоугольники. Rectangle pic.hdc, 100, 100, 200, 200 Rectangle hDCPic, 200, 200, 300, 300 End Sub Вывод оператора Debug.Print показывает, что функция GetDC возвращает дескриптор, отличный от указываемого свойством hDC: hDC:469695987 GetDC:1946224922 Следовательно, когда AutoRedraw имеет значение True, проблема возникает, если пытаться использовать и API­функции рисования при помощи дескриптора контекста устройства (полученного от функции GetDC), и функции рисования VB, задействуя свойство hDC. Visual Basic будет перерисовывать окно данными, хранящимися в постоянном растровом изображении в произвольные моменты времени, стирая любую графику, которая была выведена непосредственно в окно через контекст устройства, полученный от GetDC. События Redraw и Paint Когда окно VB, например рамка с изображением, по каким­то причинам (из­ менение размера, изменение положения относительно других окон, вызов метода Refresh) требует перерисовки, действие может развиваться по двум сценари­ ям. Если AutoRedraw равно True, то VB будет автоматически перерисовывать окно, используя постоянное растровое изображение. Для этого и предназначено свойство AutoRedraw. Более того, не будет возбуждаться событие Paint. Если же AutoRedraw равно False, то VB просто возбуждает событие Paint, оставляя заботу о перерисовке на усмотрение программиста. Если требуется создать постоянную графику при AutoRedraw, установленном в False, нужно поместить код, осуществляющий рисование, в событие Paint целевого окна. Заметьте, что в тех примерах, которые будут приводиться в следующих раз­ делах, использована рамка с изображением, у которой свойство AutoRedraw установлено в False, но нет ни одного случая размещения программы рисования в событии Paint. Свойства Picture, Image и метод CLS Согласно определению, свойство Picture управляющего элемента VB возвра­ щает дескриптор растрового изображения фона данного окна. С другой стороны, свойство Image предназначено возвращать дескриптор постоянного растрового изображения. Если его не существует, то VB создает постоянное растровое изоб­ Контексты устройств
34 Обзор контекстов устройств ражение, которое является копией растрового изображения фона (но это разные изображения). Затем свойство Image возвращает дескриптор вновь созданного постоянного растрового изображения. Вспомните, что метод Visual Basic Cls базируется на свойстве hDC. Таким образом, когда AutoRedraw равно True, постоянное растровое изображение очи­ щается цветом фона, что отражается и в окне. Когда AutoRedraw равно False, очищается само окно, и в нем отображается постоянное растровое изображение. Перья Перья (pens) представляют собой графические объекты, используемые для черчения линий и кривых. Они требуются многим функциям рисования. Напри­ мер, функции LineTo: BOOL LineTo( HDC hdc, // Дескриптор контекста устройства. int nXEnd, // Xкоордината конечной точки линии. int nYEnd // Yкоордината конечной точки линии. ); Эта функция рисует линию, используя то перо, которое в данный момент вы­ брано в контексте устройства. Windows GDI поддерживает два типа перьев: косметические и геометрические. Косметические перья Косметическое перо (cosmetic pen) используется для быстрых процедур ри­ сования. Оно требует меньше ресурсов, чем геометрическое, но имеет всего три атрибута: ширину, стиль и цвет. Для создания косметического пера можно использовать функции CreatePen, CreatePenIndirect или ExtCreatePen. Windows также поддерживает три стандартных косметических пера, обозначаемых BLACK_PEN, WHITE_PEN и DC_ PEN (только Windows 98/2000), которые доступны при использовании функции GetStockObject. Декларация функции CreatePen выглядит так: HPEN CreatePen( int fnPenStyle, // Стиль пера. int nWidth, // Ширина пера. COLORREF crColor // Цвет пера. ); где COLORREF является значением в форме &Hbbggrr, а 16­разрядное значение bb задает голубую составляющую цвета (от 0 до &HFF). То же самое можно сказать о зеленом и красном цветах. Например, &H0000FF – чистый красный цвет. В VB можно использовать следующую декларацию: Declare Function CreatePen Lib "gdi32" ( _ ByVal fnPenStyle As Long, _ ByVal nWidth As Long, _ ByVal crColor As Long _ ) As Long
35 Параметр fnPenStyle имеет одно из следующих значений:  PS_SOLID обозначает сплошное перо;  PS_DASH представляет штрих­линейное перо. Используется только для пера шириной в один пиксел;  PS_DOT определяет штрих­пунктирное перо. Применяется только для пера шириной в один пиксел;  PS_DASHDOT обозначает штрих­линейно­пунктирное перо (попеременно используются штрихи и точки). Применяется только для пера шириной в один пиксел;  PS_DASHDOTDOT представляет штрих­линейно­пунктирно­пунктирное перо (попеременно используются штрих и две точки). Применяется только для пера шириной в один пиксел;  PS_NULL определяет невидимое перо;  PS_INSIDEFRAME представляет сплошное перо. При использовании его с функцией рисования, которая принимает ограничивающий прямоугольник, например, с функцией Ellipse, размеры фигуры (эллипса) сокращаются так, чтобы она полностью помещалась внутри ограничивающего прямо­ угольника с учетом ширины пера. Применяется только к геометрическим перьям. Параметр nWidth задает ширину пера в логических единицах (logical unit) (объясняется позже). Чтобы создать линию шириной в один пиксел, можно задать значение этого параметра равным нулю. Геометрические перья В то время как косметические перья рисуют очень быстро (в 3–10 раз быст­ рее, чем геометрические), геометрические перья (geometric pens) являются более гибкими, имея такие свойства, как ширина, стиль, шаблон, необязательная штри­ ховка, стиль завершения (end style) и стиль соединения (join style). Геометрическое перо создается с помощью функции ExtCreatePen: HPEN ExtCreatePen( DWORD dwPenStyle, // Стиль пера. DWORD dwWidth, // Ширина пера. CONST LOGBRUSH *lplb, // Указатель на структуру атрибутов кисти. DWORD dwStyleCount, // Длина массива, содержащего биты // пользовательского стиля. CONST DWORD *lpStyle // Необязательный массив битов // пользовательского стиля. ); Эта функция допускает создание косметических или геометрических перьев, определяемых включением битов стиля PS_COSMETIC или PS_GEOMETRIC в ар­ гументе dwPenStyle. Кисти Кисть (brush) представляет собой графический объект, который используется для закрашивания внутренней части многоугольников, эллипсов и путей. Контексты устройств
3 Обзор контекстов устройств Windows различает логические и физические кисти. Функции GDI, которые создают кисть, возвращают дескриптор логической кисти. Однако, когда вы вы­ бираете эту кисть в контексте устройства, используя функцию SelectObject, драйвер данного устройства создает физическую кисть, которой и осуществляется рисование. (В конце концов, черно­белый принтер не может использовать, напри­ мер, красную логическую кисть.) Начало координат кисти Необходимо понимать, что такое начало координат кисти (brush origin). На Рис. 22.3 иллюстрируется важность этого понятия. Начало координат представляет собой тот пиксел кисти, который помещен на первый пиксел рисуемого объекта в начальный момент рисования. На рис. 22.3 показано, что происходит, когда вы хотите рисовать в окне и на управляющем эле­ менте окна, используя одну и ту же кисть. Этот процесс включает две отдельных операции рисования. В окне Window1 на рис. 22.3 начало координат установлено на левый верхний пиксел кисти для обеих операций рисования. Но, когда начало координат кисти Control1 Window1 Кисть Начало координат кисти для окна Начало координат кисти для управля ющего элемента Начало координат кисти для окна Начало координат кисти для управля ющего элемента Control2 Window2 Рис. 22 .3. Начало координат кисти
3 выравнивается по левому верхнему углу управляющего элемента для начала ри­ сования на нем, двоичный шаблон управляющего элемента не точно совпадает с шаблоном окна. Как показано в окне Window2 на рис. 22.3, чтобы выровнять шаблоны, нуж­ но изменить начало координат кисти при рисовании на управляющем элементе таким образом, чтобы оно приходилось на пиксел в первом столбце второго ряда пикселов кисти. Начало координат кисти устанавливается с помощью функции SetBrushOrgEx: BOOL SetBrushOrgEx( HDC hdc, // Дескриптор контекста устройства. int nXOrg, // Xкоордината нового начала координат. int nYOrg // Yкоордината нового начала координат. LPPOINT lppt // Указывает на предыдущее начало координат кисти. ); Типы кисти В Windows GDI реализовано четыре типа кисти: сплошная (solid), стандарт- ная (stock), шаблонная (pattern) и штриховая (hatch). Сплошная кисть – это логическая кисть, которая содержит 64 пиксела одного цвета. Сплошные кисти создаются с помощью функции CreateSolidBrush. Стандартная кисть изначально содержится в Windows GDI, которая подде­ рживает семь предопределенных стандартных кистей. Такая кисть может быть получена с помощью функции GetStockObject: HGDIOBJ GetStockObject( int fnObject // Тип стандартного объекта. ); Эта функция возвращает дескриптор объекта GDI (HGDIOBJ). Параметр fnObject может принимать одно из значений, приведенных в табл. 22.3 (в таб­ лицу включены значения для всех семи стандартных кистей GDI). Таблица 22.3. Типы стандартных объектов Значение fnObject Значение BLACK_BRUSH Черная кисть DKGRAY_BRUSH Темносерая кисть DC_BRUSH Кисть сплошного цвета (Windows 98/2000) GRAY_BRUSH Серая кисть HOLLOW_BRUSH То же самое, что и NULL_BRUSH LTGRAY_BRUSH Светлосерая кисть NULL_BRUSH Кисть Null WHITE_BRUSH Белая кисть BLACK_PEN Черное перо DC_PEN Перо сплошного цвета (Windows 98/2000) WHITE_PEN Белое перо Контексты устройств
3 Обзор контекстов устройств Таблица 22.3. Типы стандартных объектов (окончание) Значение fnObject Значение ANSI_FIXED_FONT Системный моноширинный шрифт Windows ANSI_VAR_FONT Системный пропорциональный шрифт Windows DEVICE_DEFAULT_FONT Аппаратнозависимый шрифт (Windows NT) DEFAULT_GUI_FONT Шрифт по умолчанию для объектов пользовательского интерфейса, таких как меню и диалоговые окна OEM_FIXED_FONT Моноширинный шрифт, задаваемый производителем аппаратуры SYSTEM_FONT Системный шрифт (используется для рисования меню, управляющих элементов диалоговых окон и текста) SYSTEM_FIXED_FONT Системный моноширинный шрифт, используемый в Windows 3.0 и более ранних версиях DEFAULT_PALETTE Палитра, устанавливаемая по умолчанию Библиотека Windows USER32.dll также поддерживает коллекцию из 21 стан­ дартной кисти. Эти кисти соответствуют цветам элементов окон – меню, полосы прокрутки и кнопки. Функцию GetSysColorBrush можно использовать для получения дескриптора одной из этих стандартных кистей. Заметьте, что стандартные кисти не требуют, чтобы их удаляли после заверше­ ния работы, впрочем, от их удаления никакого вреда не будет. Штриховая кисть рисует шаблоны с вертикальными, горизонтальными и/или диагональными линиями. Windows GDI поддерживает шесть предопределенных штриховых кистей. Шаблонная кисть создается на основе растровой картинки. Чтобы создать ло­ гическую шаблонную кисть, сначала следует создать растровую картинку и затем вызвать CreatePatternBrush или CreateDIBPatternBrushPt для создания собственно шаблонной кисти. Пути Путь (path) – это последовательность из одной или нескольких фигур (или форм), которые заполнены или обведены (либо и то, и другое вместе). Заметьте, что в отличие от пера или кисти у путей нет дескрипторов, и они не создаются и не уничтожаются, как другие графические объекты. В данный момент времени в контексте устройства может быть только один путь. Если выбирается новый путь, то предыдущий выбор пути сбрасывается. Для создания и выбора пути сначала необходимо определить те точки, которые описывают этот путь. Ниже представлен порядок действий: 1. Вызвать функцию BeginPath. 2. Вызвать соответствующие функции рисования. 3. Вызвать функцию EndPath. На эту последовательность вызовов функций ссылаются как на скобки пути (path bracket). Функция BeginPath выглядит так:
3 BOOL BeginPath( HDC hdc // Дескриптор контекста устройства. ); Функция EndPath на нее похожа: BOOL EndPath( HDC hdc // Дескриптор контекста устройства. ); Функции рисования, которые можно использовать в скобках пути, показаны в табл. 22.4 . Таблица 22.4 . Функции рисования, используемые в скобках пути Имена функций AngleArc LineTo Polyline Arc MoveToEx PolylineTo ArcTo Pie PolyPolygon Chord PolyBezier Polyline CloseFigure PolyBezierTo Rectangle Ellipse PolyDraw RoundRect ExtTextOut Polygon TextOut С выбранным путем можно осуществлять различные операции:  вычерчивание контура пути с помощью текущего пера;  заполнение внутренней части пути с использованием текущей кисти;  преобразование кривых пути в сегменты линий;  преобразование пути в путь отсечения (clip path);  преобразование пути в область (region). Контексты устройств
Глава 23. Типы контекстов устройств В настоящее время существует четыре разных типа контекстов устройства:  Display. Контексты устройства данного типа используются для рисования в окнах или на экране;  Printer. Контексты устройства данного типа применяются для рисования на принтере или плоттере;  Memory. Контексты устройства данного типа поддерживают операции рисо­ вания на растровом изображении в памяти;  Information. Контексты устройства этого типа обеспечивают поиск данных устройства и поэтому используют меньше ресурсов, чем контексты устройс­ тва, предназначенные для рисования. Давайте обсудим каждый из перечисленных типов контекста. Информационные контексты устройства Win32 GDI поддерживает специальный тип контекста устройства, называемый информационным контекстом устройства (information device context). Он исполь­ зуется для извлечения данных устройства, устанавливаемых по умолчанию. Инфор­ мационные контексты устройства относятся к «облегченному» виду, для которого не требуется слишком много ресурсов. Их нельзя использовать для выполнения операций рисования, они выполняют строго информационные функции. Информа­ ционный контекст устройства создается с помощью функции CreateIC: HDC CreateIC( LPCTSTR lpszDriver, // Указатель строки, определяющей имя драйвера. LPCTSTR lpszDevice, // Указатель строки, определяющей имя // устройства. LPCTSTR lpszOutput, // Указатель строки, определяющей порт или // имя файла. CONST DEVMODE *lpdvmInit // Указатель на необязательные // инициализирующие данные. ); Структура DEVMODE содержит различную информацию об устройстве: Type DEVMODE dmDeviceName As String * CCHDEVICENAME ' CCHDEVICENAME = 32 dmSpecVersion As Integer dmDriverVersion As Integer dmSize As Integer
31 dmDriverExtra As Integer dmFields As Long dmOrientation As Integer dmPaperSize As Integer dmPaperLength As Integer dmPaperWidth As Integer dmScale As Integer dmCopies As Integer dmDefaultSource As Integer dmPrintQuality As Integer dmColor As Integer dmDuplex As Integer dmYResolution As Integer dmTTOption As Integer dmCollate As Integer dmFormName As String * ' CCHFORMNAME = 32 dmUnusedPadding As Integer dmBitsPerPel As Long dmPelsWidth As Long dmPelsHeight As Long dmDisplayFlags As Long dmDisplayFrequency As Long dmICMMethod As Long dmICMIntent As Long dmMediaType As Long dmDitherType As Long dmReserved1 As Long dmReserved2 As Long End Type В качестве примера рассмотрим следующую программу, которая создает инфор­ мационный контекст принтера и затем с помощью функций GetCurrentObject и GetObject получает название гарнитуры шрифта, установленного для данного принтера по умолчанию. Sub GetPrinterInfo() Dim dc As Long Dim hObj As Long Dim f As LOGFONT dc = CreateIC(vbNullString, "HP LaserJet 4000 Series PS", vbNullString, 0) hObj = GetCurrentObject(dc, OBJ_FONT) GetObjectAPI hObj, LenB(f), f Debug.Print "Face:" & StrConv(f.lfFaceName, vbUnicode) DeleteDC dc End Sub Информационные контексты устройства
32 Типы контекстов устройств Контексты устройства памяти Win32 GDI поддерживает специальный тип контекста устройства, называемый контекстом устройства памяти (memory device context). Данный тип использу­ ется для рисования на растровом изображении в памяти. Подобное изображение должно быть совместимо с другими контекстами устройств, то есть иметь те же размеры и тот же цветовой формат, что и растровое изображение, связанное с устройством. По этой причине контекст памяти иногда называют совместимым контекстом устройства (compatible DC). Контекст памяти можно создать с по­ мощью вызова функции CreateCompatibleDC: HDC CreateCompatibleDC( HDC hdc // Дескриптор контекста устройства. ); Эта функция создает контекст памяти, совместимый с контекстом устройства, заданным параметром hdc. Ее возвращаемое значение – дескриптор контекста памяти. Важно отметить, что, когда контекст памяти создается впервые, его растровое изображение – это просто заполнитель (placeholder) размером 1×1 пиксел. Прежде чем начать рисовать, необходимо выбрать растровое изображение в контексте уст­ ройства с подходящими шириной и высотой, вызвав функцию SelectObject. Одним из основных применений контекста памяти является сохранение растро­ вого изображения на время выполнения других операций. Например, как вам уже известно, Visual Basic создает и поддерживает постоянное растровое изображение в контексте памяти для всех форм и изображений в рамке, у которых свойство AutoRedraw установлено в True. Таким образом, когда форма или изображение в рамке нуждается в перерисовке (например, если были изменены размеры), VB может просто отобразить постоянное растровое изображение на соответствующий управляющий элемент. Другое важное применение контекстов памяти – это выполнение сложных операций рисования, которые в памяти производятся быстрее, чем непосредс­ твенно на устройстве отображения. После завершения рисования готовое растро­ вое изображение может быть быстро перенесено на соответствующее устройство с помощью функции BitBlt. В качестве примера рассмотрим следующую программу, которая создает кон­ текст памяти, совместимый с изображением в рамке (pic). Затем каждый пик­ сел растрового изображения в памяти раскрашивается случайно выбранными цветами. Наконец, растровое изображение переносится на изображение в рамке операцией BitBlt. При выполнении данной программы с закомментированной и раскомментированной строкой dcComp = pic.hdc можно заметить отличие между рисованием непосредственно на изображении в рамке и с использованием контекста памяти. Разница в производительности здесь практически незаметна. Public Sub MemoryDCExample() ' Предполагается наличие рамки с изображением, названной Pic. ' Сохраняем DC для последующего восстановления. hDCPic = SaveDC(pic.hdc)
33 ' Создаем DC памяти (совместимый DC). dcComp = CreateCompatibleDC(pic.hdc) ''dcComp = pic.hdc WidthInPixels = pic.Width / Screen.TwipsPerPixelX HeightInPixels = pic.Height / Screen.TwipsPerPixelY ' Создаем растровое изображение. hBitmap = CreateCompatibleBitmap(pic.hdc, WidthInPixels, _ HeightInPixels) lDummy = SelectObject(dcComp, hBitmap) ' Выбираем случайный цвет пиксела между 0 и 2^24  1 Randomize Timer For r = 0 To WidthInPixels  1 For c = 0 To HeightInPixels  1 l=Rnd*(2^24 1) SetPixel dcComp, r, c, l Next Next BitBlt pic.hdc, 0, 0, WidthInPixels, HeightInPixels, dcComp, 0, 0, SRCCOPY RestoreDC pic.hdc, hDCPic DeleteDC dcComp DeleteObject hBitmap End Sub Контексты устройств принтера Win32 GDI предоставляет специальный тип контекста устройства, называе­ мый контекстом устройства принтера (printer device context), который исполь­ зуется при печати на принтере или при черчении на плоттере. Получить доступ к контексту принтера можно с помощью вызова функции CreateDC. После завершения печати контекст принтера должен быть удален с помощью функции DeleteDC (но не ReleaseDC). Функция CreateDC объявляется следующим образом: HDC CreateDC( LPCTSTR lpszDriver, // Указатель строки, определяющей имя драйвера. LPCTSTR lpszDevice, // Указатель строки, определяющей имя // устройства. LPCTSTR lpszOutput, // Этот параметр должен быть установлен в NULL. CONST DEVMODE *lpInitData // Указатель на необязательные данные // принтера. ); Контексты устройства памяти
34 Типы контекстов устройств В VB можно записать такую декларацию: Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" ( _ ByVal lpDriverName As String, _ ByVal lpDeviceName As String, _ ByVal lpOutput As String, _ lpInitData As DEVMODE _ ' Может быть null. ) As Long В Windows 9x параметр lpszDriver обычно игнорируется и должен быть установлен в NULL. Но есть одно исключение. Если этому параметру присвое­ но значение строки «DISPLAY», то все остальные параметры игнорируются, а функция CreateDC возвращает контекст дисплея. В Windows NT этот параметр должен быть строкой «DISPLAY» (для получения контекста дисплея) или именем спулера печати «WINSPOOL». Параметр lpszDevice является строкой, которая определяет имя устройс­ тва вывода в том виде, в каком это имя представлено в диспетчере печати (print manager) Windows. Параметр lpszOutput не используется и должен быть установлен в NULL. Параметр lpInitData указывает на структуру DEVMODE, которая содержит данные инициализации драйвера, зависящие от конкретного устройства. Если установить этот параметр в нуль, то для инициализации будут использованы зна­ чения, установленные по умолчанию. Функция DocumentProperties извлекает эту структуру, заполненную значениями для конкретного устройства. Структура DEVMODE довольно сложна. В VB она имеет следующую декларацию: Type DEVMODE dmDeviceName As String * CCHDEVICENAME ' CCHDEVICENAME = 32 dmSpecVersion As Integer dmDriverVersion As Integer dmSize As Integer dmDriverExtra As Integer dmFields As Long dmOrientation As Integer dmPaperSize As Integer dmPaperLength As Integer dmPaperWidth As Integer dmScale As Integer dmCopies As Integer dmDefaultSource As Integer dmPrintQuality As Integer dmColor As Integer dmDuplex As Integer dmYResolution As Integer dmTTOption As Integer dmCollate As Integer dmFormName As String * CCHFORMNAME ' CCHFORMNAME = 32 dmUnusedPadding As Integer dmBitsPerPel As Long
35 dmPelsWidth As Long dmPelsHeight As Long dmDisplayFlags As Long dmDisplayFrequency As Long dmICMMethod As Long dmICMIntent As Long dmMediaType As Long dmDitherType As Long dmReserved1 As Long dmReserved2 As Long End Type Здесь, конечно, не будут рассматриваться все ее поля. Однако стоит отметить, что член структуры dmDeviceName – это имя устройства в том виде, в каком оно представлено в диспетчере печати. Перечисление принтеров В Windows реализована функция перечисления EnumPrinters, которая пе­ речисляет доступные системе принтеры: BOOL EnumPrinters( DWORD Flags, // Типы объектов "принтер" для перечисления. LPTSTR Name, // Имя объекта "принтер". DWORD Level, // Задает тип структуры с информацией о принтере. LPBYTE pPrinterEnum, // Указатель на буферприемник инфоструктуры // принтера. DWORD cbBuf, // Размер буфера в байтах. LPDWORD pcbNeeded, // Указатель на переменную, содержащую // количество скопированных (или заданных) // байтов. LPDWORD pcReturned // Указатель на переменную, содержащую // количество скопированных инфоструктур // принтера. ); В VВ данная функция декларируется следующим образом: Declare Function EnumPrinters Lib "winspool.drv" Alias "EnumPrintersA" _ (_ ByVal flags As Long, _ ByVal Name As String, _ ByVal Level As Long, _ pPrinterEnum As Long, _ ByVal cdBuf As Long, _ pcbNeeded As Long, _ pcReturned As Long _ ) As Long Параметры выглядят довольно запутано, но их подробное описание содержит­ ся в документации. Обратимся лучше к примеру вместо детального рассмотрения данной функции. Контексты устройств принтера
3 Типы контекстов устройств Функция EnumPrinters возвращает массив структур с информацией о при­ нтерах – по одной структуре на каждый принтер вместе с пояснительными строка­ ми. Существует пять разных структур с информацией о принтере. Ниже показана та, которая нас интересует: Public Type PRINTER_INFO_2 pServerName As Long pPrinterName As Long pShareName As Long pPortName As Long pDriverName As Long pComment As Long pLocation As Long pDevMode As Long ' Указатель на DEVMODE. pSepFile As Long pPrintProcessor As Long pDatatype As Long pParameters As Long pSecurityDescriptor As Long Attributes As Long Priority As Long DefaultPriority As Long StartTime As Long UntilTime As Long Status As Long cJobs As Long AveragePPM As Long End Type Заметьте, что функция EnumPrinters не является рекурсивной – она ра­ ботает скорее как EnumProcess, чем как EnumWindows (обе эти функции уже обсуждались в этой книге). Таким образом, единственным способом узнать, что вы выделили буфер достаточного размера, является выполнение программы. Возвра­ щаемое значение pcbNeeded указывает на значение типа long, которое содержит необходимое количество байтов. Если вы назначили буфер на меньшее число байтов, то придется повторить все с самого начала. Давайте рассмотрим программу для перечисления некоторых значений при­ нтера: Sub ListPrinters() Dim lNeeded As Long Dim lReturned As Long Dim lData() As Long Dim pi2 As PRINTER_INFO_2 Dim dm As DEVMODE Dim i As Integer ReDim lData(0 To 4000)
3 EnumPrinters PRINTER_ENUM_LOCAL, vbNullString, 2, _ lData(0), 4000, lNeeded, lReturned Debug.Print "Needed: " & lNeeded Debug.Print "Returned: " & lReturned If lNeeded > 4000 Then MsgBox "Увеличьте размер буфера." Fori=0TolReturned1 Debug.Print "** Printer " & i ' Копируем iую структуру PRINTER_INFO_2 . CopyMemory ByVal VarPtr(pi2), _ ByVal VarPtr(lData(i * LenB(pi2) / 4)), LenB(pi2) Debug.Print "Name: " & LPSTRtoBSTR(pi2.pPrinterName) Debug.Print "Port: " & LPSTRtoBSTR(pi2.pPortName) Debug.Print "Driver: " & LPSTRtoBSTR(pi2.pDriverName) Debug.Print "Comment: " & LPSTRtoBSTR(pi2.pDriverName) CopyMemory ByVal VarPtr(dm), ByVal pi2.pDevMode, LenB(dm) Debug.Print "Driver Ver: " & dm.dmDriverVersion Next End Sub Вывод на моем компьютере был таким: Needed: 2008 Returned: 2 ** Printer 0 Name: HP LaserJet 4000 Series PS Port: LPT1: Driver: HP LaserJet 4000 Series PS Comment: HP LaserJet 4000 Series PS Driver Ver: 3 ** Printer 1 Name: EPSON Stylus COLOR 800 Port: LPT2: Driver: EPSON Stylus COLOR 800 Comment: EPSON Stylus COLOR 800 Driver Ver: 0 Определение принтера, установленного по умолчанию В Windows 95, чтобы получить информацию о принтере, установленном по умолчанию, нужно присвоить первому параметру функции EnumPrinters зна­ чение PRINTER_ENUM_DEFAULT. Однако в Windows NT это не работает. Кроме того, создается впечатление, что функция EnumPrinters никак не упорядочивает перечисляемые принтеры. Удобно, когда принтер, установленный по умолчанию, Контексты устройств принтера
3 Типы контекстов устройств всегда первый в списке. Но в Windows NT нельзя найти принтер, установленный по умолчанию, с помощью функции EnumPrinters. Тем не менее получить подобную информацию можно с помощью функции GetProfileString: Public Function GetDefaultPrinter() Dim sDefPrinter As String sDefPrinter = String(1024, vbNullChar) GetProfileString "windows", "device", "xxx", sDefPrinter, 1024 Debug.Print sDefPrinter End Function На моем компьютере вывод был таким: HP LaserJet 4000 Series PS,winspool,LPT1: Перечисление драйверов принтеров В Windows также реализована функция перечисления EnumPrinterDrivers, которая перечисляет доступные системе драйверы принтера: BOOL EnumPrinterDrivers( LPTSTR pName, // Указатель на имя сервера. LPTSTR pEnvironment, // Указатель на имя окружения. DWORD Level, // Структура уровня. LPBYTE pDriverInfo, // Указатель на массив структур. DWORD cbBuf, // Размер массива в байтах. LPDWORD pcbNeeded, // Указатель на количество // скопированных (или заданных) байтов. LPDWORD pcReturned // Указатель на количество структур // DRIVER_INFO. ); В VB это можно представить следующим образом: Declare Function EnumPrinterDrivers Lib "winspool.drv" _ Alias "EnumPrinterDriversA" ( _ ByVal pName As String, _ ByVal pEnvironment As String, _ ByVal Level As Long, pDriverInfo As Long, _ ByVal cdBuf As Long, _ pcbNeeded As Long, _ pcReturned As Long _ ) As Long Это сложная функция, поэтому из трех разных структур DRIVER_INFO здесь рассматривается только одна: Type DRIVER_INFO_2 cVersion As Long pName As Long pEnvironment As Long
3 pDriverPath As Long pDataFile As Long pConfigFile As Long End Type Далее представлена программа для перечисления драйверов принтера: Sub ListDrivers() Dim lNeeded As Long Dim lReturned As Long Dim lData() As Long Dim di2 As DRIVER_INFO_2 Dim i As Integer ReDim lData(0 To 4000) EnumPrinterDrivers vbNullString, vbNullString, 2, lData(0), _ 4000, lNeeded, lReturned Debug.Print "Needed: " & lNeeded Debug.Print "Returned: " & lReturned If lNeeded > 4000 Then MsgBox "Увеличьте размер буфера." Fori=0TolReturned1 Debug.Print "** Driver " & i CopyMemory ByVal VarPtr(di2), _ ByVal VarPtr(lData(i * LenB(di2) / 4)), LenB(di2) Debug.Print "Name: " & LPSTRtoBSTR(di2.pName) Debug.Print "Version: " & LPSTRtoBSTR(di2.cVersion) Debug.Print "Path: " & LPSTRtoBSTR(di2.pDriverPath) Debug.Print "DataFile: " & LPSTRtoBSTR(di2.pDataFile) Debug.Print "ConfigFile: " & LPSTRtoBSTR(di2.pConfigFile) Next End Sub На моем компьютере получился следующий результат: Needed: 854 Returned: 2 ** Driver 0 Name: HP LaserJet 4000 Series PS Version: Path: C:\WINNT\System32\spool\DRIVERS\W32X86\2\PSRIPT.DLL DataFile: C:\WINNT\System32\spool\DRIVERS\W32X86\2\HP4000_6 .PPD ConfigFile: C:\WINNT\System32\spool\DRIVERS\W32X86\2\PSRIPT.DLL ** Driver 1 Name: EPSON Stylus COLOR 800 Контексты устройств принтера
400 Типы контекстов устройств Version: Path: C:\WINNT\System32\spool\DRIVERS\W32X86\2\E_CPDJ33.DLL DataFile: C:\WINNT\System32\spool\DRIVERS\W32X86\2\E_C93J33.DLL ConfigFile: C:\WINNT\System32\spool\DRIVERS\W32X86\2\E_CUDJ33.DLL Печать Печать с использованием Windows GDI включает следующие шаги: 1. Создать контекст устройства принтера с помощью CreateDC. Как вы уже знаете, этой функции требуется имя принтера. 2. Вызвать функцию StartDoc, чтобы начать документ: Declare Function StartDoc Lib "gdi32" Alias "StartDocA" ( _ ByVal hdc As Long, _ lpdi As DOCINFO _ ) As Long 3. Вызвать функцию StartPage, чтобы начать новую страницу: Declare Function StartPage Lib "gdi32" ( _ ByVal hdc As Long _ ) As Long 4. Вызвать нужные функции рисования GDI, чтобы вывести данные в буфер памяти. 5. Вызвать функцию EndPage, с помощью которой Windows пошлет эту стра­ ницу на принтер: Declare Function EndPage Lib "gdi32" ( _ ByVal hdc As Long _ ) As Long 6. Повторить предыдущие три шага для каждой страницы. 7. Вызвать функцию GDI EndDoc (но не одноименную функцию VB EndDoc) для завершения документа: Declare Function EndDocAPI Lib "gdi32" Alias "EndDoc" ( _ ByVal hdc As Long _ ) As Long Например, следующая программа распечатает эллипс на принтере LaserJet 4000: Sub PrintIt() Dim printDC As Long Dim di As DOCINFO ' Инициализируем DOCINFO. di.cbSize = LenB(di) di.lpszDocName = "Документ" di.lpszOutput = vbNullString ' Или имя файла для печати. di.lpszDataType = vbNullString
401 ' Создаем DC принтера. printDC = CreateDC(vbNullString, "HP LaserJet 4000 Series PS", _ vbNullString, 0) ' Начинаем документ и страницу. StartDoc printDC, di StartPage printDC ' Печатаем эллипс. Ellipse printDC, 0, 0, 600, 600 ' Завершаем страницу и документ. EndPage printDC EndDocAPI printDC ' Удаляем DC принтера. DeleteDC printDC End Sub Для печати с использованием Win32 GDI, конечно, существует намного боль­ ше возможностей, чем перечислено здесь. Существует около 80 различных функ­ ций GDI, относящихся к печати. Контексты устройств дисплея Контексты устройства дисплея (display device context) используются для рисования в окнах и на экране. Существует несколько типов контекстов дисплея. Они приведены в табл. 23.1 . Таблица 23.1. Типы контекстов дисплея Тип Область Кэш Функция Класс Комментарий рисования стиля Common Клиентская Да GetDC или Нет Не требует (Общий) область BeginPaint (по умолчанию) дополнительной памяти Class Клиентская Нет GetDC CS_CLASSDC Только один DC для всех (Классовый) область окон этого класса Parent Все окно Да GetDC CS_PARENTDC Предназначен только для (Родительский) и родитель или оконпотомков ское окно BeginPaint Private Клиентская Нет GetDC CS_OWNDC Создается новый восмисот (Закрытый) область байтовый DC для каждого окна Window Все окно Да GetWindowDC NA NA – Not Available; (Оконный) или GetDCEx не доступен Обратите внимание, что VB через свойство hDC предоставляет частный кон­ текст устройства, поэтому обычно именно его используют программисты VB. Однако все же следует кратко рассмотреть все остальные типы контекста, приве­ денные в табл. 23.1 . Контексты устройств дисплея
402 Типы контекстов устройств Кэшируемые и некэшируемые контексты дисплея Как видно из табл. 23.1, некоторые контексты дисплея являются кэшируемыми (cached). Windows поддерживает кэш контекстов устройства для общего (common), родительского (parent) и оконного (window) контекстов. Операционная система создает новый кэшируемый контекст, когда это требуется, но кэшируемые контек­ сты расходуют память из кучи приложения, выделяемой по умолчанию, поэтому следует позаботиться о том, чтобы не использовать слишком много кэшируемых контекстов одновременно. Фактически кэшируемые контексты предназначены для кратковременного использования и немедленного освобождения по завершении с помощью вызова функции ReleaseDC (или EndPaint). Кроме того, кэшируемые контексты необходимы в тех случаях, когда нужно внести небольшие изменения в атрибуты, установленные по умолчанию. Каждый раз, когда кэшируемый контекст возвращается в кэш, его установки изменяются на установки по умолчанию. (Между прочим, у 16­разрядной Windows был предел – пять кэшируемых контекстов.) Некэшируемые контексты предназначены для создания и неограниченного использования приложением. Они имеют лучшие характеристики, чем ранее рас­ сматриваемые, так как после создания постоянно доступны. Каждый некэшируе­ мый контекст расходует 800 байт памяти. Классы и контексты дисплея Для того чтобы рисовать в окне, нужно сначала получить контекст устройства с помощью таких функций, как GetDC, GetDCEx или GetWindowDC. Тип контекста дисплея, предоставляемый Windows в результате вызова одной из этих функций, зависит от стиля оконного класса, на базе которого было создано данное окно. Напомним, что для регистрации оконного класса функции RegisterClass требуется структура WNDCLASS. Член этой структуры style используется (час­ тично) для задания типа контекста устройства, принимаемого по умолчанию. Общие контексты дисплея Общий контекст дисплея предоставляется Windows (в ответ на запрос контек­ ста) в качестве контекста по умолчанию, если в стиле класса не определен конк­ ретный контекст. Общие контексты устройства особенно эффективны, поскольку им не требуется дополнительные память или системные ресурсы. При помощи общего контекста устройства можно рисовать только в клиент­ ской области окна. Поэтому начало координат координатной системы изначально устанавливается в верхний левый угол клиентской области. Кроме того, в качестве области отсечения задается клиентская область. Это означает, что любой рисунок, который выходит за пределы клиентской области, отсекается (не отображается). Если приложение запрашивает общий контекст устройства, используя функцию BeginPaint, устанавливается также область, требующая перерисовки. Частные контексты дисплея Стиль класса CS_OWNDC определяет, что каждое окно этого класса получает час- тный контекст дисплея. Windows хранит каждый такой контекст в памяти кучи GDI. Освобождать его при помощи ReleaseDC не нужно. Эти контексты вы должны
403 использовать только с режимом отображения MM_TEXT, чтобы убедиться, что окно стирается правильно. (О режимах отображения будет рассказано в главе 24.) Как упоминалось ранее, VB­свойство hDC возвращает дескриптор частного контекста устройства. Контексты дисплея класса Стиль класса CS_CLASSDC определяет, что все окна этого класса совместно используют один и тот же контекст дисплея, который называется контекстом дисплея класса (class display context). Контексты дисплея класса предоставляют некоторые преимущества частных контекстов дисплея, но лучше, чем они, ис­ пользуют ресурсы. С этими контекстами следует работать только в режиме отоб­ ражения MM_TEXT, чтобы убедиться, что окно стирается правильно (о режимах отображения будет рассказано в главе 24). Родительские контексты дисплея Стиль класса CS_PARENTDC определяет, что каждое окно этого класса исполь­ зует контекст дисплея своего родительского окна. Следовательно, как и контексты класса, многие окна совместно используют один контекст дисплея, сохраняя ре­ сурсы. Основное преимущество родительских контекстов заключается в скорости их работы. Координатные системы Функции рисования GDI требуют задания координат для операции рисования. Например, нельзя нарисовать линию, не определив, где она начинается и кончается. Требуется задать координатную систему. Тема координатных систем Windows не­ сколько сложна для восприятия, давайте попробуем в ней разобраться. Физические устройства Начнем с очевидного утверждения, что конечной целью GDI является изоб­ ражение графических объектов (включая текст) на физическом устройстве. Фи­ зические устройства включают:  область распечатки на листе бумаги в принтере;  область изображения на экране монитора;  окно в целом;  клиентская область окна. Физическое пространство и физические координатные системы Каждое физическое устройство имеет естественную систему координат, ко­ торая определяется способом отображения видеоинформации в его физическом пространстве. Можно выделить ее следующие характеристики (см. рис. 23.1):  начало координат (origin) представляет собой верхний левый угол конкрет­ ного устройства;  направление координатных осей (orientation). Увеличение значений по го­ ризонтальной оси происходит слева направо, а по вертикальной оси – сверху вниз; Координатные системы
404 Типы контекстов устройств  единицами измерения (units) физической системы координат являются пикселы (для монитора или окна) или точки, печатаемые принтером (printer dots). Можно ссылаться и на те, и на другие единицы измерения просто как на пикселы. Обратите внимание, что если устройством является клиентская область окна, то начало физической системы координат находится в верхнем левом углу клиентской области окна. Кажется логичным называть физическую систе­ му координат системой координат устройства. Однако в Windows этот термин используется для обозначения координатной системы, которая идентична физической системе координат за одним исключением – начало коор­ динат не обязательно находится в левом верхнем углу устройства. Дюймы монитора Лазерный принтер с 600 dpi печатает 600 точек на дюйм. Легко перевести точки принтера в более удобные дюймы или миллиметры. А для мониторов все не так просто. Трудность связана с определением разрешения мониторов. Разрешающая способ­ ность принтера определяется в точках на дюйм, что наиболее удобно, а разрешающая способность монитора – в терминах общего количества пикселов в вертикальном и горизонтальном направлениях. Проблема в том, что у Windows нет способа полу­ чить реальные физические размеры монитора, и поэтому она не может перевести раз­ решение по горизонтали и вертикали в количество пикселов на физический дюйм. Попытка решения этой проблемы состоит в том, что Windows определяет ло- гический дюйм (logical inch). Как вы увидите, это редко соответствует реальному физическому дюйму монитора. Фактически это Windows­версия физического дюйма, независимая от монитора. При работе с принтером Windows использует реальные физические дюймы. Термин «логический дюйм» звучит так, как если бы он имел отношение к по­ нятию логической системы координат (logical coordinate system), с которым вам еще предстоит познакомиться. Поэтому впредь будем называть логические дюймы дюймами монитора (monitor inche), так как они применяются только к мониторам. Следует еще раз подчеркнуть, что это нестандартная терминология. Для принтера физический дюйм (physical inche) соответствует некоторому количеству точек. Например, для лазерного принтера с 600 dpi 1 физический дюйм равен 600 точкам принтера. Можно было бы сказать, что дюйм составляет 600 точек лазерного принтера с 600 dpi. Конечно, это совершенно неприемлемо. Однако дюймы монитора опре­ деляются именно так – в пикселах, а 1 дюйм монитора равен 96 пикселам. Таким образом, программист может задать отрезок линии длиной два дюйма монитора, а Windows (или драйвер устройства) будет знать, сколько пикселов (2 × 96 = 192) использовать для изображения этого отрезка и сколько требуется точек (2 × 600 = 1200), чтобы распечатать его на лазерном принтере с 600 dpi. Устройство Начало координат Рис. 23.1. Естественная физическая система координат
405 Как же определяются дюймы монитора? Апплет Экран (Display) Панели уп- равления (Control Panel) Windows позволяет пользователю выбрать один из двух размеров шрифта – мелкий (small) или крупный (large). Windows использует эту установку для определения размера дюйма монитора. Если пользователь выбирает мелкий шрифт, Windows устанавливает дюйм монитора равным 96 пикселам. Если выбирается крупный шрифт, система устанавливает дюйм монитора равным 120 пикселам. Следовательно 1 дюйм монитора = 96 пикселам ' На Панели управления установлен ' мелкий размер шрифта. 1 дюйм монитора = 120 пикселам ' На Панели управления установлен ' крупный размер шрифта. Ранее уже говорилось, что дюймы монитора довольно сильно отличаются от физических дюймов. Например, эта книга набрана на компьютере с 21­дюймовым монитором с разрешением 1600×1200 и крупным размером шрифта. Соответс­ твенно, Windows устанавливает дюйм монитора равным 120 пикселам, и размеры области изображения на экране в дюймах монитора будут такими: ширина экрана = 1600 / 120 = 13,3 дюймов монитора высота экрана = 1200 / 120 = 10 дюймов монитора Однако физические размеры области изображения на экране фактически рав­ ны 14,9 по ширине и 11,2 по высоте. В этом случае дюйм монитора приблизительно равняется 1,12 физического дюйма. Предположим, что я увеличил разрешение до 1800×1440 (следующий вариант выбора для моего видеоадаптера). Размеры области изображения монитора стали бы такими: ширина экрана = 1800 / 120 = 15 дюймов монитора высота экрана = 1440 / 120 = 12 дюймов монитора Теперь дюйм монитора по горизонтали равен 14,9 / 15 = 0,99 физического дюйма, но по вертикали он составляет 11,2 / 12 = 0,93 физического дюйма. Разница возникает из­за того, что разрешение 1800×1440 имеет соотношение геометричес­ ких размеров 1800 / 1440 = 1,25, а не более распространенное соотношение 1,33, которое является характерным для разрешений 640×480, 800×600, 1024×768 и 1600×1200, а также для физических размеров экрана моего монитора. Функцию GetDeviceCaps (сокращение от англ. Get Device Capacities – опре­ делить характеристики устройства) можно использовать для получения количес­ тва пикселов на дюйм монитора вместе с другими значениями. Она объявляется следующим образом: int GetDeviceCaps( HDC hdc, // Дескриптор контекста устройства. int nIndex // Индекс запрашиваемой характеристики. ); Далее представлены некоторые из наиболее полезных значений nIndex, име­ ющих отношение к монитору:  HORZSIZE – ширина физического экрана в мм;  VERTSIZE – высота физического экрана в мм; Координатные системы
40 Типы контекстов устройств  HORZRES – ширина экрана в пикселах;  VERTRES – высота экрана в строках растра;  LOGPIXELSX – количество пикселов на логический дюйм по отношению к ширине экрана;  LOGPIXELSY – количество пикселов на логический дюйм по отношению к высоте экрана. Например, так выглядят эти значения для моего 21­дюймового монитора: HORZ SIZE: 320 VERT SIZE: 240 HORZ RES: 1600 VERT RES: 1200 LOGPIXELSX: 120 LOGPIXELSY: 120 Как ни странно, это не соответствует действительности. Формулы, относящи­ еся к этим значениям, должны быть такими: HORZ SIZE: 25.4 * HORZ RES/LOGPIXELSX VERT SIZE: 25.4 * VERT RES/LOGPIXELSY В Windows 9x данные значения являлись бы правильными, но Windows NT (в которой я работаю) всегда использует значения 320 и 240 по причинам, которые мне неизвестны.
Глава 24. Координатные системы контекстов устройств В Windows GDI вместе с функциями рисования используются три пространства, каждое со своей собственной системой координат, – мировое (внешнее) пространс- тво (world space), пространство страницы (page space), которое называют также логическим пространством (logical space), и пространство устройства (device space). Они показаны на рис. 24.1 . Функции рисования в зависимости от уста­ новок так называемого мирового преобразования (world transform)1 рисуют или в мировом пространстве, или в пространстве страницы. После того как процедура рисования завершается, Windows применяет одну или несколько отображающих (mapping) функций или преобразований, обозначенных на рис. 24.1 как T1, T2 и T3, чтобы все точки рисунка разместились в физическом пространстве самого устройства. Область экрана T1 T2 Внешнее пространство (внешние координаты) Пространство страницы (логические координаты) Пространство устройства (координаты устройства) Физическое пространство (физические координаты) T3 Рис. 24.1. Координатные системы Windows 1 Имеется в виду преобразование координат из мирового пространства в пространство страницы. Более детально описывается далее в разделе «Мировое пространство». – Прим. науч. ред. Координатные системы GDI Следует отметить, что направление осей в каждом пространстве такое же, как и в физическом, а мировое пространство поддерживается только в Windows NT. Отображающие функции, которые операционная система использует для отображения точек из одного пространства в другое, потенциально могут быть набором следующих пяти основных преобразований (правда, поворот и наклон до­
40 Координатные системы контекстов устройств пускаются только для преобразования T1 из мирового пространства в пространс­ тво страницы):  сдвиг;  отражение относительно оси;  масштабирование (расширение или сжатие вдоль оси);  поворот относительно начала координат;  наклон. Эти преобразования показаны на рис. 24.2 . Оригинал Поворот относительно начала координат Отражение относительно оси x Масштаби рование поосиx Наклон в направлении оси x Сдвиг Рис. 24.2 . Пять базисных преобразований Эти преобразования представляют собой все возможные базисные (nice), или элементарные, преобразования плоскости. Для тех, кто изучал линейную алгебру, можно добавить, что любое несингулярное (обычное) линейное преобразование плоскости может быть представлено в виде указанных преобразований (за исклю­ чением сдвига). Если вы заинтересовались этой темой, то разрешите порекомендо­ вать вам мою книгу Introduction to Linear Algebra with Applications, опубликованную издательством Saunders College Publishing. Преимущество использования таких преобразований заключается в том, что функции рисования можно существенно упростить, перекладывая значи­ тельную часть их нагрузки на отображающие функции. Например, для изоб­ ражения эллипса, который не является центрированным относительно начала координат, можно в пространстве страницы нарисовать центрированный круг, потом с помощью преобразований в вертикальном направлении изменить фор­ му круга на форму эллипса и затем переместить его в заданное положение. В данной главе рассматривается несколько примеров, иллюстрирующих такой подход. Важно подчеркнуть, что функции рисования используют мировые, логические, но не физические координаты. Нельзя также рисовать в пространстве устройства. Дело в том, что этим функциям ничего не известно ни о расположении начала координат физического устройства, ни о его единицах измерения (пикселах). Так и должно быть. Например, в результате выполнения следующего кода: Ellipse hDC, 1, 2, 5, 10 рисуется закрашенный эллипс, ограничивающий прямоугольник. Верхний левый угол прямоугольника располагается в точке (1,2), а нижний правый – в точке
40 (5,10) в логических (или мировых) координатах. Это не имеет никакого отноше­ ния ни к положению начала физических координат, ни к направлению осей, ни к дюймам, миллиметрам, пикселам или к каким­либо другим измерениям. Все это выполняется при помощи преобразований, приведенных на рис. 24.1 . Уже говорилось о том, что в Windows разрешены не все пять типов преобразо­ ваний между любыми парами пространств с рис. 24.1 . Эта операционная система поддерживает следующие типы:  преобразование из пространства устройства в физическое (T3) может быть только сдвигом;  преобразование из пространства страницы в пространство устройства (T2) может быть сдвигом с последующими масштабированием (по одной или по обеим осям) и отражением (относительно одной или обеих осей);  преобразование из мирового пространства в пространство страницы (T1) мо­ жет быть любым сочетанием всех пяти базисных преобразований – сдвигов, поворотов, масштабирования, наклонов и отражений. Виртуальное пространство Нет никаких оснований использовать представление о трех отдельных не­ физических пространствах, так как оно может приводить к ненужным слож­ ностям. Альтернативой может служить представление только об одном нефизи­ ческом пространстве, которое мы будем называть виртуальным пространством (virtual space) с виртуальной системой координат (virtual coordinate system). Как показано на рис. 24.3 , все процессы рисования выполняются в виртуальном про­ странстве с виртуальными координатами. Все довольно просто: для получения результирующего изображения Windows применяет единственную отобража­ ющую функцию T, которая является объединением всех трех преобразований с рис. 24.1 . Но Microsoft сочла нужным определить три пространства (см. рис. 24.1), по­ этому они будут рассматриваться наряду с нашим виртуальным пространством. А вы можете сами решить, каким вы предпочитаете видеть процесс преобразо­ ваний. Область экрана T Физическое пространство (физические координаты) Виртуальное пространство (виртуальные координаты) Рис. 24.3. Виртуальное пространство Виртуальное пространство
410 Координатные системы контекстов устройств Пространство устройства Пространство устройства во многих отношениях является особым пространс­ твом. Во­первых, в пространстве устройства нельзя рисовать, так как в Windows GDI нет таких функций. Во­вторых, Windows разрешает только сдвигать точки в пространстве устройства для получения соответствующих точек в физическом пространстве, другие базисные преобразования запрещены. Для задания сдвига между пространством устройства и физическим пространством можно использо­ вать функцию SetViewportOrgEx: BOOL SetViewportOrgEx( HDC hdc, // Дескриптор контекста устройства. int ViewportOriginX, // Xкоордината точки отсчета области // вывода в пикселах. int ViewportOriginY, // Yкоордината точки отсчета области // вывода в пикселах. LPPOINT lpPoint // Адрес структуры, получающей исходную // точку отсчета. ); Заметьте, что lpPoint можно установить в нуль, в этом случае его значение игнорируется. Точка (ViewportOriginX, ViewportOriginY) задает начало координат области вывода (viewport origin) в единицах измерения устройства (пикселах). Это та точка физического пространства, в которую сдвига­ ется начало координат пространства устройства, как показано на рис. 24.4 . Таким образом, формула для преобразования T3 на рис 24.1 будет такой: PhysicalX = DeviceX + ViewportOriginX PhysicalY = DeviceY + ViewportOriginY В Windows определены функции LPToDP и DPToLP для наблюдения за эффек­ том сдвига между двумя пространствами. Этот сдвиг трудно выделить так, чтобы наблюдать его действие непосредствен­ но, поскольку невозможно рисовать в пространстве устройства. Тем не менее вы увидите данный эффект в примере, который будет приведен позже. T3 Точка отсчета области вывода Физическое пространство (физические координаты) Пространство устройства (координаты устройства) Рис. 24.4 . Сдвиг из пространства устройства в физическое пространство
411 Пространство страницы Как уже говорилось, преобразование T2 на рис. 24.1 , переводящее из про­ странства страницы в пространство устройства, является композицией сдвига с последующим масштабированием (по одной или по обеим осям) и отражения (относительно одной или обеих осей). Конечно, любое из этих преобразований может быть тождественным. Сдвиг Сдвиг определяется с помощью функции SetWindowOrgEx (не очень удачное название): BOOL SetWindowOrgEx( HDC hdc, // Дескриптор контекста устройства. int WindowOriginX, // Xкоордината отсчета окна // в логических единицах. int WindowOriginY, // Yкоордината отсчета окна // в логических единицах. LPPOINT lpPoint // Адрес структуры, получающей исходную // точку отсчета. ); Эта функция определяет начало координат окна (window origin), то есть точку, которая отображается в начало координат в процессе сдвига. Обратите внимание на имеющееся различие: начало координат окна отображается на начало логических координат, в то время как начало координат области вывода само является отоб­ ражением от начала координат устройства. Так выглядит формула этого преобразования: NewX = LogicalX + WindowOriginX NewY = LogicalY + WindowOriginY На рис. 24.5 показан этот начальный сдвиг. Протяженность окна по горизонтали Масштабирование поосиx Протяженность области вывода по горизонтали Начало координат окна Масштабирование по оси y Протяженность окна по вертикали Пространство страницы (логические координаты) Пространство устройства (координаты устройства) Протяженность области вывода по вертикали Рис. 24.5 . Из пространства страницы в пространство устройства Пространство страницы
412 Координатные системы контекстов устройств Масштабирование Следующим шагом преобразования из пространства страницы в пространство устройства является масштабирование по одной или обеим логическим осям. Для масштабирования по направлению x­координаты нужно умножить x­координату точки на положительное число. Если оно меньше единицы, то масштабирование является сжатием (contraction), если больше – растяжением (expansion). Часто коэффициент масштабирования является дробным числом. Однако Windows более эффективно работает с целыми числами (в VB ими являются данные типа Long). В Windows коэффициенты масштабирования определяются установкой двух значений, называемых протяженностями (extent) в каждом на­ правлении1. Эти значения показаны на рис. 24.5 . Коэффициент масштабирования принимается равным отношению протяженностей для данного направления. Таким образом, для горизонтального направления применяется следующая формула:. Значение ViewportExtentEx задается в пикселах, а значение WindowExtentEx в логических единицах. Отсюда следует, что коэффициент масштабирования ScaleX измеряется в пикселах на логическую единицу. Другими словами, он оп­ ределяет количество пикселов на единицу логического изображения, что не явля­ ется собственно масштабированием, поскольку пространство страницы физически не существует. Это просто указание единиц измерения. Например, для установки следующего коэффициента масштабирования: 1 logical unit = 1 / 64 дюйма монитора можно было бы задать такие значения (предполагается, что системный шрифт крупный, и, следовательно, имеется 120 пикселов на дюйм монитора): WindowExtentX = 64 ViewportExtentX = 120 ' 1 дюйм монитора Таким образом, в функциях рисования GDI можно указывать одну логическую единицу, которая равна 1 / 64 дюйма монитора. Поэтому масштабирование можно трактовать и по­другому, а именно как способ задания точности представления или разрешения изображения, то есть наименьший доступный физический размер. Аналогично для выполнения вертикального масштабирования Wndows умно­ жает y­координату точки на отношение 1 То есть длинами единичных отрезков каждой из осей. – Прим. науч. ред.
413 Таким образом, одна логическая единица по вертикальной оси соответствует ScaleY пикселам физического устройства. Теперь можно объединить результаты действия сдвига и масштабирования, записав следующее: Для задания протяженностей можно использовать функции GDI: BOOL SetWindowExtEx( HDC hdc, // Дескриптор контекста устройства. int nXExtent, // Новая протяженность окна по // горизонтали в логических единицах. int nYExtent, // Новая протяженность окна по // вертикали в логических единицах. LPSIZE lpSize // Исходная протяженность окна. ); BOOL SetViewportExtEx( HDC hdc, // Дескриптор контекста устройства. int nXExtent, // Протяженность области вывода по // горизонтали в пикселах. int nYExtent, // Протяженность области вывода по // вертикали в пикселах. LPSIZE lpSize // Исходная протяженность области вывода. ); Заметьте, что если параметр lpSize установлен в нуль, то он игнорируется. Отражение Для выполнения операции отражения относительно оси X нужно умножить y­координату точки (да, y­координату) на –1 . Так же для отражения относительно оси Y надо умножить на –1 x­координату. Вместо того чтобы задавать операцию отражения отдельно в том или ином специальном параметре, можно просто изменить знак одной из протяженностей. Не имеет значения, знак окна или области вывода вы измените –эффект будет тем же самым. Таким образом, формулы полностью описывают преобразование из пространства страницы в пространство устройства. Из виртуального пространства в физическое
414 Координатные системы контекстов устройств Из виртуального пространства в физическое Преобразование из мировых в логические координаты включает в себя поворот и наклон, которые не допускаются в преобразовании из пространства страницы в пространство устройства. Однако эти два преобразования используются гораздо реже, чем остальные три базисных преобразования. Так как мировое пространс­ тво поддерживается только в Windows NT, в этой главе оно обсуждается очень кратко. Если отталкиваться от позиции виртуального пространства и не привлекать мировые координаты, то преобразование из виртуального пространства в физи­ ческое определяется следующими формулами: iginX ViewportOr ntX WindowExte tentX ViewportEx inX WindowOrig LogicalX PhysicalX + × − = ) ( , iginY ViewportOr ntY WindowExte tentY ViewportEx inY WindowOrig LogicalY PhysicalY + × − = ) ( . Windows поддерживает несколько режимов отображения, о которых будет говориться далее. Следует заметить, что только один из них – анизотропный ре­ жим – допускает независимую установку всех значений этих формул. В остальных режимах Windows, упрощая работу с формулами, сама устанавливает некоторые из значений. Пример Предположим, требуется изобразить эллипсы, показанные на рис. 24.6 справа. Их размеры заданы в дюймах монитора. (1,1) 1 (0.5,1) (1,1) (0.5,2) (1,1) 1 0.25 0.5 0.5 Пространство страницы (логические координаты) Физическое пространство (устройство) Рис. 24.6 . Рисование эллипсов Здесь есть много вариантов дальнейших действий. Один из подходов заклю­ чается в том, чтобы нарисовать круги, показанные на рис. 24.6 слева, а затем пре­ образовать эти круги в заданные эллипсы с помощью следующих операций:
415  масштабирование по направлению Y с коэффициентом Ѕ;  отражение относительно оси X;  сдвиг начала логических координат в точку (1,1) физического пространства. Во­первых, следует позаботиться о некоторых подготовительных действиях – сохранении контекста устройства рамки с изображением, установке типов пера и кисти, получении количества пикселов на дюйм монитора и установке анизот­ ропного режима отображения (о нем говорится позже в разделе «Режимы отобра­ жения»): ' Сохраняем DC для последующего восстановления. hDCPic = SaveDC(pic.hdc) ' Устанавливаем ширину пера. SelectObject pic.hdc, GetStockObject(BLACK_PEN) ' Устанавливаем кисть. SelectObject pic.hdc, GetStockObject(NULL_BRUSH) ' Получаем размер дюйма монитора. PixPerInchX = GetDeviceCaps(pic.hdc, LOGPIXELSX) PixPerInchY = GetDeviceCaps(pic.hdc, LOGPIXELSY) ' Устанавливаем режим отображения. SetMapMode pic.hdc, MM_ANISOTROPIC На мой взгляд, проще действовать так, как будто параметры (протяженность и начало координат) могут быть нецелочисленными значениями, а впоследствии осуществить необходи­ мую корректировку. Поэтому установим коэффициент масштаби­ рования по горизонтали равным одной логической единице на дюйм, а по вертикали – двум логическим единицам на дюйм. Эти коэффициенты приведут к сплющиванию логических кругов в направлении Y с коэффициентом 2, тем самым воспроизводя эллип­ тические формы. В то же время их нужно отразить относительно оси X, задавая в качестве вертикаль­ ной протяженности области вывода (viewport) от­ рицательное число: SetWindowExtEx pic.hdc, 1, 2, sz SetViewportExtEx pic.hdc, PixPerInchX, PixPerInchY, sz Сдвиг выполняем, устанавливая начало координат области вывода в точку (1,1) в дюймах монитора: SetViewportOrgEx pic.hdc, PixPerInchX, PixPerInchY, pt Теперь нарисуем круги, используя ограничивающие квадраты, показанные на рис. 24.6: Ellipse pic.hdc, 1, 1, 1, 1 Ellipse pic.hdc, 0.5, 1, 0.5, 2 Из виртуального пространства в физическое Рис. 24.7 . Результат
41 Координатные системы контекстов устройств Наконец, пришло время скорректировать тип параметров: несколько выше сде­ лано допущение об их нецелочисленности, хотя на самом деле параметры должны относиться к типу Long. Чтобы исправить это, придется умножить координаты ограничивающих квадратов на 2. А для компенсации потребуется сделать то же самое и с логическими единицами. Правильный исходный код показан ниже: ' Устанавливаем масштабные коэффициенты и отражение. SetWindowExtEx pic.hdc, 2, 4, sz ' Устанавливаем сдвиг. SetViewportOrgEx pic.hdc, PixPerInchX, PixPerInchY, pt ' Рисуем логические эллипсы (круги). Ellipse pic.hdc, 2, 2, 2, 2 Ellipse pic.hdc, 1, 2, 1, 4 Результат показан на рис. 24.7 . Установка логических координат в физическом пространстве Можно также представить преобразование из виртуального пространства в физическое, используя представление об изображении осей логических коор­ динат как о результате их отображения. Само изображение относится, конечно, к физическому пространству. Для удобства наблюдения за результатом масштаби­ рования логические оси рассматриваются как отрезки линий конечной длины. На самом деле оси – это линии, а не отрезки линий. На рис. 24.8 показано изображение логических координат, полученное в ре­ зультате преобразования из предыдущего примера. Особенность этой точки зрения заключается в том, что можно представлять преобразование как задание новой системы координат в физическом пространстве. (1,1) 1 (0.5,1) (1,1) (0.5,2) (1,1) 1 0.25 0.5 0.5 Отображение логических координат Пространство страницы (логические координаты) Физическое пространство (устройство) Рис. 24.8 . Изображение логических осей
41 Некоторые авторы предпочитают ссылаться на эту новую систему координат как на логическую, что, строго говоря, неверно. Данная система координат находится не в пространстве страницы, а в физическом пространстве. Однако мы тоже будем следовать этому условному наименованию. Пример Эта точка зрения проиллюстрирована в следу­ ющем примере. Допустим, требуется создать рису­ нок в рамке с изображением (см. рис. 24.9). Рисунок состоит из указанных заранее деталей:  4 луча длиной 1 дюйм монитора, исходя­ щих из начала физических координат рамки с изображением. Лучи делят квадрант на равные сектора;  5 концентрических эллипсов, расположенных в центре рамки с изображением. Длина глав­ ной (горизонтальной) оси наименьшего (вну­ треннего) эллипса равна ј дюйма монитора. Длина каждой следующей главной оси больше предыдущей на ј дюйма. Малые оси каждого эллипса равны половине их главной оси. Для создания этого рисунка использованы две логические системы координат: одна для линий, другая – для эллипсов, как показано на рис. 24.10. Рис. 24.9. Рисунок, сделанный с использованием GDI Устройство Логические координаты для рисования линий Логические координаты для рисования эллипсов Рис. 24.10. Установка логических координат в физическом пространстве Установка логических координат Как и прежде, исходный код начинается с подготовительных операций: ' Сохраняем DC для последующего восстановления. hDCPic = SaveDC(pic.hdc)
41 Координатные системы контекстов устройств ' Устанавливаем тип пера. SelectObject pic.hdc, GetStockObject(BLACK_PEN) ' Устанавливаем кисть. SelectObject pic.hdc, GetStockObject(NULL_BRUSH) ' Получаем размер дюйма монитора. PixPerInchX = GetDeviceCaps(pic.hdc, LOGPIXELSX) PixPerInchY = GetDeviceCaps(pic.hdc, LOGPIXELSY) ' Устанавливаем режим отображения. SetMapMode pic.hdc, MM_ANISOTROPIC Далее подготавливается преобразование для рисования лучей. Следует устано­ вить коэффициент масштабирования равным ста логическим единицам на дюйм монитора, что должно обеспечить достаточное разрешение для корректного отобра­ жения отрезков линий, длины которых округляются Windows до типа long. ' ————— ' Рисует лучи ' ————— ' Без сдвига. SetWindowOrgEx pic.hdc, 0, 0, pt SetViewportOrgEx pic.hdc, 0, 0, pt ' Коэффициент масштабирования: 100 логических единиц на дюйм монитора. SetWindowExtEx pic.hdc, 100, 100, sz SetViewportExtEx pic.hdc, PixPerInchX, PixPerInchY, sz ' Рисуем линии. Fori=1To4 ' Сдвигаем текущую позицию в начало логических координат. MoveToEx pic.hdc, 0, 0, pt ' Рисуем луч с радиусом 100 логических единиц. LineTo pic.hdc, 100 * Cos(1.57 * i / 5), 100 * Sin(1.57 * i / 5) Next Для рисования эллипсов нужно изменить логическую систему координат. Заметьте, что рисунок отражен относительно оси X, хотя это не является необхо­ димым, так как эллипсы симметричны относительно этой оси (объяснение этому вы найдете в разделе «Метрические режимы отображения»). ' Начальный сдвиг отсутствует. SetWindowOrgEx pic.hdc, 0, 0, pt ' Заключительное преобразование из логического (0,0) в центр рамки ' с изображением. SetViewportOrgEx pic.hdc, (pic.Width / 2) / Screen.TwipsPerPixelX, _ (pic.Height / 2) / Screen.TwipsPerPixelY, pt
41 ' Коэффициент масштабирования устанавливаем равным 8 логическим ' единицам на дюйм монитора по горизонтали. ' Устанавливаем коэффициент масштабирования по вертикали так, ' чтобы выровнять эллипсы 2 к 1. ' Отражение относительно оси X (реально в этом нет необходимости). SetWindowExtEx pic.hdc, 8, 16, sz SetViewportExtEx pic.hdc, PixPerInchX, PixPerInchY, sz ' Рисуем эллипсы. Fori=1To5 Ellipse pic.hdc, i, i, i, i Next i ' Восстанавливаем DC. RestoreDC pic.hdc, hDCPic Режимы отображения В предыдущих примерах для начала координат и протяженностей устанавли­ вались любые значения. Это было возможно, так как был задан соответствующий режим отображения (mapping mode) MM_ANISOTROPIC. SetMapMode pic.hdc, MM_ANISOTROPIC Однако во многих случаях такая свобода действий не нужна. Например, вам может потребоваться масштабировать две оси пропорционально, сохраняя соот­ ношение геометрических размеров (то есть отношение длин единичных отрезков двух осей). Это должно гарантировать, что круг или квадрат, нарисованные в пространстве страницы, будут отображены в круг или квадрат в физическом про­ странстве. Чтобы соответствовать набору типичных ситуаций, Windows поддерживает несколько режимов отображения. Они определяют, кем устанавливаются значе­ ния для положений начала координат и протяженностей – программистом или операционной системой. Как вы уже знаете, режим отображения задается с помощью функции SetMapMode: int SetMapMode( HDC hdc, // Дескриптор контекста устройства. int fnMapMode // Новый режим отображения. ); где fnMapMode может принимать значение одной из констант режима отоб­ ражения: MM_ANISOTROPIC, MM_ISOTROPIC, MM_TEXT , MM_HIENGLISH, MM_LOENGLISH, MM_HIMETRIC, MM_LOMETRIC или MM_TWIPS. Эти режимы опи­ саны в следующих разделах. Режим отображения текста В режиме отображения текста (text­mapping mode) протяженности устанав­ ливаются Windows в (1,1) и не могут быть изменены. То есть масштабирование Режимы отображения
420 Координатные системы контекстов устройств отсутствует, и одна логическая единица соответствует одной физической единице (пикселу). Программист может задавать начала координат, определяя левый верх­ ний угол области, в которую будет выведен текст. Положение точки начала коор­ динат, устанавливаемое по умолчанию, имеет значение (0,0). Если используется значение по умолчанию, то пространство страницы и физическое пространство совпадают. Метрические режимы отображения Режимы MM_HIENGLISH, MM_LOENGLISH, MM_HIMETRIC, MM_LOMETRIC и MM_TWIPS похожи друг на друга. Во все этих случаях начало координат может устанавливаться программистом. По умолчанию оно имеет значение (0,0). Про­ тяженности устанавливаются Windows таким образом, чтобы каждая логическая единица соответствовала некоторой физической величине. Это делает ненужным выяснение количества единиц устройства (пикселов или точек принтера), прихо­ дящихся на один дюйм. Далее приводятся масштабные коэффициенты. Префикс HI означает высокую точность (high precision), а префикс LO – низкую (low precision):  MM_HIENGLISH. Каждая логическая единица соответствует 0,001 дюйма;  MM_HIMETRIC. Каждая логическая единица соответствует 0,01мм;  MM_LOENGLISH. Каждая логическая единица соответствует 0,01 дюйма;  MM_LOMETRIC. Каждая логическая единица соответствует 0,1 мм;  MM_TWIPS. Каждая логическая единица соответствует 1/20 точки принтера (приблизительно 1/1440 дюйма). Наконец, каждый из данных режимов отображения включает также отражение относительно оси X. Поэтому для того чтобы рисовать в видимом физическом пространстве, следует рисовать, используя отрицательные значения Y. Результаты выполнения этого исходного кода показаны на рис. 24.11: ' Устанавливаем начало координат. SetWindowOrgEx pic.hdc, 0, 0, pt SetViewportOrgEx pic.hdc, 0, 0, pt ' Устанавливаем режим отображения. SetMapMode pic.hdc, MM_LOENGLISH ' Переходим к началу логических координат. MoveToEx pic.hdc, 0, 0, pt ' Рисуем линию. LineTo pic.hdc, 100, 100 ' Выводим текст. TextOut pic.hdc, 100, 100, "test", 4 ' Рисуем прямоугольник. Rectangle pic.hdc, 50, 50, 10, 10 Рис. 24.11. Режим MM_LOENGLISH
421 Обратите внимание: чтобы получить коэффициенты масштабирования для режимов отображения English и метрического, Windows сама устанавливает про­ тяженности окна и области вывода. Хотя здесь и не рассматриваются детали этой процедуры, интересно отметить, что данная операция в Windows 9x и Windows NT выполняется различно. Анизотропный режим отображения В этом режиме установка начала координат и протяженностей окна, а также области вывода оставлены на усмотрение программиста. Таким образом, анизот­ ропный режим предоставляет наибольшую свободу выбора. Изотропный режим отображения Изотропный режим похож на анизотропный. Но есть одно исключение: Windows регулирует протяженности так, чтобы логические единицы на каждой оси представляли одно и то же расстояние на физическом устройстве. Это можно сделать только в том случае, если пикселы являются квадратными и логическая единица отображается одним и тем же количеством пикселов в каждом направ­ лении. Windows будет сокращать требуемую протяженность области вывода для достижения этой цели. Конечно, назначение изотропного режима заключается в том, чтобы обес­ печить сохранение соотношения геометрических размеров, углов и пропорций, чтобы, например, логический квадрат изображался в виде квадрата на физическом устройстве, а логический круг – в виде круга. Вы можете сами наблюдать, как Windows изменяет протяженности области вывода, с помощью следующего простого исходного кода, который использует функцию GetViewportExtents: ' Устанавливаем режим отображения. SetMapMode pic.hdc, MM_ISOTROPIC SetWindowOrgEx pic.hdc, 0, 0, pt SetViewportOrgEx pic.hdc, 0, 0, pt ' Ширина и высота рамки с изображением 1000 логических единиц. SetWindowExtEx pic.hdc, 1000, 1000, sz SetViewportExtgEx pic.hdc, pic.Width / Screen.TwipsPerPixelX, _ pic.Height / Screen.TwipsPerPixelY, sz ' Рисуем прямоугольник. Rectangle pic.hdc, 100, 100, 900, 900 Debug.Print "Viewport Extents Setting: " & pic.Width / _ Screen. TwipsPerPixelX _ & " / " & pic.Height / Screen.TwipsPerPixelY Мировое пространство
422 Координатные системы контекстов устройств GetViewportExtEx pic.hdc, sz Debug.Print "Viewport Extents Setting: " & sz.cx & " / " & sz.cy При выполнении этого кода Windows настраивает вертикальную протя­ женность области вывода, поскольку высота рамки с изображением больше ее ширины: Viewport Extents Setting: 176 / 391 Viewport Extents: 176 / 176 Обратите внимание, что при использовании этого режима важно вызвать SetWindowExtEx до установки SetViewportExtEx. Мировое пространство Поскольку мировое пространство поддерживается только в Windows NT, здесь будет о нем рассказано очень кратко. Преобразование из мирового пространства в пространство страницы может быть сочетанием любых пяти базисных преобразований: сдвига, поворота, отра­ жения, масштабирования и наклона. Это наиболее полное из возможных преоб­ разований кроме того дублирует функциональные возможности других преобра­ зований. Преобразование из мирового пространства в пространство страницы задается с помощью функции SetWorldTransform: BOOL SetWorldTransform( HDC hdc, // Дескриптор контекста устройства. CONST XFORM *lpXform // Адрес данных преобразования. ); Здесь XFORM представляет собой структуру, которая определяет преобразование: struct _XFORM { FLOAT eM11; FLOAT eM12; FLOAT eM21; FLOAT eM22; FLOAT eDx; FLOAT eDy; } Несмотря на имеющееся в документации утверждение о линейности мирово­ го преобразования, оно не является таковым, за исключением того случая, когда сдвиг равен нулю. Точка (x,y) отображается в точку (x',y') с помощью сле­ дующих формул: x'=x*eM11+y*eM21+eDx y'=x*eM12+y*eM22+eDy На язык матриц это может быть записано следующим образом: .
423 Давайте представим эту формулу в виде: , где . Значения eDx и eDy – это величины сдвига в направлениях X и Y соответс­ твенно. Матрица M может быть получена перемножением матриц. Прежде всего необходимо определить тот порядок, в котором будут производиться поворот, отражение, масштабирование и наклон. Затем создать матрицу для каждой из этих операций и путем перемножения получить M. Ниже приводятся матрицы для конкретных операций. Поворот Для поворота в направлении от положительной полуоси X к полжительной полуоси Y на угол A используйте матрицу: . Отражение Для отражения относительно оси X ил оси Y потребуются матрицы FX и FY соответственно: . Масштабирование Для масштабирования в направлении X или в направлении Y на величину r>0 прменяются матрицы SX(r) и SY(r) соот­ ветственно: . Наклон Для наклона в направлении X или в направле­ нии Y на величину r ипользуйте матрицы HX(r) и HY(r) соответственно: Следующий исходный код дает результат, по­ казанный на рис. 24.12 . Public Sub RotatingText() Рис. 24.12. Поворот текста с использованием мирового пространства Мировое пространство
424 Координатные системы контекстов устройств ' Сохраняем DC для последующего восстановления. hDCPic = SaveDC(pic.hdc) ' Устанавливаем ширину пера. SelectObject pic.hdc, GetStockObject(BLACK_PEN) ' Устанавливаем кисть. SelectObject pic.hdc, GetStockObject(NULL_BRUSH) ' Получаем размер дюйма монитора. PixPerInchX = GetDeviceCaps(pic.hdc, LOGPIXELSX) PixPerInchY = GetDeviceCaps(pic.hdc, LOGPIXELSY) " Устанавливаем режим отображения. SetMapMode pic.hdc, MM_ANISOTROPIC SetGraphicsMode pic.hdc, GM_ADVANCED ' Устанавливаем масштаб перехода от логических единиц к физическим. SetWindowExtEx pic.hdc, 80, 80, sz SetViewportExtEx pic.hdc, PixPerInchX, PixPerInchY, sz ' Начинаем с центра устройства. SetViewportOrgEx pic.hdc, pic.Width / 2 / Screen.TwipsPerPixelX, _ pic.Height / 2 / Screen.TwipsPerPixelY, pt ' Рисуем эллипс. Ellipse pic.hdc, 18, 18, 18, 18 Const pi = 3.14159 Fori=0To15 xf.eDx = 0 xf.eDy = 0 xf.eM11 = Cos(i * pi / 8) xf.eM12 = Sin(i * pi / 8) xf.eM21 = xf.eM12 xf.eM22 = xf.eM11 SetWorldTransform pic.hdc, xf TextOut pic.hdc, 0, 0, " rotating text", 18 Next End Sub В заключение нужно сказать, что в Windows GDI есть функции, которые позволя­ ют легко перемножать матрицы – CombineTransform и ModifyWorldTransform.
Глава 25. Шрифты Шрифт (font) в Windows представляет собой набор символов с одинаковым оформ­ лением. Терминология, которая используется в Windows для описания шрифтов, не­ обязательно совпадает с типографской. Здесь использована терминология Windows. Термин гарнитура (typeface) относится к особенностям оформления символов шрифта. Например, Times New Roman и Arial – две разных гарнитуры. Одной из основных характеристик оформления гарнитуры шрифта является наличие или отсутствие засечек (serif), представляющих собой небольшие черточки, назначе­ ние которых помочь глазам плавно скользить от одного символа к другому, делая чтение менее утомительным. Рис. 25.1 показывает различие между шрифтами с засечками и без засечек. Засечки Это шрифт без засечек Рис. 25 .1. Шрифты с засечками и без засечек Термин стиль (style) относится к плотности (weight) и наклону (slant) шриф­ та. Плотность шрифта изменяется в диапазоне от тонкого (thin) до черного (black) в следующем порядке: тонкий (thin), ярко­светлый (extralight), светлый (light), обычный (normal), средний (medium), полужирный (semibold), жирный (bold), сверхжирный (extrabold), темный (heavy), черный (black). Наклон шрифта характеризуется как прямой (roman), наклонный (oblique) или курсив (italic). У шрифта с прямым стилем нет наклона; в шрифте с наклон­ ным стилем реализован наклон символов прямого шрифта; шрифт со стилем кур­ сив разрабатывается наклонным. Согласно документации, размер шрифта в Windows не имеет точного значе­ ния. В общем случае он может быть определен измерением расстояния от нижнего края буквы «g» нижнего регистра до верхнего края расположенной рядом бук­ вы «M» верхнего регистра. Размер шрифта измеряется в единицах, называемых пунктами (point). Пункт равен 0,013837 дюйма. В соответствии с типографской системой измерения в пунктах, изобретенной Пьером Симоном Фурнье, пункт приблизительно равен 1/72 дюйма.
42 Шрифты Семейства шрифтов В Windows шрифты объединяются в семейства, которые являются наборами шрифтов, имеющих одинаковые характеристики засечек и одинаковую ширину ли­ нии, то есть ширину толстых и тонких линий, составляющих символы. В Windows су­ ществует пять семейств, каждое из которых определяется символьной константой:  Decorative (FF_DECORATIVE). Декоративные (novelty) шрифты;  Modern (FF_MODERN). Моноширинные шрифты;  Roman (FF_ROMAN). Пропорциональные шрифты с засечками, такие как Times New Roman;  Script (FF_SCRIPT). Шрифты, оформляемые в виде рукописных;  Swiss (FF_SWISS). Пропорциональные шрифты без засечек, такие как Arial;  Dontcare (FF_DONTCARE). Родовое имя семейства, используемое в тех случа­ ях, когда информации о шрифте не существует или она не имеет значения. Технологии создания шрифтов В Windows существует четыре способа визуализации шрифтов на экране дис­ плея или при печати на принтере: растровый (raster, bitmap), векторный (vector, stroke), TrueType и OpenType. Различие между этими способами сводится к тому, как хранятся глифы (glyphs) в файле шрифта. Глиф – это данные или команды, которые определяют символ. Символы растрового шрифта хранятся в виде растровых изображений. Как следствие, масштабирование растрового шрифта дает очень плохие результаты. Символы векторного шрифта хранятся в виде отрезков линий. Однако векторные шрифты отображаются гораздо медленнее, чем шрифты TrueType и OpenType, и выглядят очень тонкими, так как ширина линий, составляющих символы, равна всего одному пикселу. Символы TrueType и OpenType хранятся как отрезки прямых и кривых ли­ ний вместе с указаниями, которые используются для регулировки визуализации символов, основанной на размере пункта. Поэтому данные шрифты могут масшта­ бироваться как в сторону увеличения, так и в сторону уменьшения, не меняя вне­ шнего вида. (Шрифты OpenType допускают и определения символов PostScript, и определения символов TrueType.) Глифы шрифта хранятся в файле шрифтового ресурса (font­resource file), или просто в файле шрифта (font file). Для растровых и векторных шрифтов данные делятся на две части: заголовок, описывающий атрибуты шрифта, и информация о глифах. Эти файлы имеют расширение .FON. Каждый шрифт TrueType и OpenType имеет два взаимосвязанных файла – небольшой заголовочный файл с расширени­ ем .FOT и файл с данными шрифта с расширением .TTF. Наборы символов В самом начале книги рассматривались наборы символов ASCII, ANSI и Unicode. Большинство шрифтов Windows используют наборы символов, которые прина­ длежат к одной из следующих групп:
42  Windows (ANSI);  Unicode;  OEM (сокращение от англ. Оriginal Еquipment Manufacturer – зависящий от изготовителя оборудования);  Symbol (символьный);  Vendor­specific (зависящий от поставщика); Набор символов OEM обычно используется для консольных приложений (в ок­ нах текстового режима). Набор символов Symbol содержит специальные символы во второй половине (символы 128–255) кодовой таблицы, такие как математичес­ кие символы и научная нотация. Логические и физические шрифты Для того чтобы API­функция могла использовать конкретный шрифт, он дол­ жен существовать на компьютере пользователя. Это является потенциальной проблемой, поскольку нет возможности предугадать, какие шрифты установлены на том или ином компьютере. Для решения этой проблемы Windows использует понятия логических и физических шрифтов. Физические шрифты могут быть разделены на два типа: шрифты GDI, ко­ торые хранятся в файлах на жестком диске компьютера, и шрифты устройства (device fonts), которые являются внутренними или встроенными шрифтами дан­ ного устройства. Приложение запрашивает шрифт, создавая объект «шрифт» с помощью функции CreateFont или CreateFontIndirect. Атрибуты шрифта, которые требуют эти функции, определяют логический шрифт. Когда логический шрифт выбирается в контексте устройства с использованием функции SelectObject, Windows заменяет его похожим по форме физическим шрифтом на компьютере пользова­ теля. Для выполнения этой процедуры Windows применяет алгоритм отображе- ния шрифтов (font­mapping algorithm). Данный процесс называется реализацией шрифта (font realization). Шрифты TrueType просто визуализируются на конкрет­ ном устройстве. А для других шрифтов Windows подбирает самый подходящий шрифт устройства, задавая относительную значимость различным характеристи­ кам шрифта. Наиболее важными из характеристик являются (в порядке убывания значимости) название гарнитуры, набор символов, переменное расстояние между символами по сравнению с постоянным, семейство, высота, ширина, плотность, наклон, подчеркнутый и зачеркнутый шрифты. Структуры, связанные со шрифтами Существует более 24 структур, связанных со шрифтами, но две из них выде­ ляются особо. Структура LOGFONT описывает логический шрифт: Public Const LF_FACESIZE = 32 Type LOGFONT lfHeight As Long Логические и физические шрифты
42 Шрифты lfWidth As Long lfEscapement As Long lfOrientation As Long lfWeight As Long lfItalic As Byte lfUnderline As Byte lfStrikeOut As Byte lfCharSet As Byte lfOutPrecision As Byte lfClipPrecision As Byte lfQuality As Byte lfPitchAndFamily As Byte lfFaceName(1 To LF_FACESIZE) As Byte End Type а структура TEXTMETRIC определяет физический: Type TEXTMETRIC tmHeight As Long tmAscent As Long tmDescent As Long tmInternalLeading As Long tmExternalLeading As Long tmAveCharWidth As Long tmMaxCharWidth As Long tmWeight As Long tmOverhang As Long tmDigitizedAspectX As Long tmDigitizedAspectY As Long tmFirstChar As Byte tmLastChar As Byte tmDefaultChar As Byte tmBreakChar As Byte tmItalic As Byte tmUnderlined As Byte tmStruckOut As Byte tmPitchAndFamily As Byte tmCharSet As Byte End Type Функция CreateF ont используется для создания объекта «логический шрифт». Ее параметры воспроизводятся членами структуры LOGFONT: Declare Function CreateFont Lib "gdi32" Alias "CreateFontA" ( _ ByVal nHeight As Long, _ ByVal nWidth As Long, _ ByVal nEscapement As Long, _ ByVal nOrientation As Long, _ ByVal fnWeight As Long, _ ByVal fdwItalic As Long, _ ByVal fdwUnderline As Long, _ ByVal fdwStrikeOut As Long, _
42 ByVal fdwCharSet As Long, _ ByVal fdwOutputPrecision As Long, _ ByVal fdwClipPrecision As Long, _ ByVal fdwQuality As Long, _ ByVal fdwPitchAndFamily As Long, _ ByVal lpszFace As String _ ) As Long Функция CreateFontIndirect выполняет в основном те же самые дейс­ твия, что и предыдущая функция: Declare Function CreateFontIndirect Lib "gdi32" Alias _ "CreateFontIndirectA" ( _ lpLogFont As LOGFONT _ ) As Long Здесь не рассматривается назначение всех этих составляющих. Многие из них достаточно очевидны. Получение текущего логического/физического шрифта API­функция GetTextMetrics возвращает структуру TextMetric для реа­ лизованного в данный момент физического шрифта контекста устройства: BOOL GetTextMetrics( HDC hdc, // Дескриптор контекста устройства. LPTEXTMETRIC lptm // Адрес структуры атрибутов шрифта. ); В VB ее синтаксис таков: Declare Function GetTextMetrics Lib "gdi32" Alias "GetTextMetricsA" ( _ ByVal hdc As Long, _ lpMetrics As TEXTMETRIC _ ) As Long Для получения логического шрифта, который был выбран в контексте уст­ ройства, нужно идти более сложным путем. Функция SelectObject: HGDIOBJ SelectObject( HDC hdc, // Дескриптор контекста устройства. HGDIOBJ hgdiobj // Дескриптор объекта. ); возвращает дескриптор предыдущего объекта заданного типа, который был вы­ бран в контексте устройства. Можно временно выбрать новый шрифт в контек­ сте устройства, для того чтобы получить возвращаемое значение от функции SelectObject. Затем необходимо немедленно восстановить исходный логичес­ кий шрифт: Const SYSTEM_FONT = 13 Dim hCurrentFont As Long Dim lf As LOGFONT ' Получаем дескриптор текущего шрифта. Логические и физические шрифты
430 Шрифты hCurrentFont = SelectObject(Me.hdc, GetStockObject(SYSTEM_FONT)) ' Получаем информацию о текущем шрифте. GetObjectAPI hCurrentFont, LenB(lf), lf ' Восстанавливаем шрифт. SelectObject Me.hdc, hCurrentFont Debug.Print StrConv(lf.lfFaceName, vbUnicode) Перечисление шрифтов Функцию EnumFontFamiliesEx можно использовать для перечисления шрифтов в системе: int EnumFontFamiliesEx( HDC hdc, // Дескриптор контекста устройства. LPLOGFONT lpLogfont, // Указатель на информацию о логическом шрифте. FONTENUMPROC lpEnumFontFamExProc, // Указатель на функцию обратного вызова. LPARAM lParam, // Данные, определяемые приложением. DWORD dwFlags // Зарезервировано; должно быть равно нулю. ); Данная функция перечисления использует функцию обратного вызова, как это делает, например, EnumWindows. Поэтому требуется функция для вызова EnumFontFamiliesEx: Sub EnumFonts() Dim cFonts As Long Dim lgFont As LOGFONT lgFont.lfCharSet = DEFAULT_CHARSET EnumFontFamiliesEx Me.hdc, lgFont, AddressOf EnumFontFamExProc, cFonts, 0 End Sub При установке значения lgFont.lfCharSet = DEFAULT_CHARSET функция EnumFontFamiliesEx будет перечислять все имеющиеся шрифты, использующие все наборы символов. Далее потребуется функция обратного вызова: Public Function EnumFontFamExProc(ByVal lpelfe As Long, _ ByVal lpntme As Long, ByVal FontType As Long, ByRef lParam As Long) _ As Long Dim elfe As ENUMLOGFONTEX Dim sFullName As String ' Получаем копию структуры. CopyMemory elfe, ByVal lpelfe, LenB(elfe)
431 sFullName = Trim0(StrConv(elfe.elfFullName, vbUnicode)) sFullName = sFullName & "" & _ Trim0(StrConv(elfe.elfStyle, vbUnicode)) & "" & _ Trim0(StrConv(elfe.elfScript, vbUnicode)) Form1.List1.AddItem sFullName ' Делаем инкремент счетчика шрифтов. lParam = lParam + 1 ' Продолжаем перечисление. EnumFontFamExProc = 1 End Function Эта программа выведет список шрифтов, изображенный на рис. 25.2 . Наконец, для того чтобы увидеть образец шрифта в рамке изображения (см. рис. 25.2), можно воспользоваться следующей програм­ мой, которая при помощи CreateFont созда­ ет объект выбранного шрифта, а при помощи SelectObject выделяет его в рамке изобра­ жения: Private Sub List1_Click() ' Создаем шрифт и задаем его ' для текстового поля Text1. Dim hFont As Long Dim v As Variant If List1.ListIndex = 1 Then Exit Sub pic.Refresh ' Получаем имя гарнитуры шрифта. v = Split(List1.List(List1.ListIndex), "", 1) ' Создаем логический шрифт. hFont = CreateFont( _  MulDiv(14, GetDeviceCaps(hdc, LOGPIXELSY), 72), _ 0,_ 0,_ 0,_ FW_NORMAL, _ 0,_ 0,_ 0,_ Перечисление шрифтов Рис. 25.2 . Просмотр шрифтов
432 Шрифты ANSI_CHARSET, _ OUT_DEFAULT_PRECIS, _ CLIP_DEFAULT_PRECIS, _ DEFAULT_QUALITY, _ DEFAULT_PITCH, _ v(0)) ' Задаем его для рамки с изображением (picture box). SelectObject pic.hdc, hFont ' Печатаем текст в рамке с изображением. TextOut pic.hdc, 0, 0, "This is " & List1.List(List1.ListIndex), _ Len("This is " & List1.List(List1.ListIndex)) ' После завершения удаляем шрифт. DeleteObject hFont End Sub
Приложение 1. Буфер обмена Приложение 2. Оболочка Windows Приложение 3. Реестр и индивидуальные инициализационные файлы Часть V Приложения
Приложение 1. Буфер обмена Как вам известно, буфер обмена представляет собой механизм Windows, который позволяет передавать данные между приложениями. В Visual Basic есть объект Clipboard, который дает возможность использовать буфер обмена VB­програм­ мистам. Тем не менее есть существенные доводы за применение именно Win32 API­функций, связанных с буфером обмена, а не готового объекта Clipboard. Буфер обмена Windows Один из доводов заключается в том, что объект Clipboard входит в объек­ тную модель Visual Basic (vb5.olb и vb6.olb), но не в объектную модель VB для приложений (vba5.dll и vba6.dll). Поэтому он недоступен при программировании на VBA, например, при создании программ в Microsoft Office. У меня не раз воз­ никала мысль использовать этот объект в приложениях Microsoft Word, Excel или Access, но, судя по всему, в VBA это невозможно. Моим первым шагом, естествен­ но, было добавление ссылки на библиотеку объектов vb6.olb к проекту VBA. Но при попытке сделать это в Word 97 я был «вознагражден» сообщением об общей ошибке защиты (GPF). Более того, я не смог вернуться к своему документу и вынужден был перезагрузить компьютер. Вторым доводом за использование API­функций буфера обмена может слу­ жить создание VB­программы просмотра буфера обмена. Это одна из наиболее полезных небольших утилит, работающая с несколькими фрагментами для встав­ ки в программы, в то время как стандартная утилита просмотра буфера обмена сохраняет только один (последний) скопированный в буфер обмена фрагмент. Как известно, буфер обмена может хранить одновременно данные нескольких разных форматов. Давайте ограничимся только текстовым форматом. Различные виды текста представляют следующие символьные константы: Public Const CF_TEXT = 1 ' Текст ANSI. Public Const CF_OEMTEXT = 7 Public Const CF_UNICODETEXT = 13 Заметьте, что в каждый конкретный момент времени в буфер можно помещать текст только одного формата (или растровое изображение одного формата и т.д .) . Windows автоматически будет преобразовывать текст из одного формата в другой в зависимости от того, в какое приложение осуществляется вставка. Например, если в буфер обмена попадает CF_TEXT (текст в кодировке ANSI), а приложение, в которое осуществляется вставка, требует CF_UNICODETEXT, Windows преобразует текст из ANSI в Unicode.
435 Копирование текста в буфер обмена Процесс копирования текста в буфер обмена включает в себя следующие шаги: 1. Выделить память для размещения данных (GlobalAlloc). 2. Заблокировать выделенную память (GlobalLock). 3. Скопировать данные в память (CopyMemory). 4. Разблокировать память (GlobalUnlock). 5. Открыть буфер обмена (OpenClipboard). 6. Удалить его текущее содержимое (EmptyClipboard). 7. Занести данные в буфер обмена (SetClipboardData). 8. Закрыть буфер обмена (CloseClipboard). Далее эти действия рассматриваются более детально. Выделение памяти для данных Первый шаг – выделить память для данных, используя функцию GlobalAlloc: HGLOBAL GlobalAlloc( UINT uFlags, // Атрибуты выделения памяти. DWORD dwBytes // Количество выделяемых байтов. ); Хотя в документации говорится, что GlobalAlloc совместима только с 16­ разрядной версией Windows, здесь необходимо использовать именно эту функ­ цию. Значит, данное утверждение не совсем верно. В VB можно записать так: Declare Function GlobalAlloc Lib "kernel32" ( _ ByVal uFlags As Long, _ ByVal dwBytes As Long _ ) As Long Параметр uFlags должен иметь значение следующей константы: GMEM_SHARE Or GMEM_MOVEABLE Для обнуления выделенной памяти можно включить сюда также константу GMEM_ZEROINIT. Итак, будут использованы следующие определения: Public Const GMEM_SHARE = &H2000& Public Const GMEM_MOVEABLE = &H2 Public Const GMEM_ZEROINT = &H40 Public Const FOR_CLIPBOARD = GMEM_MOVEABLE Or GMEM_SHARE Or GMEM_ZEROINT Заметьте, что функция GlobalAlloc возвращает дескриптор выделен­ ной памяти. Это не указатель на какую­либо область память (адрес). Причина, по которой получен именно дескриптор, а не указатель, заключается в том, что Windows может переместить в какой­то момент выделенную память в другое место (функции буфера обмена требуют, чтобы выделяемая память имела тип GMEM_MOVEABLE). Буфер обмена Windows
43 Буфер обмена Блокировка, копирование и разблокировка Прежде чем вы сможете использовать выделенный блок памяти, следует полу­ чить на него указатель. Необходимо попросить Windows временно заблокировать (lock) положение выделенной памяти. Для получения указателя нужно вызвать GlobalLock: LPVOID GlobalLock( HGLOBAL hMem // Дескриптор глобального объекта памяти. ); Так эта функция вызывается в VB: Declare Function GlobalLock Lib "kernel32" ( _ ByVal hMem As Long _ ) As Long Следующий шаг – скопировать данные, предназначенные для буфера обмена, в выделенную память. (Если эти данные представляют собой текст, то он должен завершаться нулем.) Для такой цели идеально подходит функция CopyMemory. Указатель, возвращающий GlobalLock, является адресом источника, который требуется для CopyMemory. Как только данные скопированы в выделенную па­ мять буфера обмена, вы должны, используя GlobalUnlock, разблокировать ее: BOOL GlobalUnlock( HGLOBAL hMem // Дескриптор глобального объекта памяти. ); или в VB: Declare Function GlobalUnlock Lib "kernel32" ( _ ByVal hMem As Long _ ) As Long Заметьте, что этой функции требуется дескриптор блока памяти, а не указа­ тель. Если по каким­то причинам вы не сохранили дескриптор, необходимо вос­ становить его по указателю, используя GlobalHandle: HGLOBAL GlobalHandle( LPCVOID pMem // Указатель на глобальный блок памяти. ); Открытие, очистка, установка и закрытие Теперь вы готовы использовать API­функции буфера обмена. Следующий шаг – открытие буфера обмена с помощью OpenClipboard: BOOL OpenClipboard( HWND hWndNewOwner // Дескриптор окна открытого буфера обмена. ); или в VB: Declare Function OpenClipboard Lib "user32" ( _ ByVal hwnd As Long _ ) As Long
43 Этой функции требуется дескриптор окна, которому будут принадлежать дан­ ные буфера обмена. Она возвращает False, если другое приложение уже открыло буфер. Таким образом, можно использовать OpenClipboard для проверки состо­ яния буфера. Далее следует занести данные в буфер обмена, используя SetClipboardData: HANDLE SetClipboardData( UINT uFormat, // Формат буфера обмена. HANDLE hMem // Дескриптор данных. ); или в VB: Declare Function SetClipboardData Lib "user32" ( _ ByVal uFormat As Long, _ ByVal hMem As Long _ ) As Long Учтите, что hMem – это дескриптор блока памяти, а не указатель. Параметр uFormat является символьной константой, которая описывает формат буфера обмена. В заключение следует закрыть буфер обмена: BOOL CloseClipboard(VOID) или в VB: Declare Function CloseClipboard Lib "user32" () As Long В описанном процессе есть несколько моментов, которые нужно выделить особо:  не забудьте разблокировать память, прежде чем передавать ее буферу обмена;  важно не оставлять буфер обмена открытым дольше, чем это действительно необходимо;  после вызова SetClipboardData выделенный блок памяти больше не при­ надлежит вашему приложению, вы не должны пытаться обращаться к нему. Дескриптор и указатель этого блока должны рассматриваться как недейс­ твительные. Windows самостоятельно освобождает любую память, которая больше не нужна. Вы не должны заниматься этим, используя GlobalFree или любые другие способы. Пример Давайте попробуем связать все воедино. Следующая процедура помещает текст в буфер обмена: Sub CopyTextToClipboard(sText As String) Dim hMem As Long, pMem As Long hMem = GlobalAlloc(FOR_CLIPBOARD, LenB(sText)) pMem = GlobalLock(hMem) Буфер обмена Windows
43 Буфер обмена CopyMemory ByVal pMem, ByVal sText, LenB(sText) GlobalUnlock hMem If OpenClipboard(Me.hwnd) <> 0 Then MsgBox "Буфер обмена уже открыт другим приложением." Else EmptyClipboard SetClipboardData CF_TEXT, hMem CloseClipboard End If End Sub Вставка текста из буфера обмена Процесс извлечения текста из буфера обмена включает следующие шаги: 1. Определить, содержит ли буфер обмена данные в текстовом формате (IsC lipboardFormatAvailable). 2. Открыть буфер обмена (OpenClipboard). 3. Получить дескриптор глобальной памяти, содержащей какие­либо данные (GetClipboardData). 4. Заблокировать эту память (GlobalLock). 5. Скопировать текст из блока памяти буфера обмена в память, принадлежа­ щую приложению (CopyMemory). 6. Разблокировать память буфера обмена (GlobalUnlock). 7. Закрыть буфер обмена (CloseClipboard). Следующая функция возвращает текст, находящийся в буфере обмена. Обра­ тите внимание на использование функции GlobalSize для определения размера блока памяти буфера, используемого для хранения текста. Public Function PasteTextFromClipboard() As String Dim hMem As Long, pMem As Long Dim lMemSize As Long Dim sText As String PasteTextFromClipboard = "" ' Проверяем наличие текста в буфере обмена. If IsClipboardFormatAvailable(CF_TEXT) = 0 Then Exit Function End If ' Открываем буфер обмена. If OpenClipboard(frmClipViewer.hwnd) <> 0 Then hMem = GetClipboardData(CF_TEXT) ' Если текста нет, закрываем буфер обмена и выходим. IfhMem=0Then CloseClipboard Exit Function
43 Else ' Получаем указатель памяти. pMem = GlobalLock(hMem) ' Получаем размер памяти. lMemSize = GlobalSize(hMem) ' Размещаем локальную строку. sText = String$(lMemSize, 0) ' Копируем текст из буфера обмена. CopyMemory ByVal sText, ByVal pMem, lMemSize ' Разблокируем память буфера обмена. GlobalUnlock hMem ' Закрываем буфер обмена. CloseClipboard ' Возвращаем текст. PasteTextFromClipboard = Trim0(sText) End If End If End Function Другие функции буфера обмена Вот еще несколько функций, которые, возможно, вы захотите исследовать:  CountClipboardFormats возвращает количество текущих форматов бу­ фера обмена;  EnumClipboardFormats перечисляет текущие форматы буфера обмена;  GetClipboardOwner возвращает дескриптор текущего владельца буфера обмена;  GetOpenClipboardWindow возвращает дескриптор окна, в котором открыт буфер обмена. Пример создания окна просмотра буфера обмена Окно просмотра буфера обмена (clipboard viewer) представляет собой окно, которое получает уведомления об изменении в буфере. В каждый момент време­ ни может существовать несколько активных окон просмотра, но Windows будет посылать уведомляющие сообщения только тому окну, которое было установлено последним в цепочке окон просмотра буфера. Передача сообщения далее по це­ почке входит или не входит в функцию каждого последующего окна просмотра. Для установки окна просмотра буфера обмена следует вызывать функцию SetClipboardViewer: HWND SetClipboardViewer( HWND hWndNewViewer // Дескриптор окна просмотра буфера обмена. ); Здесь hWndNewViewer – дескриптор нового окна просмотра. Возвращаемым значением (в случае успеха) является дескриптор текущего окна просмотра (быв­ шего первым до вызова данной функции), которое становится вторым окном Создание окна просмотра буфера обмена
440 Буфер обмена просмотра в цепочке. Данное значение должно быть сохранено, поскольку оно является дескриптором окна, которому новое окно просмотра должно передавать по цепочке сообщения буфера обмена. После установки окно просмотра будет получать сообщения WM_DRAWCLIPBOARD всякий раз, когда содержимое буфера обмена изменится. Получив сообщение, окно просмотра может (в своей оконной процедуре) извлечь данные буфера обмена. Вызов ChangeClipboardChain приведет к тому, что Windows удалит окно просмотра из цепочки. Синтаксис этой функции таков: BOOL ChangeClipboardChain( HWND hWndRemove, // Дескриптор удаляемого окна. HWND hWndNewNext // Дескриптор следующего окна. ); Параметр hWndRemove должен быть дескриптором окна просмотра, ко­ торое следует удалить, а параметр hWndNewNext – дескриптором следующего окна в цепочке. После вызова этой функции Windows пошлет сообщение WM_ CHANGECBCHAIN текущему окну просмотра. Заметьте, что текущим может быть окно, вызвавшее ChangeClipboardChain, или даже удаляемое окно просмотра. Любое приложение может удалить любое окно просмотра буфера обмена, если ему известен дескриптор этого окна, а также дескриптор следующего в цепочке. Параметрами этого сообщения являются wParam = hWndRemove lParam = hWndNewNext Иными словами, параметры в функцию ChangeClipboardChain передаются через оконную процедуру. Это достаточно тонкая операция, поэтому следует рассмотреть ее более под­ робно. На рис. П1.1 показана цепочка из четырех окон просмотра. Предположим, что какому­то приложению потребовалось вызвать функцию ChangeClipboardChain: ChangeClipboardChain hWndX, hWnd(X+1) чтобы удалить окно просмотра ViewerX (где X = 1, 2, 3 или 4). Viewer1 (Текущее окно просмотра) Viewer2 Viewer3 Viewer4 Рис. П1.1. Цепочка окон просмотра буфера обмена
441 В результате Windows удалит ViewerX из цепочки буфера обмена и пошлет со­ общение WM_CHANGECBCHAIN окну просмотра Viewer1 (текущее окно просмотра). Существует три возможных варианта, которые следует рассмотреть. Если ViewerX – это Viewer1, тогда Viewer1 удален. Теперь Windows считает текущим окном просмотра Viewer2 и, следовательно, никакие дополнительные действия не нужны. В частности, окну просмотра Viewer1 не нужно обрабатывать сообщение WM_CHANGECBCHAIN, но следовало бы передать это сообщение по це­ почке окну Viewer2. Заметьте, что окно Viewer1 может определить, что удаляется именно оно, если hWndX (= hWndRemove = wParam) является его собственным дескриптором. Если ViewerX – это Viewer2, тогда окно Viewer1 должно выполнить некоторые действия. Дело в том, что Viewer1 хранит дескриптор следующего в цепочке окна просмотра (Viewer2). Поскольку оно удалено, то его дескриптор больше не явля­ ется дескриптором следующего в цепочке окна. Поэтому окну Viewer1 требуется, чтобы его переменная hNextViewer указывала на окно Viewer3, как в следующем фрагменте: If wParam = hNextViewer Then ' wParam = hWndRemove = hWnd2 hNextViewer = lParam ' lParam = hWnd3 End If Наконец, если ViewerX – это не текущее окно просмотра (Viewer1) и не следу­ ющее за ним (Viewer2), то сообщение должно передаваться далее по цепочке. Обратите внимание, что можно немного упростить программу, передавая сооб­ щение по цепочке без обработки во всех случаях, за исключением, когда ViewerX является Viewer2: If wParam = hNextViewer Then ' Отсоединяем окно просмотра. hNextViewer = lParam Else ' Просто передаем сообщение. SendMessage hNextViewer, WM_CHANGECBCHAIN, wParam, lParam End If Итак, для создания окна просмотра буфера обмена нужно выполнить следу­ ющие шаги: 1. Вызвать SetClipboardViewer. 2. Обработать сообщения WM_CHANGECBCHAIN и WM_DRAWCLIPBOARD. В VB придется, конечно, модифицировать класс окна так, чтобы можно было обрабатывать эти сообщения. Архив примеров содержит приложение rpiClipViewer, которое подключается к цепочке буфера обмена и помещает весь копируемый в буфер текст в окно со списком (сейчас оно настроено так, что хранит только последние 100 копий). Таким образом, если требуется извлечь фрагмент, помещенный в буфер обмена несколько «копий» тому назад, то надо просто найти его в буфере. На рис. П1.2 показано главное окно rpiClipViewer. Создание окна просмотра буфера обмена
442 Буфер обмена Текстовое поле внизу показывает фрагмент, выбранный в окне со списком, но с добавленными символами перевода строки для удобства чтения. Двойной щелчок на пункте окна со списком (или нажатие клавиши Enter) отошлет данный фрагмент в буфер обмена для последующей вставки. Событие Load для этой формы выглядит так: Private Sub Form_Load() Dim hnd As Long ' Ищем работающее окно просмотра и переключаемся, ' если оно существует. hnd = FindWindow("ThunderRT6FormDC", "rpiClipViewer") If hnd <> 0 And hnd <> Me.hwnd Then SetForegroundWindow hnd End End If BecomeClipboardViewer bAddToList = True Me.Show End Sub С помощью этой программы можно определить, работает или нет в данный момент какое­либо окно просмотра буфера обмена. Если такое окно имеется, то приложение переключается на него. Проверка существования работающего экзем­ пляра приложения обсуждалась в главе 11. Данный подход надуман и не совсем очевиден, поэтому рассмотрим его кратко. Рис. П1.2 . Окно программы просмотра буфера обмена
443 Один из способов определения наличия работающего приложения за­ ключается в том, чтобы использовать FindWindow для проверки существова­ ния окна с соответствующим заголовком. Однако, как только происходит собы­ тие Load, такое окно завершает свою работу, поэтому приведенный выше код возвращает свой собственный дескриптор. Решить эту проблему можно таким образом: во время проектирования задать для заголовка главной формы значе­ ние, отличное от окончательного, например, rpiClipView вместо rpiClipViewer. Затем нужно изменить заголовок в событии Activate, связанном с данной формой: Private Sub Form_Activate() Me.Caption = "rpiClipViewer" End Sub Указанное событие не произойдет, пока не будет выполнен код, связанный с событием Load. Обратите внимание на использование оператора END для завер­ шения обработки события Load, если приложение уже выполняется. Функция BecomeClipboardViewer просто модифицирует класс окна со списком и вызывает SetClipboardViewer: Sub BecomeClipboardViewer() ' Модифицируем класс командной кнопки. Subclass If Not bIsSubclassed Then MsgBox "Модификация невозможна", vbCritical Exit Sub End If ' Устанавливаем кнопку в качестве окна просмотра буфера обмена. hNextViewer = SetClipboardViewer(lstItems.hwnd) End Sub Для модификации класса используется функция SetWindowLong: Sub Subclass() ' Модифицируем класс кнопки. hPrevWndProc = SetWindowLong(lstItems.hwnd, GWL_WNDPROC, AddressOf _ WindowProc) If hPrevWndProc <> 0 Then bIsSubclassed = True End If End Sub При обработке события Unload вызывается следующая функция для удале­ ния (unhook) данного окна из цепочки окон просмотра буфера обмена: Создание окна просмотра буфера обмена
444 Буфер обмена Sub UnbecomeClipboardViewer() ChangeClipboardChain lstItems.hwnd, hNextViewer RemoveSubclass End Sub Далее приведена оконная процедура для окна просмотра буфера обмена (окно со списком): Public Function WindowProc(ByVal hwnd As Long, ByVal iMsg As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long Dim sItem As String Select Case iMsg Case WM_DRAWCLIPBOARD ' Получаем текст буфера обмена и помещаем его в текстовое поле. sItem = PasteTextFromClipboard If sItem <> "" Then frmClipViewer.txtCurrent = sItem If sItem <> "" And bAddToList Then ' Добавляем пункт к окну со списком. cItems = cItems + 1 frmClipViewer.lstItems.AddItem sItem, 0 ' Если превышен максимум, удаляем последний пункт. If cItems > MAX_ITEMS Then frmClipViewer.lstItems.RemoveItem MAX_ITEMS  1 cItems = cItems  1 End If ' Выбираем пункт. If frmClipViewer.lstItems.ListCount >= 1 Then frmClipViewer.lstItems.Selected(0) = True End If ' Обновляем метку. frmClipViewer.lblItemCount = cItems & " элементов" End If ' Посылаем сообщение следующему окну просмотра буфера обмена. If hNextViewer <> 0 Then SendMessage hNextViewer, WM_DRAWCLIPBOARD, wParam, lParam End If Exit Function Case WM_CHANGECBCHAIN ' Смотрим, какое окно просмотра удалено. ' Является ли оно следующим в цепочке? ' wParam содержит дескриптор удаленного окна просмотра.
445 If wParam = hNextViewer Then ' Удаляем это окно просмотра из цепочки. hNextViewer = lParam Else SendMessage hNextViewer, WM_CHANGECBCHAIN, wParam, lParam End If Exit Function End Select ' Вызываем исходную оконную процедуру. WindowProc = CallWindowProc(hPrevWndProc, hwnd, iMsg, wParam, lParam) End Function Создание окна просмотра буфера обмена
Приложение 2. Оболочка Windows В этом приложении дан краткий обзор оболочки Windows. Вы познакомитесь с ее основными возможностями, которыми не так уж трудно пользоваться, а получить любую дополнительную информацию об оболочке, отсутствующую здесь, вы смо­ жете непосредственно из документации. Оболочка (shell) – это приложение Windows, которое обеспечивает управление другими приложениями. Функции оболочки Windows содержатся, в основном, в библиотеке shell32.dll. Установка конкретной версии Internet Explorer изменяет набор выполняемых оболочкой Windows функций. Библиотека оболочки shell32.dll поддерживает множество различных возмож­ ностей. Ниже приведены лишь некоторые из них:  перетаскивание (drag­and­drop) файлов из Проводника (File Manager);  ассоциации файлов (file associations);  извлечение значков из исполняемых файлов;  системная область значков на панели задач (system tray);  операции с файлами, например, удаление файла в Корзину;  прочие функции оболочки (такие как добавление документа в список пос­ ледних открывавшихся). Следует подчеркнуть, что наличие тех или иных функций оболочки зависит от установленных на компьютере версий shell32.dll и comctl32.dll, а это, в свою очередь, зависит от установленных версий Windows и Internet Explorer. Согласно документации, существуют следующие номера версий:  версия 4.0: Windows 95/4.0;  версия 4.70: Internet Explorer 3.x;  версия 4.71: Internet Explorer 4.0;  версия 4.72: Internet Explorer 4.01;  версия 5.00: Windows NT 5.0 и Internet Explorer 5.0 . Из документации не совсем ясно, к чему конкретно относится номер версии, хотя там все же отмечается, что версии у shell32.dll и comctl32.dll должны быть одни и те же (за исключением версии 5.0). Однако на конкретном компьютере это условие может и не выполняться. Например, утилита rpiPEInfo, о которой уже говорилось в книге, выводит следующие значения на моем компьютере: Shell32.dll: File Version 4.0 .1381.4 Comctrl32.dll: File Version 4.72 .3110.1 На моем компьютере установлен Internet Explorer 4.0, версия 4.72 .3110.8 . Пос­ кольку я не устанавливал ни одной новой версии данных файлов, очевидно, что
44 здесь что­то не так. Это создает проблемы при разработке приложений оболочки, которые были бы совместимы с большинством существующих систем. А теперь перейдем к некоторым возможностям оболочки Windows. Перетаскивание В оболочке Windows относительно просто добавить к управляющему элементу VB возмож­ ность распознавать файлы, перетаскиваемые (dragged) из Проводника Windows и опускаемые (dropped) на этот управляющий элемент. Вся сложность в том, что Windows сообщает это­ му элементу о том, что файл был перетащен и опущен. Следовательно, требуется модифици­ ровать класс соответствующего управляющего элемента. Исходный код rpiShell демонстрирует, как сделать доступной возможность drag­and­drop (перетащить и опустить) в VB. На рис. П2.1 по­ казано главное окно (этот проект иллюстрирует также ассоциации файлов и работу с реестром, обсуждаемые в приложении 3). Чтобы увидеть действие программы, нажмите кнопку Enable Drag-and-Drop (Разрешить пе­ ретаскивание и опускание) и перетащите один файл из окна Проводника в рамку с изображением, которая находится справа от кнопки. Полное имя (путь и имя) файла появится в текстовом поле. Исходный код этого примера довольно прост. Кроме выполнения модифика­ ции класса рамки с изображением (модификация класса рассматривалась в главе 18), нужно вызвать API­функцию оболочки DragAcceptFiles, декларация VB которой выглядит так: Declare Sub DragAcceptFiles Lib "shell32.dll" _ (ByVal hWnd As Long, ByVal fAccept As Long) Здесь hWnd является дескриптором окна, которому оболочка посылает сооб­ щения, а fAccept должен иметь значение True, чтобы разрешить перетаскивание, или False, чтобы его запретить. Следующий исходный код модифицирует класс и разрешает перетаскивание (bIsSubClassed – переменная уровня модуля): Sub EnableDrag() ' Модифицируем класс рамки с изображением. hPrevWndProc = SetWindowLong(Picture1.hWnd, GWL_WNDPROC, AddressOf _ WindowProc) If hPrevWndProc <> 0 Then Рис. П2.1. Окно программы, иллюстрирующей перетаскивание Перетаскивание
44 Оболочка Windows bIsSubclassed = True fraDragDrop.Caption = "DragDrop (Разрешен)" ' Устанавливаем перетаскивание. DragAcceptFiles Picture1.hWnd, True End If End Sub А следующий код, наоборот, отключает и перетаскивание, и модификацию класса: Sub DisableDrag() Dim lret As Long ' Отключаем, если нужно, модификацию класса. If bIsSubclassed Then lret = SetWindowLong(Picture1.hWnd, GWL_WNDPROC, hPrevWndProc) bIsSubclassed = False fraDragDrop.Caption = "DragDrop (Запрещен)" ' Снимаем установку перетаскивания. DragAcceptFiles Picture1.hWnd, False End If End Sub Когда файл перетаскивается и опускается на рамку с изображением, оболочка Windows посылает рамке с изображением сообщение WM_DROPFILES. Следующая оконная процедура обрабатывает это сообщение. Public Function WindowProc(ByVal hWnd As Long, ByVal iMsg As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long Dim lpBuffer As String lpBuffer = String$(1024, 0) Select Case iMsg Case WM_DROPFILES ' Извлекаем имя файла. DragQueryFile wParam, 0, lpBuffer, 1024 ' Распечатываем его. frmShell.txtDragDrop.Text = lpBuffer ' Освобождаем ресурсы. DragFinish wParam
44 End Select ' Вызываем исходную оконную процедуру. WindowProc = CallWindowProc(hPrevWndProc, hWnd, iMsg, wParam, lParam) End Function В ответ на сообщение WM_DROPFILES нужно вызвать одну или обе функции оболочки – DragQueryFile или DragQueryPoint. Первая функция возвращает имя (и путь) перетаскиваемого файла, а вторая – положение точки внутри рамки с изображением, в которую был опущен файл. Кроме того, следует освободить вызовом функции DragFinish используемые ресурсы. Далее приводятся декла­ рации указанных функций: Declare Function DragQueryFile Lib "shell32.dll" Alias "DragQueryFileA" ( _ ByVal HDROP As Long, ByVal UINT As Long, _ ByVal lpStr As String, ByVal ch As Long) As Long Declare Function DragQueryPoint Lib "shell32.dll" ( _ ByVal HDROP As Long, lpPoint As POINTAPI) As Long Declare Sub DragFinish Lib "shell32.dll" (ByVal HDROP As Long) Параметр hDROP, имеющийся в каждой из функций, передается в параметре wParam оконной процедуры. Он идентифицирует опускаемый файл (или фай­ лы). Данная программа работает только с одним файлом, но вы можете легко ее усовершенствовать так, чтобы она выполнялась для нескольких одновременно опускаемых файлов. Ассоциации файлов Как вам известно, Windows может устанавливать связь (ассоциацию) между расширением файла и приложением (исполняемым файлом). Благодаря этому двойной щелчок по значку файла в окне Проводника или выбор пункта Open (Открыть) из всплывающего контекстного меню (которое появляется в результате нажатия правой кнопки мыши), приводит к тому, что Windows запускает ассоци­ ированное с файлом приложение и загружает в него сам файл. Оболочка Windows позволяет нам с выгодой применять эту возможность в приложениях Visual Basic. В частности, можно использовать функцию оболочки FindExecutable для получения полного пути и имени файла того приложения, которое ассоциировано с данным файлом; затем применить ShellExecute, чтобы открыть этот файл в связанном с ним приложении. Декларация VB функции FindExecutable выглядит так: Declare Function FindExecutable Lib "shell32.dll" Alias "FindExecutableA" ( _ ByVal lpFile As String, ByVal lpDirectory As String, _ ByVal lpResult As String) As Long Здесь lpFile – имя соответствующего файла, а lpDirectory – имя рабочего каталога приложения по умолчанию. Это необязательное значение (его можно ус­ тановить в NULL) используется для поиска файла, заданного параметром lpFile, когда последний не содержит полного пути. Параметр lpResult является буфером Ассоциации файлов
450 Оболочка Windows строки, которая принимает полное имя исполняемого приложения. Буфер должен иметь размер не меньше, чем MAX_PATH ( = 260). Приложение rpiShell, окно которого показано на рис. П2.1, иллюстрирует ис­ пользование FindExecutable. Исходный код, связанный с кнопкой Associate (Ассоциировать), достаточно прост: Private Sub cmdAssociate_Click() Dim sFile As String Dim sEXE As String sEXE = String$(MAX_PATH, 0) sFile = txtFile If sFile = "" Then Exit Sub FindExecutable sFile, vbNullString, sEXE txtEXE = sEXE End Sub Функция ShellExecute может или открывать файл, или распечатывать его. Так она декларируется в VB: Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (_ ByVal hWnd As Long, _ ByVal lpOperation As String, _ ByVal lpFile As String, _ ByVal lpParameters As String, _ ByVal lpDirectory As String, _ ByVal nShowCmd As Long _ ) As Long К примеру, исходный код, связанный с кнопкой Execute (Выполнить) прило­ жения rpiShell (см. рис. П2.1), записывается таким образом: Private Sub cmdExecute_Click() Dim sFile As String Dim lResp As Long sFile = txtFile lResp = ShellExecute(Me.hWnd, "open", sFile, vbNullString, _ vbNullString, SW_SHOWNORMAL) End Sub Заметьте, в документации говорится, что последний параметр должен быть ус­ тановлен в нуль для файла с документом, но на моем компьютере это не работает. Функцию ShellExecute можно также использовать для того, чтобы открыть папку в Проводнике Windows Explorer. Далее представлены три варианта функ­
451 ции, отличающиеся только вторым параметром: ShellExecute(handle, vbNullString, <PathToFolder>, _ vbNullString, vbNullString, SW_SHOWNORMAL); ShellExecute(handle, "open", <PathToFolder>, _ vbNullString, vbNullString, SW_SHOWNORMAL); ShellExecute(handle, "explore", <PathToFolder>, _ vbNullString, vbNullString, SW_SHOWNORMAL); Первые два варианта ShellExecute открывают заданную папку. Например, следующий вызов: ShellExecute Me.hWnd, "open", "d:\temp", _ vbNullString, vbNullString, SW_SHOWNORMAL выводит на экран диалоговое окно, показанное на рис. П2.2 . Третий вариант ShellExecute открывает новое окно Проводника и выводит содержимое заданной папки. Рис. П2.2 . Пример использования ShellExecute Системная область значков на панели задач В оболочке Windows реализованы функции для работы с системной областью значков (system tray), размещенной на панели задач. Вы, скорее всего, знакомы с системной областью значков. Используя функции оболочки Windows, можно разместить в этой области панели задач пиктограммы, предназначенные для быстрого обращения к программам с помощью мыши. Для работы с областью значков потребуется функция Shell_NotifyIcon: Public Declare Function Shell_NotifyIcon Lib "shell32" _ Alias "Shell_NotifyIconA" ( _ ByVal dwMessage As Long, _ lpData As NOTIFYICONDATA _ ) As Boolean При помощи данной функции можно добавить, модифицировать или удалить пиктограмму в системной области значков. Таким образом, параметр dwMessage может иметь одно из следующих значений:  NIM_ADD – добавить пиктограмму;  NIM _MODIFY – изменить пиктограмму;  NIM _DELETE – удалить пиктограмму. Системная область значков на панели задач
452 Оболочка Windows Параметр pnid является адресом структуры NOTIFYICONDATA, содержание которой зависит от значения dwMessage: Public Type NOTIFYICONDATA cbSize As Long hwnd As Long uID As Long uFlags As Long uCallbackMessage As Long hIcon As Long szTip(1 To 64) As Byte End Type Структура состоит из следующих членов:  cbSize указывает размер данной структуры в байтах, который составляет 88 байт. Заметим, что правильнее было бы воспользоваться функцией LenB;  hWnd является дескриптором окна, которое получает уведомляющие сооб­ щения, связанные с пиктограммой системной области панели задач;  uID представляет собой определяемый приложением идентификатор пик­ тограммы в системной области. Может принимать значение 0;  uFlags является комбинацией флагов, указывающей, какие из других чле­ нов структуры содержат действительные данные. Возможны следующие варианты: – NIF_ICON. Член hIcon содержит действительные данные; – NIF_MESSAGE. Член uCallbackMessage содержит действительные данные; – NIF_ICON. Член szTip содержит действительные данные;  uCallbackMessage представляет собой идентификатор сообщения. Windows будет посылать это сообщение окну, идентификатор которого определяется членом структуры hwnd, в случае наступления события, связанного с переме­ щением мыши в пределах ограничивающего прямоугольника пиктограммы в системной области;  hIcon является дескриптором пиктограммы, которая добавляется, модифи­ цируется или удаляется;  szTip определяет текст всплывающей подсказки для данной пиктограммы. Согласно документации, нужно установить функцию обратного вызова на дру­ гое, неиспользуемое событие мыши. В примере, взятом из документации, им явля­ ется событие формы MouseMove. Достигается это следующими установками: nid.hwnd = Me.hWnd nid.uCallbackMessage = WM_MOUSEMOVE Если в ограничивающем прямоугольнике пиктограмм системной области про­ изойдет событие мыши (то есть будет перемещен указатель, произойдет одинар­ ный или двойной щелчок), Windows инициирует событие формы MouseMove. Более того, она пошлет идентификатор сообщения мыши в параметре X события формы MouseMove следующим образом: X = MessageID*Screen.TwipsPerPixelX
453 Следовательно, чтобы восстановить идентификатор сообщения, нужно поде­ лить X на Screen.TwipsPerPixelX. Пример Рассмотрим в качестве примера следующую программу, которая добавляет (удаляет) пиктограмму системной области и реагирует на события двойного щел­ чка левой клавиши или освобождения (отжатия) правой клавиши мыши. Событие Load заполняет структуру NOTIFYICONDATA: Private Sub Form_Load() Dim sTip As String ' Заполняем структуру NOTIFYICONDATA. sTip = "Всплывающая подсказка" CopyMemory nid.szTip(1), ByVal sTip, LenB(sTip) nid.cbSize = LenB(nid) nid.hwnd = Me.hwnd nid.uID = 0 nid.hIcon = Me.Icon.Handle nid.uFlags = NIF_ICON Or NIF_MESSAGE Or NIF_TIP ' Устанавливаем сообщение обратного вызова на перемещение мыши. nid.uCallbackMessage = WM_MOUSEMOVE End Sub Событие формы MouseMove реагирует на события мыши в ограничивающем прямоугольнике пиктограммы системной области, посылая соответствующую текстовую строку окну Immediate. Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) ' Когда мышь пересекает значок панели задач, ' значение X равно MessageID * Screen.TwipsPerPixelX. Select Case X / Screen.TwipsPerPixelX Case WM_LBUTTONDBLCLK Debug.Print "Двойной щелчок мыши" Case WM_RBUTTONUP Debug.Print "Щелчок правой кнопкой мыши" End Select End Sub Следующие процедуры добавляют или удаляют пиктограмму системной об­ ласти панели задач: Системная область значков на панели задач
454 Оболочка Windows Private Sub cmdAdd_Click() Shell_NotifyIcon NIM_ADD, nid End Sub Private Sub cmdDelete_Click() Shell_NotifyIcon NIM_DELETE, nid End Sub Теперь у вас есть возможность придумать какое­нибудь новое применение для системной области и ее пиктограмм. Сообщите, если найдете что­то действительно интересное. Операции с файлами В оболочке Windows реализована функция SHFileOperation, которая поз­ воляет копировать, переименовывать и удалять файлы или папки так же, как это делается в Проводнике. В частности, при копировании файла на экран выводится диалоговое окно с индикатором выполнения (если, конечно, операция копиро­ вания занимает достаточно времени, чтобы система успела вывести это окно), а при удалении файла одновременно с помещением его в Корзину появляется окно, символизирующее этот процесс. Декларация SHFileOperation вместе с декла­ рацией соответствующего пользовательского типа записываются таким образом: Type SHFILEOPSTRUCT hWnd As Long wFunc As Long pFrom As String pTo As String fFlags As Long fAnyOperationsAborted As Long hNameMappings As Long lpszProgressTitle As String End Type Declare Function SHFileOperation Lib "shell32.dll" Alias _ "SHFileOperationA" ( _ lpFileOp As SHFILEOPSTRUCT) As Long Не будем подробно анализировать эту функцию, приведем только примеры удаления и копирования. Следующий исходный код реализует удаление файла в Корзину (Recycle Bin): Dim DelFileOp As SHFILEOPSTRUCT Dim result As Long ' Инициализируем структуру. With FileOp .hWnd = 0 . wFunc = FO_DELETE ' Путь и имя удаляемого файла. . pFrom = <filepathname>
455 . fFlags = FOF_SILENT Or FOF_ALLOWUNDO Or FOF_NOCONFIRMATION End With ' Удаляем. result = SHFileOperation(DelFileOp) If result <> 0 Then ' При выполнении операции произошла ошибка. MsgBox "При выполнении операции удаления произошла ошибка". Else If DelFileOp.fAnyOperationsAborted <> 0 Then MsgBox "Операция прервана" End If End If Параметр wFunc структуры SHFILEOPSTRUCT может принимать любое из следующих значений: FO_COPY, FO_DELETE, FO_MOVE или FO_RENAME. Параметр fFlags может быть задан комбинацией значений, каждое из которых определя­ ет отдельные установки, например, необходимость подтверждения выполнения соответствующей операции. Флаг fAnyOperationsAborted устанавливается Windows в состояние True, если пользователь прервал выполнение данной опе­ рации. Следующий исходный код, также взятый из проекта rpiShell, будет копиро­ вать файл, отображая стандартный индикатор выполнения копирования, который использует Проводник Windows. Обратите внимание, что индикатор выводится на экран только в том случае, если операция копирования занимает достаточно времени, чтобы система могла его отобразить. Private Sub cmdFileCopy_Click() Dim FileOp As SHFILEOPSTRUCT Dim result As Long With FileOp .hWnd = 0 . wFunc = FO_COPY . pFrom = InputBox("Задайте имя и путь к копируемому файлу") . pTo = InputBox("Задайте целевой каталог") . fFlags = FOF_NOCONFIRMATION End With result = SHFileOperation(FileOp) If result <> 0 Then MsgBox Err.LastDllError Else If FileOp.fAnyOperationsAborted <> 0 Then MsgBox "Операция прервана" End If End If End Sub Операции с файлами
45 Оболочка Windows Корзина Оболочки Windows 98/2000 поддерживают две функции для непосредствен­ ной работы с Корзиной: функция SHQueryRecycleBin возвращает количество элементов в Корзине и ее общий объем в байтах, а функция SHEmptyRecycleBin очищает Корзину. Учтите, что эти функции не поддерживаются в Windows 95 или Windows NT 4. Функция SHQueryRecycleBin декларируется так: Declare Function SHQueryRecycleBin Lib "shell32.dll" Alias _ "SHQueryRecycleBinA" (ByVal sRootPath As String, _ lpRBInfo As SHQUERYRBINFO) As Long где sRootPath – это строка, начинающаяся с имени диска, на котором находится Корзина (например, «c:\temp» для диска C). Структура SHQUERYRBINFO объяв­ ляется на языке C следующим образом. struct _SHQUERYRBINFO { DWORD cbSize; _ _ int64 i64Size; // Общий размер всех элементов в Корзине. _ _ int64 i64NumItems; // Количество элементов в Корзине. } Здесь в первый раз используется 64­разрядный тип данных – __int64. Струк­ туру можно преобразовать к виду VB следующим образом: Type SHQUERYRBINFO cbSize As Long lSizeLow As Long lSizeHigh As Long lCountLow As Long lCountHigh As Long End Type Декларация для SHEmptyRecycleBin записывается так: Declare Function SHEmptyRecycleBin Lib "shell32.dll" Alias _ "SHEmptyRecycleBinA" (ByVal hWnd As Long, ByVal sRootPath As String, _ ByVal dwFlags As Long) As Long где hWnd – дескриптор окна, получающего все диалоговые сообщения (для вызова функции из своей программы устанавливайте его равным Me.hWnd), sRootPath играет ту же роль, что и в SHQueryRecycleBin, а dwFlags управляет отобра­ жением диалоговых окон подтверждения и индикатора выполнения операции с помощью следующих значений:  SHERB_NOCONFIRMATION. Диалоговое окно подтверждения операции не выводится;  SHERB_NOPROGRESSUI. Диалоговое окно индикатора выполнения операции не выводится;  SHERB_NOSOUND. Отсутствует звуковое сопровождение завершения опера­ ции.
Приложение 3. Реестр и индивидуальные инициализационные файлы В Windows API входит несколько десятков функций для управления реестром. Кро­ ме того, Win32 поддерживает индивидуальные инициализационные файлы (private profile file), называемые также INI­файлами. В этой главе обсуждаются оба эти метода сохранения постоянных (persistent) данных. (Лично я предпочитаю исполь­ зовать индивидуальные инициализационные файлы, поскольку не люблю влезать в реестр Windows – в файл, который является жизненно важным для правильного функционирования операционной системы.) Реестр Windows Давайте начнем с небольшого обзора терминологии. На рис. П3.1 показана часть реестра Windows в том виде, в каком его отображает Редактор реестра (Registry Editor). Элементы в левой части окна называются ключами (key). Ключ, который на рисунке изображен открытым, можно записать так: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Browser\CurrentVersion Рис. П3.1. Реестр Windows
45 Реестр и INI-файлы Ключи реестра упорядочены в иерархическую систему «ключ–подключ» (key – subkey) с шестью ключами верхнего уровня:  HKEY_CLASSES_ROOT. Данный ключ можно назвать ссылкой (link) на ключ HKEY_LOCAL_MACHINE\SOFTWARE\Classes (и его подключи). Ссылка создается каждый раз при загрузке Windows. Данная часть реестра содержит определения типов документов, ассоциаций файлов и информации, относя­ щейся к классам;  HKEY_CURRENT_USER. Является ссылкой на часть реестра HKEY_USERS, которая относится к пользователю текущего сеанса;  HKEY_LOCAL_MACHINE. Содержит информацию об аппаратной и програм­ мной конфигурациях компьютера. Данные этого ключа хранятся в файле system.dat;  HKEY_USERS. Информация о каждом из пользователей находится в файле реестра user.dat. Когда пользователь начинает сеанс работы на компьюте­ ре, его данные заносятся в реестр, в ключ HKEY_USERS. Фактически в каж­ дый данный момент времени ключ HKEY_USERS содержит информацию о пользователе по умолчанию и о пользователе текущего сеанса. Таким обра­ зом, пользователь не может увидеть или изменить специальную информа­ цию другого пользователя;  HKEY_CURRENT_CONFIG. Является ссылкой на информацию о текущей кон­ фигурации компьютера, которая находится в ключе HKEY_LOCAL_MACHINE;  HKEY_DYN_DATA. Определяет использование механизма plug­and­play для хранения динамических данных. Каждый ключ имеет одно или несколько связанных с ним значений (value). В правой части окна рис. П3.1 показаны значения выбранного ключа. Значение состоит из двух частей: имени значения (value name), или просто имени, и данных значения (value data), или просто данных. Например, ключ, выбранный на рис. П3.1, имеет 10 значений. Данные, хранящиеся в значении ключа, могут быть трех типов: строка, двоичные данные (binaryz) и двойное слово (dword). Строковые данные в Редакторе реестра всегда отображаются в кавычках. Дво­ ичные данные записываются в виде последовательности байтов в шестнадцате­ ричной системе: 74e659ff Наконец, данные DWORD также вводятся шестнадцатеричным кодом, но начи­ наются с символов 0x. Значение типа DWORD интерпретируется скорее как одно шестнадцатеричное значение, чем как двоичное слово. Значение InstallData на рис. П3.1 является примером данных типа DWORD. Прежде чем говорить о связанных с реестром API­функциях, я хотел бы из­ ложить свое собственное мнение о реестре, несмотря на то что такой подход, по­ видимому, не пользуется популярностью. Я пишу много приложений, которые требуют сохранения постоянных (persistent) данных от сеанса к сеансу. Раньше многие коммерческие приложения применяли для хранения таких данных файл Win.ini, хотя можно было легко использовать индивидуальные инициализационные INI­файлы с помощью связанных с ними
45 API­функций (об этом еще будет говориться позже в этом приложении). Это было не очень удобно, так как файл Win.ini существенно разрастался. Теперь Microsoft рекомендует хранить постоянные данные приложений в реестре. Я считаю такой подход ошибочным. Реестр жизненно важен для функциони­ рования самой Windows. Малейшие просчеты в нем могут привести к нарушению работы всей системы. На мой взгляд, нет смысла в размещении индивидуальных данных приложения в компоненте, который настолько значителен для функцио­ нирования всей системы в целом, особенно в ситуации, когда так легко воспользо­ ваться индивидуальными INI­файлами. Кто из нас может утверждать, что никогда не ошибается при программировании и что нет ошибок в той части Win32, которая связана с реестром? По моему мнению, INI­файлы очень полезны. Такой файл можно открыть с помощью Блокнота и внести необходимые изменения, что гораздо легче, чем пользо­ ваться приложением со специальным интерфейсом. Конечно, это может быть рискованной процедурой для приложения, поэтому я всегда настоятельно реко­ мендую пользователям здесь не экспериментировать. В качестве примера позвольте привести случай из жизни. Однажды мне позвонил рассерженный пользователь одной из моих программ (программа Smart Directory для работы с файлами). Он сказал, что после установки программы его компьютер стал работать нестабильно. По его мнению, причиной проблем стала допущенная мною при записи информации в реестр ошибка. К счастью, я мог заверить его, что это не тот случай, поскольку моя программа вообще не связана с реестром. Однако бывают ситуации, когда возникает реальная необходимость обраще­ ния к реестру. Например, когда программист случайно изменяет имеющееся в нем значение. Кроме того, иногда реестр необходим для извлечения списка значений ключа. Очень скоро вы познакомитесь с таким примером. API-функции, связанные с реестром Давайте рассмотрим API­функции, связанные с реестром. За исключением функции RegConnectRegistry, которая используется для установки соедине­ ния с Редактором реестра другого компьютера, их можно разделить на две группы: функции для работы с ключами и функции для работы со значениями. Функции для работы с ключами Ниже перечислены API­функции, связанные с ключами:  RegCloseKey закрывает открытый ключ реестра;  RegCreateKeyEx создает новый ключ;  RegDeleteKey удаляет ключ из реестра;  RegEnumKeyEx перечисляет подключи ключа;  RegFlushKey записывает в реестр атрибуты открытого ключа;  RegLoadKey создает подключ в ключах HKEY_USER или HKEY_LOCAL_MACHINE и сохраняет в нем регистрационную информацию из заданного файла. Данные в этом файле должны быть организованы в форме так называемого улья (hive), который представляет собой набор ключей, подключей и значений. Улей связан с одним из ключей, находящимся на вершине иерархии реестра; Реестр Windows
40 Реестр и INI-файлы  RegNotifyChangeKeyValue уведомляет вызывающую программу об изме­ нениях в ключе реестра (но не об удалении ключа);  RegOpenKeyEx открывает ключ реестра;  RegQueryInfoKey извлекает информацию о ключе реестра;  RegReplaceKey заменяет информацию данного ключа и его подключей данными из файла. При перезапуске системы ключ и его подключи будут иметь значения, сохраненные в файле;  RegRestoreKey читает информацию реестра из заданного файла и копи­ рует ее в реестр, перезаписывая любые существующие для заданных ключей данные в реестре;  RegSaveKey сохраняет информацию реестра в файле;  RegUnloadKey выполняет операцию, обратную той, которую выполняет функция RegLoadKey. Функции, связанные со значениями Существуют следующие API­функции для работы со значениями:  RegDeleteValue удаляет значение (имя и данные) из реестра;  RegEnumValue перечисляет значения открытого ключа реестра;  RegQueryValueEx извлекает данные значения для заданного имени значе­ ния ключа;  RegQueryMultipleValue извлекает данные нескольких значений задан­ ного ключа;  RegSetValueEx устанавливает значение для заданного ключа реестра. Примеры Рассмотрим несколько примеров с использованием API­функций реестра. Но прежде чем выполнять их, вы должны создать архивную копию реестра. Это очень важно. Обратите внимание, что некоторые из функций реестра ссылаются на имя класса (class name) ключа. Согласно документации, имя класса представляет собой указатель на завершающуюся нулем строку, которая определяет класс (тип объекта) данного ключа. Этот параметр игнорируется, если ключ уже существует. Если в текущий момент определения классов отсутствуют, приложения должны передавать нулевую (null) строку. Windows 95 и Windows 98 используют имя класса только для ключей удаленного (remote) реестра, для ключей локального реестра оно игнорируется. Windows NT поддерживает данный параметр как для ключей удаленного, так и локального реестров. Из этого утверждения следует, что вам нужно игнорировать имена классов. Создание и удаление ключа реестра Для создания ключа можно использовать функцию RegCreateKeyEx, кото­ рая декларируется следующим образом: Declare Function RegCreateKeyEx Lib "advapi32.dll" Alias "RegCreateKeyExA" ( _ ByVal hKey As Long, _ ByVal lpSubKey As String, _ ByVal Reserved As Long, _
41 ByVal lpClass As String, _ ByVal dwOptions As Long, _ ByVal samDesired As Long, _ ByVal lpSecurityAttributes As Long, _ phkResult As Long, _ lpdwDisposition As Long) As Long Описание каждого из параметров вы можете отыскать в соответствующей документации. Следующая процедура AddKey создает ключ: HKEY_LOCAL_MACHINE\SOFTWARE\Roman Press Inc\Test и добавляет значение с именем «Font» и строку данных «Arial». Обратите вни­ мание на функцию FormatMessage, использующуюся для возвращения текста ошибки. (Она уже много раз встречалась вам ранее в этой книге.) Полный исход­ ный код находится в проекте rpiShell в архиве примеров. Sub AddKey() Dim hKey As Long, lDisp As Long Dim lResp As Long ' Создаем ключ. lResp = RegCreateKeyEx( _ HKEY_LOCAL_MACHINE, _ "SOFTWARE\Roman Press Inc\Test", _ 0,_ vbNullString, _ REG_OPTION_NON_VOLATILE, _ KEY_ALL _ACCESS, _ 0,_ hKey, _ lDisp) If lResp <> ERROR_SUCCESS Then MsgBox GetAPIErrorText(lResp) ' Устанавливаем значение. lResp = RegSetStringValueEx(hKey, "Font", 0, REG_SZ, "Arial", 6) If lResp <> ERROR_SUCCESS Then MsgBox GetAPIErrorText(lResp) ' Закрываем ключ. RegCloseKey hKey End Sub Процедура RemoveKey удаляет ключ. Обратите внимание, что в отличие от процесса создания процесс удаления состоит из двух шагов, так как нужно удалить два ключа. Sub RemoveKey() Dim lResp As Long Реестр Windows
42 Реестр и INI-файлы lResp = RegDeleteKey(HKEY_LOCAL_MACHINE, "SOFTWARE\Roman Press Inc\Test") If lResp <> ERROR_SUCCESS Then MsgBox GetAPIErrorText(lResp) lResp = RegDeleteKey(HKEY_LOCAL_MACHINE, "SOFTWARE\Roman Press Inc") If lResp <> ERROR_SUCCESS Then MsgBox GetAPIErrorText(lResp) End Sub Перечисление ключей и значений реестра Процедура ListDPs, приведенная ниже, демонстрирует перечисление ключей. Ее основная задача – составление списка с информацией обо всех поставщиках данных OLE DB на имеющемся компьютере. На рис. П3.2 показан такой элемент данных реестра. Корневой ключ поставщика является подключом ключа HKEY_ CLASSES_ROOT\CLSID. Для идентификации поставщика данных предназначен подключ с именем «OLE DB Provider» (поставщик OLE DB). Более подробно об OLE DB и ADO читайте в моей книге Access Database Design and Programming, второе издание которой опубликовано издательством O’Reilly. Рис. П3.2 . Элемент реестра для поставщика OLE DB Исходный код процедуры ListDPs находится в проекте rpiShell. Private Sub ListDPs() ' Ищем в реестре данные о поставщиках. Const BUF_LEN As Long = 2048 Dim lret As Long Dim hCLSIDKey As Long Dim hClassKey As Long Dim hClassSubKey As Long Dim bufKeyName As String * BUF_LEN Dim bufKeyName2 As String * BUF_LEN
43 Dim lbufValue As Long Dim bufValue As String * BUF_LEN Dim lValueType As Long Dim ft As FILETIME Dim lxKey As Long, lxKey2 As Long Dim bProvider As Boolean ' Открываем ключ CLSID в режиме только для чтения. lret = RegOpenKeyEx(HKEY_CLASSES_ROOT, "CLSID", 0, KEY_READ, _ hCLSIDKey) If lret <> ERROR_SUCCESS Then MsgBox "Cannot open CLSID key", vbCritical Exit Sub End If lxKey = 1 ' Индекс ключа. Do DoEvents ' Следующий индекс подключа. lxKey = lxKey + 1 ' Устанавливаем буфер для названия ключа. bufKeyName = String(BUF_LEN, 0) ' Получаем следующий подключ CLSID. lret = RegEnumKeyEx(hCLSIDKey, lxKey, bufKeyName, BUF_LEN, _ 0, vbNullString, 0, ft) If lret <> ERROR_SUCCESS Then Exit Do '  ' Отыскиваем среди полученных подключей CLSIDключ "OLE DB Provider". ' Если он найден, распечатываем все его подключи. '  ' Open the key lret = RegOpenKeyEx(HKEY_CLASSES_ROOT, "CLSID\" & Trim0(bufKeyName),_ 0, KEY_READ, hClassKey) If lret <> ERROR_SUCCESS Then MsgBox "Невозможно открыть ключ " & Trim0(bufKeyName) RegCloseKey hCLSIDKey Exit Sub End If ' Перечисляем подключи, разыскивая "OLE DB Provider". bProvider = False Реестр Windows
44 Реестр и INI-файлы lxKey2 = 0 Do ' Устанавливаем буфер. bufKeyName2 = String(BUF_LEN, 0) ' Перечисляем подключи. lret = RegEnumKeyEx(hClassKey, lxKey2, bufKeyName2, _ BUF_LEN, 0, vbNullString, 0, ft) If lret = ERROR_SUCCESS Then ' Проверяем наличие ключа "OLE DB Provider". If LCase$(Trim0(bufKeyName2)) = "ole db provider" Then bProvider = True Exit Do End If End If lxKey2 = lxKey2 + 1 Loop While lret = ERROR_SUCCESS '  ' Если поставщик найден, печатаем все ключи. '  If bProvider Then Debug.Print "" Debug.Print "***Найден поставщик OLE DB***" Debug.Print "CLSID = " & Trim0(bufKeyName) lxKey2 = 0 Do lbufValue = 0 bufValue = String(BUF_LEN, 0) bufKeyName2 = String(BUF_LEN, 0) lret = RegEnumKeyEx(hClassKey, lxKey2, bufKeyName2, _ BUF_LEN, 0, vbNullString, 0, ft) If lret <> ERROR_SUCCESS Then Exit Do ' Открываем ключ. lret = RegOpenKeyEx(HKEY_CLASSES_ROOT, _ "CLSID\" & Trim0(bufKeyName) & "\" & Trim0(bufKeyName2), _ 0, KEY_QUERY_VALUE, hClassSubKey) If lret = ERROR_SUCCESS Then ' Получаем значение по умолчанию.
45 ' Сначала получаем размер данных и тип значения. lret = RegQueryValueEx(hClassSubKey, vbNullString, 0&, _ lValueType, 0&, lbufValue) ' Данные – это строка? If lValueType = REG_SZ Then ' Получаем данные строки. lret = RegQueryValueExStr(hClassSubKey, vbNullString, 0&,_ lValueType, bufValue, lbufValue) ' Если это не ExtendedErrors, то распечатываем. If Trim0(bufKeyName2) <> "ExtendedErrors" Then Debug.Print Trim0(bufKeyName2) & " = " & _ Trim0(bufValue) End If End If RegCloseKey hClassSubKey End If lxKey2 = lxKey2 + 1 Loop End If Loop RegCloseKey hCLSIDKey End Sub Вывод этой процедуры на моем компьютере выглядит так: ***Найден поставщик OLE DB*** CLSID = {0C7FF16C38E311d097AB00C04FC2AD98} InprocServer32 = C:\Program Files\Common Files\system\ole db\ SQLOLEDB.DLL OLE DB Provider = Microsoft OLE DB Provider for SQL Server ProgID = SQLOLEDB.1 VersionIndependentProgID = SQLOLEDB ***Найден поставщик OLE DB*** CLSID = {3449A1C8C56C11D0AD7200C04FC29863} InprocServer32 = C:\Program Files\Common Files\system\msadc\ MSADDS.DLL OLE DB Provider = MSDataShape ProgID = MSDataShape.1 VersionIndependentProgID = MSDataShape Реестр Windows
4 Реестр и INI-файлы ***Найден поставщик OLE DB*** CLSID = {c8b522cb5cf311ceade500aa0044773d} InprocServer32 = C:\Program Files\Common Files\System\OLE DB\MSDASQL.DLL OLE DB Provider = Microsoft OLE DB Provider for ODBC Drivers ProgID = MSDASQL.1 VersionIndependentProgID = MSDASQL ***Найден поставщик OLE DB*** CLSID = {dee35060506b11cfb1aa00aa00b8de95} InprocServer32 = C:\Program Files\Common Files\system\ole db\MSJTOR35.DLL OLE DB Provider = Microsoft Jet 3.51 OLE DB Provider ProgID = Microsoft.Jet.OLEDB.3 .51 VersionIndependentProgID = Microsoft.Jet.OLEDB ***Найден поставщик OLE DB*** CLSID = {dfc8bdc0e37811d09b300080c7e9fe95} InprocServer32 = C:\Program Files\Common Files\system\ole db\MSDAOSP. DLL OLE DB Provider = Microsoft OLE DB Simple Provider ProgID = MSDAOSP.1 VersionIndependentProgID = MSDAOSP ***Найден поставщик OLE DB*** CLSID = {e8cc4cbefdff11d0b86500a0c9081c1d} InprocServer32 = C:\Program Files\Common Files\system\ole db\MSDAORA. DLL OLE DB Provider = Microsoft OLE DB Provider for Oracle ProgID = MSDAORA.1 VersionIndependentProgID = MSDAORA ***Найден поставщик OLE DB*** CLSID = {E8CCCB797C36101BAC3A00AA0044773D} InprocServer32 = C:\oledbsdk\bin\SAMPPROV.DLL OLE DB Provider = Microsoft OLE DB Sample Provider ProgID = SampProv VersionIndependentProgID = SampProv Индивидуальные инициализационные файлы Ранее уже говорилось, что для хранения постоянных данных приложений предпочтительнее использовать INI­файлы. Поэтому давайте кратко рассмот­ рим некоторые из API­функций, которые применяются для работы с этими файлами. API-функции индивидуальных инициализационных файлов Индивидуальный инициализационный файл (private profile) часто называется INI­файлом, поскольку в большинстве случаев он имеет расширение .ini. Он явля­ ется обычным текстовым файлом с форматом, заданным следующим образом:
4 ; Комментарии – это строки, начинающиеся с точки с запятой. [section1] key1=value1 key2=value2 . . . [section2] key1=value1 key2=value2 . . . У INI­файла есть разделы, которые помечены именем раздела в квадратных скобках. В каждом разделе присутствует список парных значений key=value. Win32 API поддерживает несколько функций для работы с индивидуальными INI­файлами:  GetPrivateProfileInt извлекает целочисленное значение из пары ключ/ значение: Declare Function GetPrivateProfileInt Lib "kernel32" _ Alias "GetPrivateProfileIntA" ( _ ByVal lpApplicationName As String, _ ByVal lpKeyName As String, _ ByVal nDefault As Long, _ ByVal lpFileName As String _ ) As Long  GetPrivateProfileSection извлекает весь раздел в виде массива пар ключ/значение: Declare Function GetPrivateProfileSection Lib "kernel32" _ Alias "GetPrivateProfileSectionA" ( _ ByVal lpAppName As String, _ ByVal lpReturnedString As String, _ ByVal nSize As Long, _ ByVal lpFileName As String _ ) As Long  GetPrivateProfileString извлекает строковое значение из пары ключ/ значение: Declare Function GetPrivateProfileString Lib "kernel32" _ Alias "GetPrivateProfileStringA" ( _ ByVal lpApplicationName As String, _ ByVal lpKeyName As String, _ ByVal lpDefault As String, _ ByVal lpReturnedString As String, _ ByVal nSize As Long, _ ByVal lpFileName As String _ ) As Long Индивидуальные инициализационные файлы
4 Реестр и INI-файлы  WritePrivateProfileSection записывает весь раздел: Declare Function WritePrivateProfileSection Lib "kernel32" _ Alias "WritePrivateProfileSectionA" ( _ ByVal lpAppName As String, _ ByVal lpString As String, _ ByVal lpFileName As String _ ) As Long  WritePrivateProfileString записывает строковое значение в пару ключ/значение: Declare Function WritePrivateProfileString Lib "kernel32" _ Alias "WritePrivateProfileStringA" ( _ ByVal lpApplicationName As String, _ ByVal lpKeyName As String, _ ByVal lpString As String, _ ByVal lpFileName As String _ ) As Long Так как я часто использую эти функции, я написал несколько простых функций­контейнеров, которые, на мой взгляд, легче применять: Function INIDeleteSection(SectionName As String, IniFile As String) As Long ' Записывает в раздел с именем null. INIDeleteSection = _ WritePrivateProfileString(SectionName, vbNullString, "", IniFile) End Function '  Function INIDeleteKey(SectionName As String, KeyName As String, _ IniFile) As Boolean ' Записывает в ключ null. INIDeleteKey = _ WritePrivateProfileString(SectionName, KeyName, vbNullString, _ IniFile) End Function '  Function INIEnumKeys(SectionName As String, BufferLen As Integer, _ IniFile As String) As Variant ' Перечисляет все ключи раздела. ' Возвращаемые значения: разделяемые значением null строки или код ошибки. ' Может проверять ошибки в операторах типа If IsError(... . ' ИСПОЛЬЗУЕТ БОЛЬШОЙ БУФЕР! Dim sBuf As String, lReturn As Long sBuf = String$(BufferLen, 0)
4 lReturn = GetPrivateProfileString(SectionName, vbNullString, "", sBuf, _ BufferLen, IniFile) If lReturn = BufferLen  2 Then INIEnumKeys = CVErr(1) Else INIEnumKeys = Left$(sBuf, lReturn) End If End Function '  Function INIEnumSections(BufferLen As Integer, IniFile As String) As _ Variant ' Перечисляет все разделы. ' Возвращаемые значения: разделяемые значением null имена разделов ' или код ошибки. ' Может проверять ошибки в операторах типа If IsError(... . ' ИСПОЛЬЗУЕТ БОЛЬШОЙ БУФЕР! Dim sBuf As String, lReturn As Long sBuf = String$(BufferLen, 0) lReturn = GetPrivateProfileString(vbNullString, vbNullString, "", sBuf, _ BufferLen, IniFile) If lReturn = BufferLen  2 Then INIEnumSections = CVErr(1) Else INIEnumSections = Left$(sBuf, lReturn) End If End Function '  Function INIGetSection(SectionName As String, BufferLen As Integer, _ IniFile As String) As Variant ' Перечисляет все ключи/значения раздела. ' Возвращение значения: разделяемые значением null ключи/значения ' или код ошибки. ' Может проверять ошибки в операторах типа If IsError(... . ' ИСПОЛЬЗУЕТ БОЛЬШОЙ БУФЕР! Dim sBuf As String, lReturn As Long sBuf = String$(BufferLen, 0) Индивидуальные инициализационные файлы
40 Реестр и INI-файлы lReturn = GetPrivateProfileSection(SectionName, sBuf, BufferLen, IniFile) If lReturn = BufferLen  2 Then INIGetSection = CVErr(1) Else INIGetSection = Left$(sBuf, lReturn) End If End Function '  Function INIWriteSection(SectionName As String, StringToWrite As String, _ IniFile As String) As Long ' Записывает раздел. ' Требует, чтобы разделы завершались 0, с последним 0 в конце строки. ' При нормальном завершении возвращает ненулевое значение. INIWriteSection = WritePrivateProfileSection(SectionName, _ StringToWrite, IniFile) End Function '  Function INIGetBoolean(SectionName As String, KeyName As String, _ Default As Boolean, IniFile As String) As Boolean ' Если значение ключа 0 или false, возвращает False. ' Если значение ключа 1, 1 или true, возвращает True. ' Иначе возвращает Default. Dim lReturn As Long, s As String, sBuf As String sBuf = String$(50, 0) lReturn = GetPrivateProfileString(SectionName, KeyName, CStr(Default), _ sBuf, 24, IniFile) s = LCase$(Left$(sBuf, lReturn)) Ifs="0"Ors="false"Then INIGetBoolean = False ElseIfs="1"Ors="1"Ors="true"Then INIGetBoolean = True Else INIGetBoolean = Default End If End Function ' 
41 Function INIGetInt(SectionName As String, KeyName As String, _ Default As Integer, IniFile As String) As Integer INIGetInt = GetPrivateProfileInt(SectionName, KeyName, Default, _ IniFile) End Function '  Function INIGetLong(SectionName As String, KeyName As String, _ Default As Long, IniFile As String) As Long INIGetLong = GetPrivateProfileInt(SectionName, KeyName, Default, _ IniFile) End Function '  Function INIGetString(SectionName As String, KeyName As String, _ BufferLen As Integer, Default As String, IniFile As String) As _ Variant ' Возвращаемые значения: строка или код ошибки. ' Может проверять ошибки в операторах типа: If IsError(... . Dim sBuf As String, lReturn As Long sBuf = String$(BufferLen, 0) lReturn = GetPrivateProfileString(SectionName, KeyName, _ Default, sBuf, BufferLen, IniFile) If lReturn = BufferLen  1 Then INIGetString = CVErr(1) Else INIGetString = Left$(sBuf, lReturn) End If End Function '  Function INIWriteString(SectionName As String, KeyName As String, _ StringToWrite As String, IniFile As String) As Integer INIWriteString = _ WritePrivateProfileString(SectionName, KeyName, StringToWrite, _ IniFile) End Function Индивидуальные инициализационные файлы
Предметный указатель А Адрес виртуальный 221 дескриптор 234 физический 221 Адресное пространство 224 виртуальное 224 Алгоритм отображения шрифтов 427 Альфаканал 361 Аргумент 34 Б Библиотека динамически подключаемая 34 Бит достоверности 237 присутствия 237 В Виртуальное пространство 409 Внедрение DLL 344 Г Гарнитура шрифта 425 Главное окно приложения 272 Глиф 426 Гранулярность распределения 233 Граф отслеживаемых потоков 345 Д Дескриптор 162 модуля 173 псевдодескриптор 182 строки 96 Диспетчер виртуальной памяти 221 Дюйм монитора 404 З Запись копированием 233 Засечка 425 Захват мыши 306 И Имя альтернативное 44 Инициализационный файл 457 Интефейс прикладного программирования 25 Исключение 147 обработчик 147 Исключительная ситуация 147 Исполнительная система 160 К Квант времени 205 Кисть 385 логическая 386 начало координат 386 сплошная 387 стандартная 387 физическая 386 шаблонная 387 штриховая 387 Класс окна 290, 312 Когерентность 170 Код дополнительный 84 обратный 86 прямой 84 символьный 32 ANSI 32
43 Предметный указатель ASCII 32 ASCII расширенный 32 DBCS 33 Unicode 33 Кодовая страница 32, 437 Кольцо 156 Контекст переключение 206 Контекст устройства 373, 377 атрибуты 377 дисплея 401 информационный 390 памяти 392 принтера 393 режим 380 совместимый 392 Координаты клиентские 286 оконные 286 экранные 286 Критическая секция 208 объект 209 Куча по умолчанию 240 Л Ловушка 328 Логический дюйм 404 Логический номер экземпляра 173 Логический шрифт 427 Локальное состояние ввода 306 М Маршаллинг 303 Массив символов ANSI 95 Unicode 95 Меню системное 273 Многозадачность 204 кооперативная 205 Многопоточность 205 Многопроцессорная обработка 205 Модификация класса окна 324 Мультипрограммирование. См. Многозадачность Н Надкласс 324 Надстройка класса 324 Наклон шрифта 425 О Область 374 Область вывода начало координат 410 Оболочка Windows 446 Объединение 230 Объект автоматизации 35 критическая секция 209 состояние 209 занят 209 свободен 209 Объект ядра 162 Ограничивающий прямоугольник 286 Окно владелец 276 всплывающее 274 высокоуровневое 308 диалоговое 272 клиентская область 272 неклиентская область 272 область отсечения 275 окнопотомок 274 перекрывающееся 274 подчиненное 276 рабочего стола 272 сообщения 272 Оконная процедура 290, 314 Оконная функция. См. Оконная процедура Оператор взятия адреса 41 раскрытия ссылки 41 Отображающая функция 405, 407 Ошибка нарушения доступа 227
44 Программирование в Win32 API на Visual Basic П Память виртуальная 221 виртуальная страница 221 страница 221 страничный блок 221 страничный файл 221 файл подкачки 221 физическая 221 физическая страница 221 Параметр 34 IN 34 OUT 34 Переключение задач 205 Перо 384 геометрическое 385 косметическое 384 Пиксел 359 Плотность шрифта 425 Подсистема среды 159 Подсчет используемости 163 Порядок байтов прямой 52 Поток 154 idle 206 контекст потока 171 нулевой страницы 206 приоритетный 307 системный 159 Поток необработанного ввода 292 Представление беззнаковое 81 знаковое 81 Приложение 154 командной строки 272 с графическим интерфейсом 272 Приоритет базовый 207 динамический 207 класс 206 реального времени 207 уровень 205 Прозрачность пиксела 361 Пространство логическое 405, 407 мировое (внешнее) 405, 407 страницы 405, 407 устройства 405, 407 Пространство процесса. См. Адресное пространство Протяженность 412 Процедура ловушки 331 Процесс 154 системный 157 Прямоугольник окна 286 Путь 388 Р Рабочий набор 238 системный 239 Разряд знаковый 60, 84 Растр 359 Растровое изображение 359 Реентерабельность 295 Реестр данные значения ключа 458 значение ключа 458 имя значения ключа 458 ключ 457 улей 459 Режим отображения 419 пользовательский 156 ядра 156 С Связывание динамическое 37 статическое 37 Семафор 218 Сервер ActiveX 35 автоматизации 35 Сервис 157 Синхронизация потоков 208
45 Предметный указатель Событие 215 с автоматическим сбросом 216 с ручным сбросом 216 Сообщение 289 идентификатор 289 параметры 289 уведомляющее 298 Состояние визуализации 283 Стиль 425 Стиль окна 316 Страница защищенная 233 каталог системных страниц 236 каталог страниц 235 подкачка 235 таблица системных страниц 236 таблица страниц 235 физическая попадание 235 Строка 95 Строка развертки 360 Суперкласс 324 Т Таблица импорта 36 экспорта 35 Таблица вершин 345 Таблица цветности 361 Тип данных 59 BSTR 95 логический 62 основной 61 отсылочный 59 производный 61 размерность 59 универсальный 62 Точка входа 45 У Указатель 39 null 102 Управляющий элемент 272 Уровень абстрагирования от аппаратуры (HAL) 161 Уровень привилегий 156 Устройство физическое 377 целевое 377 Утечка памяти 120 Ф Файл DLL 35 EXE 35 PE 35 исполняемый 35, 248 образа задачи 35, 248 объектный 248 общий формат (COFF) 248 отображаемый в память 164 совместно используемый 223 Файл растрового изображения 359 Файл шрифта 426 Файл шрифтового ресурса 426 Физический дюйм 404 Физический шрифт 427 Флаг 78 Фокус ввода 306 с клавиатуры 306 Функция внешняя 43 обратного вызова 279 перечисления 278 Ц Цвета основные 361 Цветовая модель 361 RGB 361 CMYK 361 Цветовая палитра 361 Цепочка ловушек 332 Ш Шрифт 425 Шрифт GDI 427 Шрифт устройства 427
4 Программирование в Win32 API на Visual Basic Э Элемент изображения 359 Я Ядро 160 A API 25 P PEфайл 248 заголовок 251 заголовок раздела 258 подпись 252 таблица разделов 258 формат 250 POSIX 159 Z Zкоордината 276
Стивен Роман Программирование в Win32 API на Visual Basic Главный редактор Мовчан Д. А. Перевод с английского Караваев А. П . Научный редактор Уткин М. А. Выпускающий редактор Морозова Н. В. Технический редактор Александрова О. С . Верстка Сучкова Н. А. Графика Шаклунов А. К . Дизайн обложки Панкусова Е. Н. Гарнитура «Петербург». Печать офсетная. Усл. печ. л. 30. Тираж 3000. Зак. No Издательство «ДМК Пресс», 105023, Москва, пл. Журавлева, д. 2/8. Электронные адреса: www.dmkpress.ru, info@dmk.ru. Отпечатано в ГУП «Чеховский полиграфический комбинат». 142300, г. Чехов, ул. Полиграфистов, 1.