Благодарности
Введение
Как организована эта книга
Как читать эту книгу
Что находится на компакт-диске
Что дальше?
От издательства
Глава 1. Основные принципы и понятия
Hello World, версия 2: вывод текста на рабочем столе
Hello, World, версия 3: создание полноэкранного окна
Hello, World, версия 4: вывод средствами DirectDraw
Ассемблер
Среда программирования
Компиляторы
Microsoft Platform SDK
Microsoft Driver Development Kit
Microsoft Developer Network
Формат исполняемых файлов Win32
Каталог экспорта
Архитектура операционной системы Microsoft Windows
Микроядро
Драйверы устройств
Управление окнами и графическая система
Исполнительная часть
Системные функции
Системные процессы
Службы
Платформенные подсистемы
Итоги
Глава 2. Архитектура графической системы Windows
Video for Windows
Still Image
OpenGL
Windows Media
Компоненты режима ядра
Драйверы режима ядра
Архитектура GDI
Группы функций GDI
Вызовы системных функций GDI
От Win32 GDI API к системным функциям механизма GDI
Архитектура DirectX
Архитектура DirectDraw
Архитектура системы печати
Служба спулера
Маршрутизатор спулера
Провайдер печати
Процессор печати
Языковой монитор и монитор порта
Процесс спулера изнутри
Графический механизм
Механизм графической визуализации
Структуры данных графического механизма
Преобразование в примитивы
Шрифтовые драйверы
Драйверы экрана
Назначение драйвера экрана
Инициализация драйвера экрана
Вывод на поверхность, перехват и возврат
Дополнительные возможности драйвера
Поддержка DirectDraw/Direct3D на уровне драйвера экрана
Драйверы принтеров
Графическая библиотека DLL драйвера принтера
Драйвер принтера для вывода документа HTML
Итоги
Глава 3. Внутренние структуры данных GDI/DirectDraw
Инкапсуляция и маскировка реализации
Указатели и манипуляторы
Тождественное отображение
Табличное отображение
Когда манипулятора недостаточно
Расшифровка манипуляторов объектов GDI
HGDIOBJ не является указателем
Максимальное количество манипуляторов GDI на уровне процесса —12 000
Максимальное количество манипуляторов GDI на уровне системы —16 384
Часть HGDIOBJ содержит индекс
Часть HGDIOBJ содержит тип объекта GDI
Поиск таблицы объектов GDI
Расшифровка таблицы объектов GDI
Поле nCount иногда используется как счетчик выбора объектов
Поле nProcess связывает манипулятор GDI с конкретным процессом
nUpper: дополнительная проверка
пТуре: внутренний тип объекта
pUser: указатель на структуру данных пользовательского режима
Структуры данных пользовательского режима
Структура данных пользовательского режима для регионов: оптимизация прямоугольных регионов
Структура данных пользовательского режима для шрифтов: таблица значений ширины
Структура данных пользовательского режима для контекста устройства: атрибуты
Обращение к адресному пространству режима ядра
WinDbg и расширение отладчика GDI
Структуры данных режима ядра
Типы объектов GDI в механизме GDI
Контекст устройства в механизме GDI
Структура PDEV в механизме GDI
Поверхности в механизме GDI
Аппаратно-зависимые растры в механизме GDI
DIB-секции в механизме GDI
Кисти в механизме GDI
Перья в механизме GDI
Палитры в механизме GDI
Регионы в механизме GDI
Траектории в механизме GDI
Шрифты в механизме GDI
Другие объекты GDI в механизме GDI
Структуры данных DirectDraw
Итоги
Глава 4. Мониторинг графической системы Windows
Внедрение DLL-разведчика
Подключение к цепочке вызовов функций API
Сбор информации
Вывод данных
Управляющая программа
Отслеживание вызовов Win32 GDI
Декодер данных GDI
Полный мониторинг API
Отслеживание СОМ-интерфейсов DirectDraw
Определение DirectDraw API
Модификация таблицы виртуальных функций
Отслеживание системных вызовов GDI
Отслеживание интерфейса DDI
Итоги
Глава 5. Абстракция графического устройства
Формат пикселов
Двойная буферизация, z-буфер и текстуры
Аппаратное ускорение
Экранное устройство и перечисление режимов
Контекст устройства
Получение информации о возможностях устройства
Атрибуты в контексте устройства
Связь контекста устройства с окном
Графический вывод в многооконной среде
Получение контекста устройства, связанного с окном
Общий контекст устройства
Классовый контекст устройства
Закрытый контекст устройства
Родительский контекст устройства
Прочие контексты устройств
Информационный контекст устройства
Совместимый контекст устройства
Метафайловый контекст устройства
Формальное представление контекста устройства
Пример: родовой класс рамочного окна
Класс строки состояния
Класс холста
Класс рамочного окна
Тестовая программа
Пример программы: графический вывод в контексте устройства
Сообщение WM_PAINT
Наглядное представление сообщений перерисовки окна
Итоги
Глава 6. Системы координат и преобразования
Система координат устройства
Страничная система координат и режимы отображения
Режимы отображения MM_LOENGLISH и MM_HIENGLISH
Режимы отображения MM_LOMETRIC и MM_HIMETRIC
Режим отображения MM_TWIPS
Режим отображения MM_ISOTROPIC
Режим отображения MM_ANISOTROPIC
Базовые точки окна и области просмотра
Другие функции окна и области просмотра
Мировая система координат
Функции мировых преобразований в Win32 API
Использование мировых преобразований
Использование систем координат
Пример программы: прокрутка и масштабирование
Итоги
Глава 7. Пикселы
Таблица объектов GDI
Манипулятор объекта GDI
API объектов GDI
Обнаружение утечки объектов GDI
Отсечение
Простые регионы
Регион отсечения
Метарегион
Пять регионов контекста устройства
Наглядное представление регионов в контексте устройства
Цвет
Цветовое пространство HLS
Индексируемые цвета и палитры
Нетривиальные возможности
Вывод пикселов
Пример: множество Мандельброта
Итоги
Глава 8. Линии и кривые
Режим заполнения фона и цвет фона
Перья
Стандартные перья
Простые перья
Расширенные перья
Получение информации о логических перьях
Класс для работы с объектами перьев GDI
Линии
Кривые Безье
Альтернативное определение кривых Безье
Дуги
Рисование дуг пером со стилем PSJNSIDEFRAME
Преобразование дуг в кривые Безье
Траектории
Получение информации о траектории
Преобразование объекта траектории
Графические операции с использованием траекторий
Преобразование пути в регион
Пример: рисование нестандартных стилевых линий
Итоги
Глава 9. Замкнутые области
Стандартные кисти
Пользовательские кисти
Кисти системных цветов
Структура LOGBRUSH
Прямоугольники
Рисование прямоугольников
Прорисовка границ и элементов управления
Эллипсы, секторы, сегменты и закругленные прямоугольники
Многоугольники
Замкнутые траектории
Регионы
Операции с объектами регионов
Прорисовка регионов
Градиентные заливки
Применение градиентных заливок для создания объемных кнопок
Практическое использование заливок
Реализация градиентных заливок в цветовом пространстве HLS
Радиальные градиентные заливки
Текстурные и растровые заливки
Узорные заливки
Итоги
Глава 10. Основные сведения о растрах
Упакованный аппаратно-независимый растр
Разделенный аппаратно-независимый растр
Класс для работы с DIB
Отображение DIB в контексте устройства
Исходный прямоугольник
Приемный прямоугольник и режимы масштабирования
Преобразование цветового формата
Растровая операция
Пример использования функции StretchDIBits
SetDIBitsToDevice
Совместимые контексты устройств
Аппаратно-зависимые растры
CreateBitmapIndirect
GetObject и DDB
CreateCompatibleBitmap и CreateDiscardableBitmap
CreateDIBitmap
LoadBitmap
Копирование растров между форматами DIB и DDB
Прямой доступ к массиву пикселов DDB
Использование DDB-растров
Использование растров в меню
Использование растра в качестве фона окна
CreateDIBSection
Класс для работы с DIB-секциями
Функции GetObjectType и GetObject для DIB-секций
GetDIBColorTable и SetDIBColorTable
Применение DIB-секций: аппаратно-независимый вывод
Применение DIB-секций: вывод в высоком разрешении
Итоги
Глава 11. Нетривиальное использование растров
Диаграмма тернарных растровых операций
Часто используемые растровые операции
Прозрачные растры
Кватернарные растровые операции: MaskBIt
Цветовые ключи: TransparentBIt
Прозрачность без маски
Прозрачный вывод с использованием отсечения
Предварительная подготовка изображений
Альфа-наложение
Постепенное проявление и исчезновение растров
Прозрачные окна
Альфа-канал: класс AirBrush
Имитация альфа-наложения
Итоги
Глава 12. Графические алгоритмы и растры Windows .
Аффинные преобразования растров
Быстрые специализированные преобразования растров
Преобразования цветов
Гамма-коррекция
Преобразование пикселов в растрах
Родовой класс цветоделения
Пример выделения каналов
Гистограмма
Пространственные фильтры
Выделение границ и рельеф
Морфологические фильтры
Итоги
Глава 13. Палитры
Получение системной палитры
Статические цвета
Логическая палитра
Полутоновая палитра
Создание специализированной палитры
Сообщения палитры
WM_PALETTEISCHANGING
WM_PALETTECHANGED
Тестовая программа
Палитра и растры
Аппаратно-независимые растры и палитры
Индекс палитры в цветовой таблице DIB
DIB-секции и палитра
Квантование цветов
Сокращение цветовой глубины растра
Итоги
Глава 14. Шрифты
Глифы
Шрифт
Семейство шрифтов и начертание
Растровые шрифты
Векторные шрифты
Шрифты TrueType
Заголовок шрифта
Максимальный профиль
Отображение символов в индексы глифов
Индексная таблица
Данные глифов
Инструкции глифа
Горизонтальные метрики
Кернинг
Метрики OS/2 и Windows
Другие таблицы
Коллекции TrueType
Установка и внедрение шрифтов
Установка открытых шрифтов
Установка закрытых шрифтов и шрифтов Multiple Master OpenType
Установка шрифтов из образа в памяти
Внедрение шрифтов
Системная таблица шрифтов
Итоги
Глава 15. Текст
Стандартные шрифты
Создание логических шрифтов
Подстановка шрифта
Система подстановки шрифтов PANOSE
Получение информации о логическом шрифте
Метрики шрифтов TrueType/OpenType
Структура LOGFONT и метрики шрифта
Точность шрифтовых метрик
Простой вывод текста
Вывод текста справа налево
Дополнительные интервалы
Ширина символа
Нетривиальный вывод текста
Кернинг
Расположение символов
Функция ExtTextOut
Uniscribe
Доступ к данным глифов
Форматирование текста
Простое абзацное форматирование
Аппаратно-независимое форматирование текста
Эффекты при выводе текста
Начертания
Геометрические эффекты
Работа с текстом в растровом формате
Текст как совокупность кривых
Текст как регион
Итоги
Глава 16. Метафайлы
Воспроизведение расширенного метафайла
Получение информации о расширенном метафайле
Передача расширенных метафайлов
Строение расширенных метафайлов
Классификация типов записей EMF
Расшифровка записей EMF
Простые объекты GDI в EMF
Растры в EMF
Регионы в EMF
Траектории в EMF
Палитры в EMF
Системы координат в EMF
Команды вывода в EMF
Аппаратная независимость EMF
Перечисление записей EMF
Замедленное воспроизведение EMF
Трассировка воспроизведения EMF
Динамическое изменение EMF
Построение производных метафайлов
EMF как средство программирования
Сохранение EMF-файла спулера
Итоги
Примеры программ
Глава 17. Печать
Язык управления принтером
Прямой вывод в порт
Печать с использованием спулера
Процессор печати EMF
Перечисление принтеров
Получение информации о принтере
Настройка драйвера принтера
Базовая печать средствами GDI
Создание контекста устройства принтера
Получение информации о контексте устройства принтера
Последовательность формирования заданий печати
Поддержка печати в программах
Имитация внешнего вида страницы
Одновременный вывод страниц
Печать нескольких страниц на одном листе
Родовой класс печати
Вывод в контексте устройства принтера
Текст
Растры
Печать графики в формате JPEG
Итоги
Примеры программ
Глава 18. DirectDraw и непосредственный режим Direct3D
СОМ-классы
Создание СОМ-объекта
HRESULT
DirectX и СОМ
Общие сведения о DirectDraw
Интерфейс IDirectDrawSurface7
Вывод на поверхности DirectDraw
Подбор цветов
Интерфейс IDirectDrawClipper
Простое окно DirectDraw
Построение графической библиотеки DirectDraw
Вывод линий
Заливка замкнутых областей
Отсечение
Внеэкранные поверхности
Поддержка прозрачности посредством цветовых ключей
Шрифт и текст
Спрайты
Непосредственный режим Direct3D
Изменение размеров окна
Двухэтапный вывод
Использование Direct3D в окне
Текстурные поверхности
Пример использования непосредственного режима Direct3D
Итоги
Алфавитный указатель
Текст
                    СЕРИЯ
>М>^
i*0*
ло^'
,0^°
д^°
лв«ллЛ
ЛОА*
в^°°
;;^^>-'
tgniTTEP*


WINDOWS GRAPHICS PROGRAMMING Win32 GDI and DirectDraw Feng Yuan Hewlett-Packard Company m invent www.hp.com/hpbooks Prentice Hall PTR Upper Saddle River, New Jersey 07458 www.phptr.com
Фень Юань Программирование графики для Windows МАСТЕР-КЛАСС Санкт-Петербург • Москва • Харьков • Минск 2002
Фень Юань Программирование графики для Windows Перевел с английского Е. Матвеев Главный редактор Е. Строганова Заведующий редакцией И. Корнеев Руководитель проекта А. Васильев Научный редактор Е. Матвеев Литературный редактор А. Жданов Художник Н. Биржаков Иллюстрации В. Шендерова Корректор В. Листова Верстка Ю. Сергиенко ББК 32.973-018.3 УДК 681.327.1 Юань Фень Ю12 Программирование графики для Windows (+CD). — СПб.: Питер, 2002. — 1072 с: ил. ISBN 5-318-00297-8 Книга посвящена графическому программированию для Windows с использованием Win32 GDI API. Кроме того, в ней приведены начальные сведения о DirectDraw и краткое введение в непосредственный режим Direct3D. Рассматриваются стандартные возможности, поддерживаемые на всех платформах Win32, 32-разрядные возможности, реализованные только в Windows NT/2000, и новейшие расширения GDI, появившиеся только в Windows 2000 и Windows 98. В книге приведено множество фрагментов кода, подходящих для практического применения. Помимо простейших тестовых и демонстрационных программ, вы найдете в ней множество функций, классов C++, драйверов, утилит и нетривиальных программ, вполне подходящих для использования в коммерческих проектах. На компакт-диске находятся полные исходные тексты, файлы рабочих областей Microsoft Visual C++, заранее откомпилированные двоичные файлы (в отладочных и окончательных версиях) и файлы в формате JPEG для глав, посвященных графическим алгоритмам. Original English language Edition Copyright © by Hewlett-Packard Company, 2001 © Перевод на русский язык, Е. Матвеев, 2002 © Издательский дом «Питер», 2002 Права на издание получены по соглашению с Prentice Hall, Inc. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственность за возможные ошибки, связанные с использованием книги. ISBN 5-318-00297-8 ISBN 0-13-086985-6 (англ.) ЗАО «Питер Бук». 196105, Санкт-Петербург, Благодатная ул., д. 67. Лицензия ИД № 01940 от 05.06.00. Налоговая льгота - общероссийский классификатор продукции ОК 005-93, том 2; 953000 - книги и брошюры. Подписано в печать 25.12.01. Формат 70x100/16. Усл. п. л. 86,43. Тираж 5000 экз. Заказ№2493. Отпечатано с диапозитивов в ФГУП «Печатный двор» им. А. М. Горького Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.
Краткое содержание Глава 1. Основные принципы и понятия зо Глава 2. Архитектура графической системы Windows .... 84 Глава 3. Внутренние структуры данных GDI/DirectDraw . . 143 Глава 4. Мониторинг графической системы Windows .... 240 Глава 5. Абстракция графического устройства 282 Глава 6. Системы координат и преобразования 340 Глава 7. Пикселы 380 Глава 8. Линии и кривые 422 Глава 9. Замкнутые области 479 Глава 10. Основные сведения о растрах 535 Глава 11. Нетривиальное использование растров 608 Глава 12. Графические алгоритмы и растры Windows . . . . 663 Глава 13. Палитры 697 Глава 14. Шрифты 744 Глава 15. Текст 801 Глава 16. Метафайлы 897 Глава 17. Печать 947 Глава 18. DirectDraw и непосредственный режим Direct3D 1000 Алфавитный указатель 1058
Содержание Благодарности 21 Введение 22 О чем эта книга 23 Как организована эта книга 24 Как читать эту книгу . 27 Что находится на компакт-диске 28 Что дальше? 29 От издательства 29 Глава 1. Основные принципы и понятия зо Основы программирования для Windows на C/C++ 31 Hello World, версия 1: запуск браузера 32 Hello World, версия 2: вывод текста на рабочем столе 34 Hello, World, версия 3: создание полноэкранного окна 35 Hello, World, версия 4: вывод средствами DirectDraw 42 Ассемблер 46 Среда программирования 50 Разработка и тестирование 50 Компиляторы 52 Microsoft Platform SDK 55 Microsoft Driver Development Kit 58 Microsoft Developer Network 59 Формат исполняемых файлов Win32 61 Каталог импорта 65 Каталог экспорта 69 Архитектура операционной системы Microsoft Windows 71 HAL 73 Микроядро 73 Драйверы устройств 74 Управление окнами и графическая система 76 Исполнительная часть 77 Системные функции 78 Системные процессы 79 Службы 81 Платформенные подсистемы 81 Итоги 82 Примеры программ 82
Содержание 7 Глава 2. Архитектура графической системы Windows 84 Компоненты графической системы Windows 84 Мультимедиа 87 Video for Windows 88 Still Image 89 OpenGL 89 Windows Media 91 Компоненты режима ядра 92 Драйверы режима ядра 92 Архитектура GDI 93 Функции, экспортируемые из GDI32.DLL 94 Группы функций GDI 94 Вызовы системных функций GDI 97 От Win32 GDI API к системным функциям механизма GDI 98 Архитектура DirectX 99 Компоненты DirectX 100 Архитектура DirectDraw 102 Архитектура системы печати 105 Клиент спулера Win32 108 Служба спулера 108 Маршрутизатор спулера 108 Провайдер печати 109 Процессор печати ПО Языковой монитор и монитор порта 112 Процесс спулера изнутри 113 Графический механизм 114 Системные функции графического механизма 116 Механизм графической визуализации 118 Структуры данных графического механизма 120 Преобразование в примитивы 121 Шрифтовые драйверы 122 Драйверы экрана 123 Драйвер видеопорта и мини-драйвер видеопорта 123 Назначение драйвера экрана 124 Инициализация драйвера экрана 124 Вывод на поверхность, перехват и возврат 125 Дополнительные возможности драйвера 127 Поддержка DirectDraw/Direct3D на уровне драйвера экрана 127 Драйверы принтеров 129 Управляющие драйверы принтеров от Microsoft 130
Содержание Графическая библиотека DLL драйвера принтера 131 Драйвер принтера для вывода документа HTML 134 Итоги 141 Примеры программ 142 Глава 3. Внутренние структуры данных GDI/DirectDraw ... из Манипуляторы и объектно-ориентированное программирование 144 Класс и объект 144 Инкапсуляция и маскировка реализации 145 Указатели и манипуляторы 148 Тождественное отображение 149 Табличное отображение 149 Когда манипулятора недостаточно 150 Расшифровка манипуляторов объектов GDI 151 Манипуляторы стандартных объектов —константы 153 HGDIOBJ не является указателем 153 Максимальное количество манипуляторов GDI на уровне процесса —12 000 153 Максимальное количество манипуляторов GDI на уровне системы —16 384 154 Часть HGDIOBJ содержит индекс 154 Часть HGDIOBJ содержит тип объекта GDI 154 Поиск таблицы объектов GDI 156 Расшифровка таблицы объектов GDI 162 Указатель pKernel ссылается на выгружаемый пул 166 Поле nCount иногда используется как счетчик выбора объектов 166 Поле nProcess связывает манипулятор GDI с конкретным процессом .... 167 nUpper: дополнительная проверка 168 пТуре: внутренний тип объекта 169 pUser: указатель на структуру данных пользовательского режима 169 Структуры данных пользовательского режима 170 Структура данных пользовательского режима для кистей: оптимизация создания однородных кистей 170 Структура данных пользовательского режима для регионов: оптимизация прямоугольных регионов 171 Структура данных пользовательского режима для шрифтов: таблица значений ширины 172 Структура данных пользовательского режима для контекста устройства: атрибуты 172 Обращение к адресному пространству режима ядра 177
Содержание 9 WinDbg и расширение отладчика GDI 183 Структуры данных режима ядра 195 Таблица объектов GDI в механизме GDI 196 Типы объектов GDI в механизме GDI 196 Контекст устройства в механизме GDI 198 Структура PDEV в механизме GDI 202 Поверхности в механизме GDI 207 Аппаратно-зависимые растры в механизме GDI 210 DIB-секции в механизме GDI 211 Кисти в механизме GDI 212 Перья в механизме GDI 214 Палитры в механизме GDI 214 Регионы в механизме GDI 216 Траектории в механизме GDI 220 Шрифты в механизме GDI 224 Другие объекты GDI в механизме GDI 231 Структуры данных DirectDraw 231 Итоги 238 Примеры программ 238 Глава 4. Мониторинг графической системы Windows 240 Отслеживание вызовов функций Win32 API 241 Построение программы мониторинга 242 Внедрение DLL-разведчика 243 Подключение к цепочке вызовов функций API 246 Сбор информации 248 Вывод данных 254 Управляющая программа 257 Отслеживание вызовов Win32 GDI 260 Файл определения GDI API 260 Декодер данных GDI 262 Полный мониторинг API 264 Отслеживание СОМ-интерфейсов DirectDraw 268 Таблица виртуальных функций 268 Определение DirectDraw API 269 Модификация таблицы виртуальных функций . 270 Отслеживание системных вызовов GDI 271 Отслеживание интерфейса DDI 275 Итоги 279 Примеры программ 280
10 Содержание Глава 5. Абстракция графического устройства 282 Современные видеоадаптеры 282 Кадровый буфер 283 Формат пикселов 286 Двойная буферизация, z-буфер и текстуры 290 Аппаратное ускорение 293 Экранное устройство и перечисление режимов 293 Контекст устройства 296 Создание контекста устройства 298 Получение информации о возможностях устройства 299 Атрибуты в контексте устройства 304 Связь контекста устройства с окном 307 Графический вывод в многооконной среде 307 Получение контекста устройства, связанного с окном 309 Общий контекст устройства 313 Классовый контекст устройства 313 Закрытый контекст устройства 314 Родительский контекст устройства 315 Прочие контексты устройств 315 Информационный контекст устройства 315 Совместимый контекст устройства 316 Метафайловый контекст устройства 316 Формальное представление контекста устройства 318 Пример: родовой класс рамочного окна 321 Класс панели инструментов 322 Класс строки состояния 323 Класс холста 323 Класс рамочного окна 324 Тестовая программа 326 Пример программы: графический вывод в контексте устройства 328 Обновляемый регион окна 328 Сообщение WMJ>AINT 329 Наглядное представление сообщений перерисовки окна 331 Итоги 339 Примеры программ 339 Глава 6. Системы координат и преобразования 340 Физическая система координат 341 Система координат устройства 343 Страничная система координат и режимы отображения 345 Режим отображения MMJTEXT 348
Содержание 11 Режимы отображения MM_L.OENGL.ISH и MM_HIENGLISH 348 Режимы отображения MMJ.OMETRIC и MMJHIMETRIC 350 Режим отображения MMJTWIPS 351 Режим отображения MM JSOTROPIC 351 Режим отображения MM_ANISOTROPIC 352 Базовые точки окна и области просмотра 355 Другие функции окна и области просмотра 357 Мировая система координат 357 Аффинные преобразования 358 Функции мировых преобразований в Win32 API 361 Использование мировых преобразований 363 Использование систем координат 370 Реализация преобразований в GDI 372 Пример программы: прокрутка и масштабирование 373 Игра го в классе KScrollCanvas 377 Итоги 378 Примеры программ 379 Глава 7. Пикселы 380 Объекты GDI, манипуляторы и таблица объектов 380 Хранение объектов GDI 382 Таблица объектов GDI 383 Манипулятор объекта GDI 384 API объектов GDI 385 Обнаружение утечки объектов GDI 387 Отсечение 390 Конвейер отсечения 390 Простые регионы 391 Регион отсечения 392 Метарегион 396 Пять регионов контекста устройства 398 Наглядное представление регионов в контексте устройства 398 Цвет 402 Цветовое пространство RGB 403 Цветовое пространство HLS 406 Индексируемые цвета и палитры 411 Нетривиальные возможности 415 Вывод пикселов 415 Пример: множество Мандельброта 418 Итоги 421 Примеры программ 421
12 Содержание Глава 8. Линии и кривые 422 Бинарные растровые операции 422 Режим заполнения фона и цвет фона 426 Перья 427 Объект логического пера 427 Стандартные перья 429 Простые перья 430 Расширенные перья 433 Получение информации о логических перьях 439 Класс для работы с объектами перьев GDI 440 Линии 442 Кривые Безье 447 PolyDraw 451 Альтернативное определение кривых Безье 453 Дуги 454 Определение дуги в градусах: функция AngleArc 455 Рисование дуг пером со стилем PSJNSIDEFRAME 456 Преобразование дуг в кривые Безье 457 Траектории 461 Построение траектории 461 Получение информации о траектории 463 Преобразование объекта траектории 467 Графические операции с использованием траекторий 471 Преобразование пути в регион 473 Пример: рисование нестандартных стилевых линий 473 Итоги 477 Пример программы 478 Глава 9. Замкнутые области 479 Кисти 479 Объект логической кисти 479 Стандартные кисти 480 Пользовательские кисти 481 Кисти системных цветов 488 Структура LOGBRUSH 489 Прямоугольники 490 Прямоугольник как структура данных 490 Рисование прямоугольников 492 Прорисовка границ и элементов управления 495 Эллипсы, секторы, сегменты и закругленные прямоугольники 497
13 Многоугольники 500 Режим заполнения многоугольников 501 Замкнутые траектории 504 Регионы 506 Создание объекта региона 507 Операции с объектами регионов 510 Прорисовка регионов 521 Градиентные заливки 523 Градиентная заливка прямоугольников 525 Применение градиентных заливок для создания объемных кнопок 527 Практическое использование заливок 528 Полупрозрачная заливка 528 Реализация градиентных заливок в цветовом пространстве HLS 529 Радиальные градиентные заливки 530 Текстурные и растровые заливки 532 Узорные заливки 532 Итоги 533 Пример программы 534 Глава 10. Основные сведения о растрах 535 Аппаратно-независимые растры 535 Файловый формат BMP 536 Упакованный аппаратно-независимый растр 545 Разделенный аппаратно-независимый растр 546 Класс для работы с DIB 546 Отображение DIB в контексте устройства 556 StretchDIBits 556 Исходный прямоугольник 556 Приемный прямоугольник и режимы масштабирования 557 Преобразование цветового формата 559 Растровая операция 559 Пример использования функции StretchDIBits 560 SetDIBitsToDevice 561 Совместимые контексты устройств 563 Аппаратно-зависимые растры 564 CreateBltmap 565 CreateBitmapIndirect 566 GetObjectnDDB 567 CreateCompatibleBitmap и CreateDiscardableBitmap 567 CreateDIBitmap 569 LoadBitmap 570
Содержание Копирование растров между форматами DIB и DDB 571 Прямой доступ к массиву пикселов DDB 575 Использование DDB-растров 576 Отображение DDB-растров 576 Использование растров в меню 584 Использование растра в качестве фона окна 589 CreateDIBSection 594 Класс для работы с DIB-секциями 596 Функции GetObjectType и GetObject для DIB-секций 598 GetDIBColorTable и SetDIBColorTable 599 Применение DIB-секций: аппаратно-независимый вывод 600 Применение DIB-секций: вывод в высоком разрешении 603 Итоги 607 Примеры программ 607 Глава 11. Нетривиальное использование растров 608 Тернарные растровые операции 608 Коды растровых операций 609 Диаграмма тернарных растровых операций 612 Часто используемые растровые операции 614 Прозрачные растры 627 Функция PlgBIt 628 Кватернарные растровые операции: MaskBIt 635 Цветовые ключи: TransparentBIt 640 Прозрачность без маски 644 Прозрачный вывод с использованием геометрических фигур 644 Прозрачный вывод с использованием отсечения 646 Предварительная подготовка изображений 647 Альфа-наложение 649 Пример альфа-наложения с постоянным коэффициентом 652 Постепенное проявление и исчезновение растров 653 Прозрачные окна 653 Альфа-канал: класс AirBrush 655 Имитация альфа-наложения 659 Итоги 661 Примеры программ 661 Глава 12. Графические алгоритмы и растры Windows .... 663 Прямой доступ к пикселам 664 Аффинные преобразования растров 667 Быстрые специализированные преобразования растров 670
Содержание 15 Преобразования цветов 672 Преобразование растров в оттенки серого 675 Гамма-коррекция 676 Преобразование пикселов в растрах 678 Родовой класс преобразований пикселов 678 Родовой класс цветоделения 682 Пример выделения каналов 684 Гистограмма 686 Пространственные фильтры 686 Фильтры сглаживания и резкости 691 Выделение границ и рельеф 692 Морфологические фильтры 693 Итоги 695 Примеры программ 696 Глава 13. Палитры 697 Системная палитра 697 Параметры экрана 698 Получение системной палитры 699 Статические цвета 702 Логическая палитра 704 Палитра по умолчанию 705 Полутоновая палитра 706 Создание специализированной палитры 708 Сообщения палитры 710 WM_QUERYNEWPALETTE 710 WM_PALETTEISCHANGING 711 WM_PALETTECHANGED 711 Тестовая программа 712 Палитра и растры 716 Аппаратно-зависимые растры и палитры 717 Аппаратно-независимые растры и палитры 720 Индекс палитры в цветовой таблице DIB 723 DIB-секции и палитра 725 Квантование цветов 726 Сокращение цветовой глубины растра 736 Итоги 742 Пример программы 743 Глава 14. Шрифты 744 Что такое шрифт? 745 Наборы символов и кодировки 745
16 Содержание Глифы 751 Шрифт 753 Семейство шрифтов и начертание 754 Растровые шрифты 758 Векторные шрифты 762 Шрифты TrueType 765 Формат файлов шрифтов TrueType 765 Заголовок шрифта 768 Максимальный профиль 769 Отображение символов в индексы глифов 770 Индексная таблица 772 Данные глифов 773 Инструкции глифа 781 Горизонтальные метрики 786 Кернинг 789 Метрики OS/2 и Windows 790 Другие таблицы 791 Коллекции TrueType 792 Установка и внедрение шрифтов 793 Ресурсные файлы шрифтов 793 Установка открытых шрифтов 794 Установка закрытых шрифтов и шрифтов Multiple Master ОрепТуре 794 Установка шрифтов из образа в памяти 795 Внедрение шрифтов 795 Системная таблица шрифтов 799 Итоги 800 Примеры программ 800 Глава 15. Текст 801 Логические шрифты 801 Метрики шрифтов в Windows 802 Стандартные шрифты 804 Создание логических шрифтов 805 Подстановка шрифта 810 Система подстановки шрифтов PANOSE 811 Получение информации о логическом шрифте 817 Метрики растровых и векторных шрифтов 819 Метрики шрифтов TrueType/OpenType 822 Структура LOGFONTj* метрики шрифта 827 Точность шрифтовых метрик 827
Содержание 17 Простой вывод текста 833 Выравнивание текста 833 Вывод текста справа налево 836 Дополнительные интервалы 839 Ширина символа 841 Нетривиальный вывод текста 846 Преобразование символов в глифы 846 Кернинг 847 Расположение символов 848 Функция ExtTextOut 850 Uniscribe 854 Доступ к данным глифов 855 Форматирование текста 864 Вывод текста с табуляцией 864 Простое абзацное форматирование 866 Аппаратно-независимое форматирование текста 868 Эффекты при выводе текста 871 Цветтекста 872 Начертания 875 Геометрические эффекты 877 Работа с текстом в растровом формате 882 Текст как совокупность кривых 888 Текст как регион 894 Итоги 895 Пример программы 896 Глава 16. Метафайлы 897 Общие сведения о метафайлах 897 Создание расширенного метафайла 898 Воспроизведение расширенного метафайла 900 Получение информации о расширенном метафайле 903 Передача расширенных метафайлов 907 Строение расширенных метафайлов 911 Записи EMF 912 Классификация типов записей EMF 914 Расшифровка записей EMF 916 Простые объекты GDI в EMF 918 Растры в EMF 919 Регионы в EMF 921 Траектории в EMF 922 Палитры в EMF 922
Содержание Системы координат в EMF 924 Команды вывода в EMF 926 Аппаратная независимость EMF 929 Перечисление записей EMF 930 Класс C++ для перечисления записей EMF 931 Замедленное воспроизведение EMF 932 Трассировка воспроизведения EMF 933 Динамическое изменение EMF 935 Построение производных метафайлов 937 EMF как средство программирования 941 Декомпилятор EMF 941 Сохранение EMF-файла спулера 943 Итоги 945 Дополнительная информация 946 Примеры программ t 946 Глава 17. Печать 947 Знакомство со спулером 947 Процесс печати 948 Язык управления принтером 949 Прямой вывод в порт 952 Печать с использованием спулера 954 Процессор печати EMF 958 Перечисление принтеров 959 Получение информации о принтере 961 Настройка драйвера принтера 961 Базовая печать средствами GDI 965 Стандартные диалоговые окна печати 965 Создание контекста устройства принтера 971 Получение информации о контексте устройства принтера 973 Последовательность формирования заданий печати 975 Поддержка печати в программах 978 Единая логическая система координат . . 978 Имитация внешнего вида страницы 981 Одновременный вывод страниц 982 Печать нескольких страниц на одном листе 983 Родовой класс печати 984 Вывод в контексте устройства принтера .% 989 Единицы измерения 989 Текст 990
Содержание 19 Растры 993 Печать графики в формате JPEG 993 Итоги 998 Дополнительная информация 999 Примеры программ 999 Глава 18. DirectDraw и непосредственный режим Direct3D юоо Технология СОМ 1001 СОМ-интерфейсы 1001 СОМ-классы 1002 Создание СОМ-объекта 1004 HRESULT 1004 DirectX и СОМ 1005 Общие сведения о DirectDraw 1007 Интерфейс IDirectDraw7 1008 Интерфейс IDirectDrawSurface7 1010 Вывод на поверхности DirectDraw 1014 Подбор цветов 1018 Интерфейс IDirectDrawClipper 1020 Простое окно DirectDraw 1021 Построение графической библиотеки DirectDraw 1023 Вывод пикселов 1024 Вывод линий 1026 Заливка замкнутых областей 1029 Отсечение 1031 Внеэкранные поверхности 1033 Поддержка прозрачности посредством цветовых ключей 1035 Шрифт и текст 1035 Спрайты 1039 Непосредственный режим Direct3D 1043 Подготовка среды непосредственного режима Direct3D 1044 Изменение размеров окна 1047 Двухэтапный вывод \ 1048 Использование Direct3D в окне 1049 Текстурные поверхности 1050 Пример использования непосредственного режима Direct3D 1052 Итоги 1055 Примеры программ , 1056 Алфавитный указатель 1058
С любовью и благодарностью посвящаю эту книгу своим родителям... маме и светлой памяти отца, а также жителям моего родного города, восточного сада Сучжоу
Благодарности Эта книга никогда бы не появилась на свет без помощи, поощрения и поддержки многих людей, которым я искренне благодарен. Я хочу поблагодарить редактора HP Press Сьюзен Райт (Susan Wright) и редактора Prentice Hall PTR Джилл Пайсони (Jill Pisoni) — они доверили неизвестному программисту написание 650-страничной книги, которая в итоге разрослась до 1200 страниц, и прощали все задержки. В Prentice Hall PTR ведущий редактор Джеймс Маркхэм (James Markham) и выпускающий редактор Фей Геммеларо (Faye Gemmelaro) давали ценные указания по структуре книги и представлении технической информации, предлагали новые способы подачи материала, улучшали авторский стиль, помогали найти и решить многие проблемы. В Hewlett Packard действует замечательная программа, которая предоставляет работнику, пожелавшему написать техническую книгу, организационную поддержку со стороны HP. Спасибо моему начальнику и вдохновителю этой книги Айвену Креспо (Ivan Crespo) за постоянное содействие на протяжении всей работы над книгой. Четыре года назад я перешел в научно-исследовательскую лабораторию Hewlett-Packard в Ванкувере, где были разработаны всемирно известные принтеры HP DeskJet, обладая некоторыми навыками программирования Win 16. За изучением исходных текстов программ, в обсуждениях и спорах с коллегами, за программированием и трассировкой ассемблерного кода в SoftICE/W я узнал так много, что через полтора года решил обратиться в HP Press и предложить этот проект. Я благодарен работникам научно-исследовательской лаборатории Hewlett-Packard в Ванкувере за все, чему я у них научился, и за их поддержку. Перехожу к самому важному. Я вечно благодарен своей жене Инь Пен за то, что она поверила, поняла и поддержала меня во время моих долгих сражений с GDI на выходных, по вечерам и даже ночью. Наш сын, Чао Чу, тоже старался помочь и каждый вечер перед сном разглядывал экран монитора. Наконец-то у меня появится свободное время и этим летом мы непременно достроим его подводного робота. Фень Юань
Введение Новая книга, посвященная программированию для Windows, принесет пользу лишь в том случае, если будет содержать глубокую, полную, современную, достоверную и практичную информацию. Глубокая книга не останавливается на уровне API, а проникает в архитектурные концепции, внутренние структуры данных и принципы реализации API. Кроме того, она должна предоставить читателю средства для самостоятельных исследований. Полная и современная книга уделяет основное внимание лучшей из существующих на сегодняшний день реализаций Win32 API — Windows 2000, основе будущих операционных систем Microsoft, и описывает ее новые возможности. Достоверная книга базируется на экспериментальных исследованиях Win32 API и внимательной проверке всей информации. Отталкиваться только от документации Microsoft нельзя, поскольку в ней описывается абстрактный интерфейс Win32 API, также зачастую попадается неполная, устаревшая и неточная информация. Практичная книга выходит за рамки простого описания API и тривиальных пояснительных примеров. Она ориентируется на практические задачи; содержит программный код, который может использоваться в реальных программах; предоставляет в распоряжение читателя полезные утилиты и помогает ему в написании профессиональных программ. Как известно, Win32 GDI (и графическое программирование для Windows в целом) является одним из краеугольных камней любой Windows-программы. Этой теме посвящено немало книг, но все сообщество программистов, часто работающих с Windows GDI, определенно нуждается в более глубокой, более полной, более современной, более достоверной и более практичной информации. Именно этими целями автор руководствовался при написании книги.
О чем эта книга 23 О чем эта книга Книга посвящена графическому программированию для Windows с использованием Win32 GDI API. Кроме того, в ней приведены начальные сведения о DirectDraw и еще более краткое введение в непосредственный режим Direct3D. Рассматриваются стандартные возможности, поддерживаемые на всех платформах Win32, 32-разрядные возможности, реализованные только в Windows NT/ 2000, и новейшие расширения GDI, появившиеся только в Windows 2000 и Windows 98. В частности, приведено полное описание альфа-ргаложения, прозрачного блиттинга, градиентных заливок, правостороннего вывода текста, прозрачных окон и передачи на принтер изображений в формате JPEG/PNG. Книга дает читателю хорошее представление о том, как работает графическая система Windows, и учит его более уверенно и эффективно пользоваться Win32 API. Книга учит тому, что любая документация Win32 требует аналитического и критического подхода. Прежде всего необходимо понять, какой логикой руководствовались разработчики, а эксперименты и здравый смысл помогут вам лучше разобраться в Win32 API, самостоятельно найти отсутствующую информацию или ошибки в документации. Книга научит вас эффективно пользоваться утилитами, помогающими лучше понять Win32 API. Что еще важнее, она научит вас создавать такие утилиты самостоятельно (нередко с использованием хитроумных приемов системного программирования) и проводить интересные эксперименты при исследованиях недокументированных аспектов Win32 API. Несколько начальных глав содержат общие сведения о внутренней работе системы, применимые в других областях Windows-программирования. В книге приведено множество фрагментов кода, подходящих для практического применения. Помимо простейших тестовых и демонстрационных программ, вы найдете в ней множество функций, классов C++, драйверов, утилит и нетривиальных программ, вполне подходящих для использования в коммерческих проектах. В книге разрабатывается целая библиотека классов C++, при помощи которых вы сможете работать с простыми окнами, окнами SDI и MDI, стандартными и пользовательскими диалоговыми окнами, панелями инструментов, строками состояния и т. д. Классы, входящие в библиотеку, упрощают работу с DIB-растрами, DDB-растрами и DIB-секциями, воспроизведение EMF, применение растровых алгоритмов, квантование цветов, кодирование/декодирование изображений в формате JPEG, расшифровку файлов шрифтов, подстановку шрифтов по метрикам PANOSE, вывод глифов, построение объемного текста и т. д. Программы, приведенные в книге, не зависят ни от MFC (Microsoft Foundation Classes), ни от каких-либо других библиотек классов, поэтому они могут использоваться в любой программе на C++. Все имена классов начинаются с префикса «К», поэтому вы можете использовать их в MFC, ATL, OWL или в вашей персональной библиотеке классов.
24 Введение Как организована эта книга Графическое программирование для Windows рассматривается на трех уровнях: на уровне реализации, на уровне API и на прикладном уровне. К уровню реализации относится все, что осталось «за кулисами» Win32 GDI API и С ОМ-интерфейсов DirectX, — недокументированный мир графического механизма и клиентских библиотек DLL подсистем Windows. Материал, изложенный в главах 2, 3 и 4, закладывает прочную основу для понимания уровня API. На уровне API предоставляется четкое, точное, последовательное описание Win32 GDI API, а также (хотя и менее подробрю) DirectDraw и непосредственного режима Direct3D. Прикладной уровень расположен над уровнем API. К нему причисляется решение практических задач, создание функций, подходящих для повторного использования, классов C++ и нетривиальных программ. При изложении материала уровень API переплетается с прикладным уровнем. Обычно каждая глава начинается с описания уровня API, а затем переходит к практическим примерам. При изложении особо сложного материала (например, описания растров) в одной главе излагается основной теоретический материал, а в последующих главах — его нетривиальные применения. Глава 1, «Основные принципы и понятия», посвящена базовым концепциям Windows-программирования, используемым во всей книге. В ней приводятся общие сведения о программировании для Windows, языке ассемблера процессоров Intel, среде разработки программ, формате исполняемых файлов Win32 и архитектуре операционной системы Windows. Моя любимая часть посвящена простейшему перехвату функций API посредством модификации каталогов импорта/экспорта в модулях Win32. В главе 2, «Архитектура графической системы Windows», приведен общий обзор графической системы Windows, от DLL различных подсистем Win32 до драйверов графических устройств. В ней рассматриваются компоненты графической системы Windows, архитектура GDI, архитектура DirectX, архитектура подсистемы печати, графический механизм, драйверы экрана и принтеров. На мой взгляд, самое интересное в этой главе — описание системных функций, объединяющих реализацию GDI пользовательского режима с графическим механизмом режима ядра, утилита для составления списка вызовов недокументированных системных функций (в GDI32.DLL, USER32.DLL, NTDLL.DLL и WIN32K.SYS) и простой драйвер принтера, генерирующий страницы HTML с внедренными растровыми изображениями. Глава 3, «Внутренние структуры данных GDI/DirectDraw», читается как детектив или книга о поисках сокровищ. Глава начинается с объяснения парадигмы объектно-ориентированного программирования Win32, основанной на использовании манипуляторов. Затем мы пытаемся разобраться, что же собой представляет манипулятор объекта GDI, переходим к поиску таблицы объектов GDI и ее расшифровке. Далее описывается сложная иерархия структур данных, используемых во внутренней работе графической системы Windows. При поиске таблицы объектов GDI применяются отладочные файлы символических имен, специально написанные утилиты и отладчик Visual C++. Мы даже напишем драйвер устройства для чтения данных из адресного пространства режима ядра.
Как организована эта книга 25 В программе Fosterer, написанной для главы 3, используется расширение отладчика Microsoft для расшифровки таблицы объектов GDI и внутренних структур данных графического механизма DirectX — притом на одном компьютере! Не упускайте такой шанс и непременно опробуйте программу Fosterer на компьютере с Windows NT или 2000. Впрочем, сначала вам придется установить отладочные файлы с символическими именами и отладчик WinDbg. Считайте описание внутренних структур данных своего рода справочным материалом, который помогает разобраться в процессе отладки на уровне DDI, поскольку подробности реализации могут изменяться в зависимости от версии операционной системы и даже от версии Service Pack. Вы можете пропустить любой раздел, который покажется недостаточно интересным, и вернуться к нему, когда вам понадобится дополнительная информация — например, чтобы лучше понять использование ресурсов объектами GDI или проблемы быстродействия. В главе 4, «Мониторинг графической системы Windows», представлены различные приемы и инструменты для слежения за графической подсистемой и за системой Windows в целом. Вы узнаете, как внедрять свои DLL во внешние процессы, как подключиться к цепочке вызовов API, как отслеживать и перехватывать вызовы функций Win32 API, как перехватывать вызовы системных функций и методы СОМ-интерфейсов и, наконец, вызовы функций интерфейса DDI режима ядра. Мои любимые темы — написание заглушек на ассемблере, перехват внутримодульных вызовов, вызовов системных функций и функций DDI; все это дает представление о том, как же в действительности работает система. Глава 4 рассчитана на опытного программиста; если она вам пока не нужна — пропустите ее. С главы 5, «Абстракция графического устройства», начинается описание функций API графического программирования Windows и примеров их практического применения. В главе 5 рассматриваются видеоадаптеры, кадровые буферы, объекты контекстов устройств, родовой класс рамочного окна и вывод в окне. Моя любимая тема — программа WinPaint, которая дает наглядное представление о сообщениях перерисовки окна. В главе 6, «Системы координат и преобразования», рассматриваются четыре системы координат, поддерживаемые в GDI, отображение окна в область просмотра, мировые (аффинные) преобразования и их роль в прокрутке и изменении масштаба. Во время работы над книгой мне не удавалось вдоволь поиграть в любимую настольную игру «вэйчи», поэтому для главы 6 я написал простую программу, рисующую доску для вэйчи. Глава 7, «Пикселы», содержит краткий обзор объектов GDI, манипуляторов и таблицы объектов на уровне GDI API. Далее рассматривается программа, при помощи которой можно следить за использованием манипуляторов GDI на уровне системы. От регионов мы переходим к механизму отсечения, цветовым пространствам и выводу отдельных пикселов, а напоследок напишем программу для вывода множеств Мандельброта. Самое полезное в этой главе — это описание системных регионов, метарегионов, регионов отсечения и регионов Рао, используемых при отсечении, а также программы ClipRegion для их наглядного представления. В главе 8, «Линии и кривые», рассматриваются бинарные растровые операции, режимы заполнения фона, фоновые цвета, объекты логических перьев,
26 Введение линии, кривые Безье, дуги, траектории и стилевые линии, не поддерживаемые в GDI напрямую. На мой взгляд, в этой главе стоит обратить внимание на математические выкладки, связанные с преобразованием эллиптических кривых в кривые Безье. В главе 9, «Замкнутые области», описываются кисти, прямоугольники, эллипсы, секторы и сегменты, закругленные прямоугольники, многоугольники, замкнутые траектории, регионы, градиентные заливки и различные приемы заполнения замкнутых фигур, используемые в графических приложениях. Особый интерес представляет применение градиентных заливок для рисования трехмерных кнопок и описание структур данных регионов. Глава 10, «Основные сведения о растрах», посвящена трем растровым форматам, поддерживаемым в GDI, — аппаратно-независимым растрам (DIB), аппа- ратно-зависимым растрам (DDB) и DIB-секциям. В этой главе описаны классы для работы с DIB, DDB и DIB-секциями, совместимые контексты устройств и стандартные применения этих растровых форматов. Обратите внимание на классы, особенно на применение DIB-секций для аппаратно-независимого воспроизведения EMF. В главе И, «Нетривиальное использование растров», рассматриваются тернарные растровые операции, вывод прозрачных растров, реализация прозрачности без применения масок, альфа-наложение и одна из новых возможностей Windows 2000 — прозрачные окна. Моя любимая часть — полное описание растровых операций и имитация кватернарных растровых операций использованием нескольких тернарных операций. В главе 12, «Графические алгоритмы и растры Windows», описан прямой доступ к пикселам растров, аффинные преобразования растров, преобразования цветов и пикселов, а также пространственные фильтры. Глава 13, «Палитры», посвящена системным и логическим палитрам, сообщениям палитр, палитрам в растрах, квантованию цветов и распределению ошибок при сокращении количества цветов. Приведенная реализация алгоритма квантования цветов по октантному дереву часто строит более качественную палитру, чем коммерческие приложения. В главе 14, «Шрифты», рассматриваются наборы символов, кодировки, глифы, гарнитуры, семейства шрифтов, растровые и векторные шрифты, шрифты TrueType, установка шрифтов в системе и их внедрение в документы. Особенно интересный материал приведен в разделе, посвященном внутреннему формату файлов шрифтов TrueType. Глава 15, «Текст», посвящена логическим шрифтам, подстановке шрифтов, системе PANOSE, текстовым метрикам, простому и сложному выводу текста, форматированию и эффектам при выводе текста. Последняя тема заслуживает особого внимания; вы узнаете, как наложить растровое изображение на выводимый текст, как создать тени и имитировать рельеф, как вывести текст наклонно и вертикально, как разместить символы вдоль кривой, как преобразовать текст в растр или контур и как создается простейший объемный текст. В главе 16, «Метафайлы», рассматривается процесс создания и воспроизведения метафайлов, их внутреннее строение и особенности внедрения в них объектов GDI. Вы познакомитесь с расшифровкой EMF, перечислением записей,
Как читать эту книгу 27 декомпиляцией и сохранением данных спулера в формате EMF. На мой взгляд, самое интересное в этой главе — декомпилятор EMF и программа EMFScope, предназначенная для сохранения файлов спулера в Windows 95/98. Глава 17, «Печать», посвящена спулеру, простейшей печати средствами GDI, поддержке печати в приложениях, выводу графики в формате JPEG (включая непосредственную передачу JPEG драйверу принтера) и печати программ C++ с цветовым выделением синтаксических конструкций. Самое интересное в этой главе — набор универсальных классов для одновременного вывода нескольких страниц независимо от разрешения и масштаба устройства. Эти классы используются и в программе вывода JPEG, и в программе вывода исходных текстов. Глава 18, «DirectDraw и непосредственный режим Direct3D», содержит вводный курс программирования для DirectX, ориентированный на опытных программистов GDI. В ней излагаются основы СОМ, приводятся классы среды DirectDraw и поверхностей DirectDraw. Здесь описаны три способа вывода в DirectDraw, объекты отсечения, внеэкранные поверхности и вывод текста в DirectDraw. Кроме того, приведены классы для простейших операций непосредственного режима Direct3D, двойной буферизации, работы с текстурами и окон с поддержкой DirectDraw. Моя любимая часть — использование GDI для создания шрифтовых поверхностей DirectDraw, обеспечивающих эффективный вывод текста на поверхностях DirectX. Как читать эту книгу Книга предназначена в основном для опытных программистов, которые работают с Win32 API непосредственно или через библиотеки классов. Вероятно, новичку лучше начать с другой книги. Прежде всего необходимо познакомиться с принципами строения Windows-программ и внимательно разобраться в том как они работают. Если вас интересует только само графическое программирование и вы не хотите разбираться с подробностями реализации на уровне системы, прочитайте главы 1 и 2, пропустите главы 3 и 4 и продолжайте читать с главы 5. При желании вы даже можете пропустить некоторые разделы глав 1 и 2. Начиная с главы 5 материал излагается последовательно и систематично. Если вы принадлежите к числу опытных, квалифицированных программистов, значит, вы точно знаете, что именно вам нужно. Возможно, вам стоит бегло просмотреть начало книги и сразу перейти к главе 3. Если вас интересует программирование системного уровня (например, отслеживание вызовов API), прочитайте соответствующие части глав 1 и 2, а также главы 3 и 4. Наконец, если вы вообще не программист (например, если ваша работа связана с тестированием программ), в главе 2 вы найдете общий обзор графической системы Windows. Вероятно, стоит прочитать начало главы 3 — вы узнаете все, что необходимо знать об утечке ресурсов GDI, и получите в свое распоряжение полезные диагностические утилиты.
28 Введение Что находится на компакт-диске К книге прилагается компакт-диск с множеством программ-примеров, функций и классов. Точнее говоря, диск содержит свыше 1300 Кбайт исходных текстов C++, 400 Кбайт заголовочных файлов C++ и слегка видоизмененную версию исходных файлов библиотеки, основанной на свободно распространяемом коде Independent JPEG Group (www.ijg.org). Программы откомпилированы в 49 исполняемых файлов, три драйвера режима ядра и одну динамическую библиотеку пользовательского режима. Разумеется, в книге приведена лишь часть программного кода. На компакт- диске находятся полные исходные тексты, файлы рабочих областей Microsoft Visual C++, заранее откомпилированные двоичные файлы (в отладочных и окончательных версиях) и файлы в формате JPEG для глав, посвященных графическим алгоритмам. На компакт-диске имеется автоматически запускаемая программа установки, которая устанавливает программные файлы, создает в меню соответствующие ссылки и включает в него важные web-адреса, по которым можно загрузить утилиты Microsoft и найти техническую информацию. Программы были разработаны и протестированы в окончательной версии Windows 2000 (сборка 2195) на видеоадаптере, поддерживающем аппаратное ускорение двумерной и трехмерной графики DirectX 7.0, хотя многие программы успешно работают в Windows 95/98/NT 4.0 и не требуют поддержки DirectX. Для самостоятельной компиляции программ в вашей системе должны быть установлены следующие компоненты. О Компилятор Visual C++ 6.0. О Обновление Visual Studio 6.0 Service Pack 3 (msdn.microsoft.com/vstudio/sp/vs6sp3). О Электронная документация библиотеки MSDN. О Обновленные заголовочные и библиотечные файлы, а также утилиты из пакета Platform SDK (www.microsoft.com/downloads/sdks/platform/platform.asp). Убедитесь, что компилятор VC 6.0 настроен на использование заголовочных файлов и библиотечных каталогов Platform SDK. О Отладочные файлы символических имен Windows 2000 используются некоторыми утилитами и оказывают немалую помощь в отладке (www.microsoft.com/ windows200/downloads/otherdownloads/symbols). О Windows 2000 DDK (www.microsoft.com/ddk) используется некоторыми драйверами режима ядра. Добавьте каталог inc DDK к каталогам заголовочных файлов VB. Добавьте каталог Iibfre\i386 DDK к каталогам библиотечных файлов VC. О WinDebug (www.microsoft.com/ddk/debugging) используется системными утилитами главы 3. Хотя все примеры в этой книге написаны на C++ без применения MFC, программисты MFC, ATL или OWL смогут без особого труда воспользоваться этим кодом. Даже программисты Visual Basic или Delphi найдут немало полезного в примерах, поскольку эти среды разработки поддерживают прямой вызов функций Win32 API.
От издательства 29 Что дальше? Работая над книгой, автор должен привести в порядок свои мысли, провести необходимые исследования и представить материал в логичной, последовательной манере. Надеюсь, эта книга, в которой я постарался подробно передать приобретенные знания, сможет чему-то научить и моих коллег-программистов. Но теперь читатели со всего мира становятся моими учителями и соучениками. Если вы обнаружите какую-нибудь ошибку или неточность, если у вас появятся комментарии, предложения или жалобы, свяжитесь со мной через мой персональный web-сайт http://www.fengyuan.com. На этом сайте также можно найти ответы на часто встречающиеся вопросы, обновления, описания наиболее сложных примеров и т. д. От издательства Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете на web-сайте издательства http://www.piter.com.
Глава 1 Основные принципы и понятия Мы отправляемся в путешествие по графической системе Windows и исследуем ее вдоль и поперек, от гладкой поверхности (уровня графических функций Win32 API) до каменистого дна (уровня драйверов экрана/принтера). Графическая система Windows содержит немало важных элементов, однако наше внимание будет сосредоточено на ее главных составляющих: интерфейсе Win32 GDI (Graphics Device Interface — интерфейс графических устройств) и компоненте DirectDraw интерфейса DirectX. Функции Win32 GDI API реализованы на многих платформах, в том числе на Win32s, Win95/98, Win NT 3.5/4.0, Windows 2000 и WinCE, причем между этими реализациями существуют значительные отличия. Например, Win32s и Win95/98 основаны на старой 16-разрядной реализации GDI с многочисленными ограничениями, а полноценные 32-разрядные реализации Windows NT 3.5/4.0 и Windows 2000 обладают гораздо большими возможностями. Интерфейсы DirectDraw характерны для платформ Win95/98, Win NT 4.0 и Windows 2000. Эта книга в основном ориентируется на платформы Windows NT 4.0 и Windows 2000, обладающие самыми мощными реализациями этих интерфейсов. Замечания, относящиеся к другим платформам, будут приводиться по мере необходимости. Но прежде чем переходить к углубленному изучению графической системы Windows, необходимо разобраться в некоторых базовых концепциях, играющих очень важную роль для дальнейших исследований. В этой главе описываются основные принципы программирования для Windows на C/C++, приводится краткий обзор программирования на ассемблере, сред программирования и отладочных средств, а также рассматриваются формат исполняемых файлов Win32 и общая архитектура операционной системы Windows.
Основы программирования для Windows на C/C++ 31 ПРИМЕЧАНИЕ Предполагается, что читатель уже обладает некоторым опытом программирования для Windows, поэтому материал излагается очень кратко. Основы программирования для Windows на C/C++ Профессия программиста прошла драматический путь развития от «средневековых» машинных кодов до современных языков программирования — таких, как С, Visual Basic, Pascal, C++, Delphi и Java. Считается, что количество строк программного кода, написанных программистом за день, практически не зависит от используемого языка. Следовательно, чем выше продвигается язык по уровню абстракции, тем продуктивнее становится работа программиста. До недавнего времени самым распространенным языком программирования для Windows считался С — в этом нетрудно убедиться по примерам программ, включенным в пакеты Microsoft Platform Software Development Kit (Platform SDK) и Device Driver Kit (DDK). Объектно-ориентированные языки — такие, как C++, Delphi и Java — быстро набирают темп и постепенно вытесняют С и Pascal. Они составляют новое поколение языков программирования для Windows. Несомненно, объектно-ориентированные языки являются шагом вперед по сравнению со своими предшественниками. Скажем, C++ даже без применения «чистых» объектных средств (классов, наследования, виртуальных функций и т. д.) превосходит С по таким современным возможностям, как жесткая прототипизация, шаблоны и подставляемые (inline) функции. Однако написание объектно-ориентированных программ для Windows — задача не из простых, поскольку прикладной интерфейс Windows (Windows API) разрабатывался без учета поддержки объектно-ориентированных языков. Например, функции косвенного вызова (в частности, обработчики сообщений и процедуры диалоговых окон) должны быть глобальными. Компилятор C++ не позволяет передать обычную функцию класса в качестве функции косвенного вызова. Для «упаковки» Windows API в иерархию классов была разработана библиотека Microsoft Foundation Classes (MFC), которая фактически превратилась в стандарт объектно-ориентированного программирования для Windows. MFC в значительной степени решает проблему интеграции объектно-ориентированного языка C++ с интерфейсом Win32 API, ориентированным на язык С. MFC передает одну глобальную функцию в качестве общего обработчика сообщений окна. Эта функция преобразует HWND в указатель на объект CWnd, переходя таким образом от манипулятора (handle) окна Win32 к указателю на объект окна C++. С ростом популярности технологий OLE, COM и ActiveX даже компания Microsoft обеспокоилась огромными размерами и сложностью MFC, поэтому для написания облегченных СОМ-серверов и элементов ActiveX сейчас рекомендуется использовать другую библиотеку классов от Microsoft — Active Template Library (ATL).
32 Глава 1. Основные принципы и понятия С учетом тенденций перехода на объектно-ориентированное программирование примеры программ в этой книге написаны в основном на C++, а не на С. Чтобы приведенный код приносил пользу программистам, работающим на С, C++, MFC, ATL, C++ Builder и даже Delphi с Visual Basic, в книге не используются ни экзотические возможности C++, ни специфические средства MFC/ATL. Hello World, версия 1: запуск браузера Довольно теории — перейдем к написанию несложных Windows-программ на C++. Ниже приведен исходный текст нашей первой программы. //Hellol.cpp #define STRICT #include <windows.h> #include <tchar.h> #include <assert.h> const TCHAR szOperation[] = _T("open"); const TCHAR szAddress[] = _T("www.helloworld.com"); int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE. LPSTR IpCmd. int nShow) { HINSTANCE hRslt = ShellExecute(NULL, szOperation. szAddress. NULL. NULL. SWJHOWNORMAL); assert( hRslt > (HINSTANCE) HINSTANCE JRROR): return 0; } ПРИМЕЧАНИЕ Примеры программ на прилагаемом компакт-диске находятся в каталогах, имена которых соответствуют номерам глав — ChaptOl, Chapt02 и т. д. Весь общий код расположен в дополнительном каталоге include на одном уровне с каталогами глав. В каталоге каждой главы находится один файл рабочей области Microsoft Visual C++, содержащий все проекты данной главы. Каждый проект находится в отдельном подкаталоге; например, проект Hello 1 расположен в каталоге Chapt_01\Hellol. В ссылках на общие файлы (например, win.h) в исходном тексте используются относительные пути вида ..\\..\include\win.h. Перед вами не стандартная программа «Hello, World», ограничивающаяся выдачей текстового сообщения, а новый представитель этого семейства из эпохи Интернета. Если выполнить эту программу, функция Win32 API Shell Execute запустит браузер и откроет в нем заданную web-страницу. В этой простой программе следует обратить внимание на некоторые особенности, которые редко встречаются в тривиальных примерах, приводимых в других книгах. Автор включил в нее эти аспекты, поскольку они способствуют развитию правильного стиля программирования. Программа начинается с определения макроса STRICT. Это сделано для того, чтобы при включении заголовочных файлов Windows различные типы объектов
Основы программирования для Windows на C/C++ 33 интерпретировались по-разному, и компилятору было проще выдавать программисту предупреждения о том, что он путает HANDLE с HINSTANCE или HPEN — с HBRUSH. Когда читатели жалуются, что примеры из некоторых книг даже не компилируются, скорее всего, эти примеры не были протестированы с определением макроса STRICT. Дело в том, что новые версии заголовочных файлов Windows включают STRICT по умолчанию, а старые версии этого не делают. Включение файла <tchar.h> обеспечивает возможность компиляции одного исходного текста в двоичный код как с поддержкой Unicode, так и без нее. Программы, предназначенные для операционных систем из семейства Windows 95/98, не рекомендуется компилировать в режиме Unicode, а если это все же делается — программист должен действовать очень внимательно и избегать применения функций API на базе Unicode, не реализованных в Win95/98. Помните, что параметр lpCmd функции WinMain никогда не кодируется в Unicode; для получения TCHAR-версии полной командной строки следует воспользоваться функцией GetCommandLineO. Включение файла <assert.h> относится к области защищенного программирования. Желательно, чтобы программист в пошаговом режиме выполнил каждую строку своей программы и убедился в отсутствии ошибок. Проверка параметров и возвращаемых значений функций директивой assert помогает обнаруживать непредвиденные ситуации на протяжении всей фазы разработки. Существует и другой способ перехвата ошибок программирования — обработка исключений в программе. Два определения массивов const TCHAR гарантируют, что эти строковые константы будут размещены в области данных, доступных только для чтения, окончательной версии двоичного кода, сгенерированной компилятором и компоновщиком. Если включить строки вида _Т( "print") прямо в вызов Shell Execute, скорее всего, они в итоге попадут в область данных, доступных для чтения/записи. Размещение констант в области данных, доступных только для чтения, гарантирует, что эти данные будут только читаться, а при попытке записи в них произойдет общая ошибка защиты (General Protection Fault, GPF). Кроме того, эти данные могут совместно использоваться разными экземплярами программы, что позволяет экономить память при запуске нескольких экземпляров одного модуля в системе. Имя второго параметра функции WinMain (обычно он называется hPrevInstance) при вызове не указывается, поскольку в программах Win32 он не используется. В Winl6 параметр hPrevInstance содержал манипулятор предыдущего экземпляра текущей программы. В Win32 каждая программа работает в отдельном адресном пространстве. Даже если в системе работают несколько экземпляров одной программы, обычно они не «видят» друг друга. Написать идеальную программу трудно, а то и вовсе невозможно, однако при помощи некоторых приемов вы можете заставить компилятор построить идеальный двоичный код. Для этого необходимо правильно выбрать тип процессора, runtime-библиотеку, тип оптимизации, способ выравнивания полей структур и базовый адрес DLL. Отладочная информация, файл символических имен или даже листинг на языке ассемблера помогут в процессе отладки, анализа отчетов или тонкой настройки быстродействия. Другой подход заключается в анализе двоичного кода с применением символических данных, средств быстрого про-
34 Глава 1. Основные принципы и понятия смотра Проводника Windows 95/98/NT и Dumpbin; вы должны убедиться в том, что программа экспортирует нужные функции, не импортирует никаких необычных функций, а также в том, что двоичный код не содержит неожиданных фрагментов. Например, программа, импортирующая функцию 420 библиотеки oleauto.dll, не будет работать в ранних версиях Win95. Если программа загружает несколько DLL по одному и тому же базовому адресу, ее выполнение замедляется из-за динамического перемещения. Если откомпилировать проект Hello 1 с параметрами по умолчанию, размер исполняемого двоичного файла в окончательной (release) версии равен 24 Кбайт. Программа импортирует три десятка функций Win32 API, хотя в исходном тексте используется лишь одна функция. В программе задействовано около 3000 байт инициализированных глобальных данных, хотя непосредственно в программе никаких данных не используется. Если попытаться выполнить программу в пошаговом режиме, вскоре выясняется, что WinMain в действительности не является начальной точкой нашей программы. Вызову WinMain в настоящей начальной функции WinMainCRTStartup предшествует немало других событий. В таких простых программах, как Hellol.cpp, можно воспользоваться DLL- версией runtime-библиотеки С и написать свою собственную реализацию функции WinMainCRTStartup — в этом случае компилятор и компоновщик сгенерируют действительно небольшой двоичный код. Эта возможность продемонстрирована в следующем примере. Hello World, версия 2: вывод текста на рабочем столе Поскольку книга посвящена программированию графики в Windows, основное внимание в ней должно уделяться графическим функциям API. Исходя из этого, следующая версия «Hello, World» работает несколько иначе. #define STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <tchar.h> #include <assert.h> void CenterText(HDC hDC. int x. int y. LPCTSTR szFace, LPCTSTR szMessage. int point) { HFONT hFont - CreateFont( -point * GetDeviceCaps(hDC. LOGPIXELSY) / 72. 0. 0. 0. FW_B0LD, TRUE. FALSE. FALSE. ANSI_CHARSET. 0UT_TT_PRECIS. CLIP_DEFAULT_PRECIS. PR00F_QUALITY. VARIABLE_PITCH. szFace); assert(hFont); HGDI0BJ hOld = Select0bject(hDC. hFont): SetTextAlign(hDC. TA_CENTER | TA_BASELINE);
Основы программирования для Windows на C/C++ 35 SetBkModeChDC. TRANSPARENT); SetTextColorChDC. RGB(0. 0, OxFF)): TextOut(hDC. x, y, szMessage. _tcslen(szMessage)); SelectObject(hDC. hOld); DeleteObject(hFont); } const TCHAR szMessage[] = _T("Hello. World"); const TCHAR szFace[] = _T("Times New Roman"); #pragma comment(linker, "-merge:.rdata=.text") #pragma comment(linker. "-align:512") extern "C" void WinMainCRTStartupO { HDC hDC = GetDC(NULL); assert(hDC); CenterText(hDC. GetSystemMetrics(SM_CXSCREEN) / 2. GetSystemMetrics(SM_CYSCREEN) / 2. szFace, szMessage. 72); ReleaseDC(NULL. hDC); ExitProcess(O); } Приведенная выше программа при помощи простых функций GDI выводит строку «Hello, World» в центр экрана, не создавая окна. Программа получает контекст устройства для окна рабочего стола (или основного монитора при наличии нескольких мониторов), создает курсивный шрифт с высотой символов в 1 дюйм и выводит строку «Hello, World» в прозрачном режиме сплошным синим цветом. Чтобы двоичный код занимал как можно меньше места, программа создает собственную функцию WinMainCRTStartup вместо того, чтобы использовать стандартную реализацию, предоставленную runtime-библиотекой C/C++. Последняя команда программы, ExitProcess, завершает выполнение процесса. Программа также приказывает компоновщику объединить область данных, доступных только для чтения (. rdata), с областью кода, доступной для чтения и исполнения (.text). Исполняемый файл, сгенерированный в окончательной версии, имеет размер всего 1536 байт. Hello, World, версия 3: создание полноэкранного окна Первая и вторая версии «Hello, World» не относились к числу обычных Windows-программ, работающих в окне. В них использовались лишь немногочисленные вызовы функций Windows API, которые показывали, как написать элементарную программу для Windows. Обычная оконная программа, написанная на C/C++, сначала регистрирует несколько классов окон, после чего создает главное окно (возможно — несколь-
36 Глава 1. Основные принципы и понятия ко дочерних окон) и входит в цикл, в котором все поступающие сообщения направляются соответствующим обработчикам. Вероятно, многие читатели хорошо знакомы с подобными примерами простейших Windows-программ. Чтобы не создавать очередной дубликат, мы попробуем написать простую объектно-ориентированную оконную программу на C++, не используя MFC. Для этого нам понадобится очень простой класс KWindow, реализующий основные операции по регистрации класса окна, созданию окна и доставке оконных сообщений. Первые две задачи решаются просто, но с третьей дело обстоит сложнее. Конечно, нам хотелось бы оформить функцию обработки сообщений как виртуальную функцию класса KWindow, но Win32 API запрещает использование подобных функций в качестве функции окна. При вызове функций классов C++ передается неявный указатель this, а их схемы передачи параметров могут отличаться от той, которая используется функцией окна. Одно из распространенных решений заключается в применении статической функции окна, которая передает запросы соответствующей функции класса C++. Для этого статическая функция окна должна иметь указатель на экземпляр KWindow. В нашем примере эта задача решается передачей указателя на экземпляр KWindow при вызове CreateWindowEx и его сохранением в структуре данных, связанной с каждым окном. ПРИМЕЧАНИЕ Имена всех классов C++ в этой книге начинаются с буквы «К» вместо традиционного префикса «С». Это упрощает работу с классами в программах, использующих MFC, ATL или другие библиотеки классов. Ниже приведен заголовочный файл класса KWindow. // win.h #pragma once class KWindow { virtual void OnDrawCHDC hDC) { } virtual void OnKeyDownCWPARAM wParam. LPARAM IParam) { } virtual LRESULT WndProc(HWND hWnd, UINT uMsg. WPARAM wParam. LPARAM IParam); static LRESULT CALLBACK WindowProcCHWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam); virtual void GetWndClassEx(WNDCLASSEX & vie): public: HWND m hWnd;
Основы программирования для Windows на C/C++ 37 KWindow(void) { m_hWnd - NULL; } virtual -KWindow(void) virtual bool CreateExCDWORD dwExStyle. LPCTSTR IpszClass. LPCTSTR IpszName, DWORD dwStyle, int x. int y, int nWidth, int nHeight. HWND hParent. HMENU hMenu. HINSTANCE hlnst); bool RegisterClassCLPCTSTR IpszClass. HINSTANCE hlnst); virtual WPARAM MessageLoop(void); BOOL ShowWindow(int nCmdShow) const { return ::ShowWindow(m hWnd, nCmdShow); BOOL UpdateWindow(void) const { return ::UpdateWindow(m_hWnd); Класс KWindow содержит всего одну переменную m_hWnd, в которой хранится манипулятор окна. В классе присутствует конструктор, виртуальный деструктор, функция для создания окна, а также функции цикла обработки сообщений, отображения и обновления окон. Закрытые (private) функции класса KWindow определяют структуру WNDCLASSEX и обрабатывают сообщения данного окна. Статическая функция WindowProc создается в соответствии с требованиями Win32 API; она передает сообщения виртуальной функции WndProc. Многие функции класса определяются как виртуальные, чтобы их поведение могло быть изменено в классах, производных от KWindow. Например, разные классы будут иметь разные реализации OnDraw, а в их реализации GetWndClassEx будут использоваться разные меню и курсоры. Удобная директива компилятора Visual C++ (#pragma once) помогает избежать многократного включения одного заголовочного файла. Чтобы добиться того же эффекта, можно определить дкя каждого заголовочного файла уникальный макрос и пропускать заголовочный файл в том случае, если макрос уже определен. Ниже приведена реализация класса KWindow. // win.cpp #define STRICT #define WIN32 LEAN AND MEAN #include <windows.h> #include <assert.h> #include <tchar.h>
38 Глава 1. Основные принципы и понятия #include "Awin.h" LRESULT KWindow::WndProc(HWND hWnd. UINT uMsg, WPARAM wParam, LPARAM IParam) { switch( uMsg ) { case WM_KEYDOWN: OnKeyDown(wParam. IParam); return 0; case WM_PAINT: { PAINTSTRUCT ps; BeginPaint(m_hWnd. &ps); OnDraw(ps.hdc); EndPaint(m_hWnd. &ps); } return 0; case WM_DESTROY: PostQuitMessage(O); return 0; } return DefWindowProcChWnd. uMsg. wParam. IParam); } LRESULT CALLBACK KWindow::WindowProc(HWND hWnd, UINT uMsg. WPARAM wParam, LPARAM IParam) { KWindow * pWindow; if ( uMsg—WMJICCREATE ) { assert( ! IsBadReadPtr((void *) IParam, sizeof(CREATESTRUCT)) ); MDICREATESTRUCT * pMDIC - (MDICREATESTRUCT *) ((LPCREATESTRUCT) 1Param)->1pCreateParams; pWindow - (KWindow *) (pMDIC->lParam); assert( ! IsBadReadPtr(pWindow, sizeof(KWindow)) ); SetWindowLong(hWnd, GWL USERDATA, (LONG) pWindow); } else pWindow-(KWindow *)GetWindowLong(hWnd. GWLJJSERDATA); if ( pWindow ) return pWindow->WndProc(hWnd. uMsg, wParam, IParam); else return DefWindowProc(hWnd. uMsg. wParam, IParam); } boo! KWindow::RegisterClass(LPCTSTR IpszClass. HINSTANCE hlnst)
Основы программирования для Windows на C/C++ 39 WNDCLASSEX wc: if ( ! GetClassInfoExChlnst. IpszClass. &wc) { GetWndClassEx(wc); wc.hlnstance = hlnst; wc.lpszClassName = IpszClass; if ( !RegisterClassEx(&wc) ) return false; } return true; bool KWindow::CreateEx(DWORD dwExStyle. LPCTSTR IpszClass. LPCTSTR IpszName, DWORD dwStyle. int x. int y, int nWidth. int nHeight, HWND hParent. HMENU hMenu. HINSTANCE hlnst) { if ( ! RegisterClassdpszClass, hlnst) ) return false; // Использовать MDICREATESTRUCT для поддержки дочерних окон MDI MDICREATESTRUCT mdic; memset(& mdic, 0. sizeof(mdic)); mdic.1 Pa ram = (LPARAM) this; m_hWnd = CreateWindowExCdwExStyle. IpszClass. IpszName, dwStyle. x. y. nWidth. nHeight. hParent. hMenu. hlnst. & mdic): return m hWnd!=NULL; void KWindow::GetWndClassEx(WNDCLASSEX & wc) { memset(& wc, 0, sizeof(wc)); wc.cbSize - sizeof(WNDCLASSEX); wc.style - 0; wc.lpfnWndProc - WindowProc; wc.cbClsExtra - 0: wc.cbWndExtra - 0; wc.hlnstance - NULL; wc.hlcon = NULL; wc.hCursor - LoadCursor(NULL. IDC_ARR0W); wc.hbrBackground - (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName - NULL: wc.lpszClassName - NULL wc.hlconSm - NULL
40 Глава 1. Основные принципы и понятия WPARAM KWindow::MessageLoop(void) { MSG msg; while ( GetMessage(&msg, NULL. 0. 0) ) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } Реализация KWindow довольно проста, если не считать статической функции WindowProc. Функция WindowProc отвечает за передачу сообщений от операционной системы Windows соответствующим обработчикам класса KWindow. Для этого мы должны иметь возможность получить указатель на экземпляр класса KWindow в функции окна Win32. С другой стороны, указатель передается только при вызове CreateWindowEx. Чтобы значение, передаваемое всего один раз, могло использоваться многократно, мы должны его где-то сохранить. В MFC информация хранится в глобальной карте, связывающей значения HWND с указателями на экземпляры класса CWnd, поэтому каждый раз, когда требуется доставить сообщение, производится хэшированный поиск нужного экземпляра CWnd. В нашей простой реализации класса KWindow было выбрано другое решение — указатель на экземпляр KWindow хранится в структуре данных, поддерживаемой в операционной системе Windows для каждого окна. WindowProc обычно получает указатель на KWindow во время обработки сообщения WMNCCREATE, которое обычно отправляется перед сообщением WM_CREATE и содержит то же значение указателя на структуру CREATESTRUCT. Указатель сохраняется вызовом SetWindowLong(GWLJJSERDATA) и позднее читается вызовом GetWindowLong(GWLUSERDATA). Так в нашем простом примере организуется связь между WindowProc к KWindow: :WndProc. У традиционных обработчиков сообщений (на базе С) есть существенный недостаток: при необходимости обратиться к дополнительным данным им требуются глобальные данные. При создании нескольких экземпляров окна, использующих общий обработчик сообщений, этот обработчик обычно не работает. Чтобы разные экземпляры окна могли использовать один общий класс окна, каждый экземпляр должен иметь собственную копию данных, доступ с которой осуществляется через общий обработчик сообщений. В классе KWindow эта проблема решена: мы создаем обработчик сообщений C++, который получает доступ к данным экземпляров. Функция KWindow: :CreateEx не передает указатель this непосредственно при вызове функции Win32 CreateWindowEx; вместо этого указатель передается в поле структуры MDICREATESTRUCT. Это необходимо для поддержки многодокументного интерфейса MDI (Multiple Document Interface) с использованием того же класса KWindow. Чтобы создать дочернее окно MDI, приложение посылает клиентскому окну MDI сообщение WM_MDICREATE и передает ему структуру MDICREATESTRUCT. Именно клиентское окно, реализуемое операционной системой, отвечает за итоговый вызов функции создания окна CreateWindowEx. Также следует учитывать, что функция CreateEx регистрирует класс окна и создает окно за один вызов. Каж-
Основы программирования для Windows на C/C++ 41 дый раз, когда требуется создать окно, функция проверяет, не был ли класс зарегистрирован ранее, и регистрирует класс только в случае необходимости. После создания класса KWindow нам уже не придется снова и снова решать задачи регистрации класса, создания окна и организации цикла сообщений — достаточно создать класс, производный от KWindow, и определить в нем только специфические аспекты. Ниже приведена третья версия программы «Hello, World» — вполне обычная программа C++, работающая в оконном режиме. // НеПоЗ.срр #define STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <assert.h> #include <tchar.h> #include ". Л. .\include\win.h" const TCHAR szMessage[] = JC'Hello. World !"); const TCHAR szFace[] = _T("Times New Roman"); const TCHAR szHint[] = _T("Press ESC to quit."); const TCHAR szProgram[] - _T("HelloWorld3"); // Функция CenterText копируется из Не11о2.срр class KHelloWindow : public KWindow { void OnKeyDown(WPARAM wParam, LPARAM IParam) { if ( wParam—VKJSCAPE ) PostMessage(m_hWnd. WM_CL0SE. 0. 0); } void OnDraw(HDC hDC) { TextOut(hDC, 0. 0. szHint, lstrlen(szHint)); CenterText(hDC. GetDeviceCaps(hDC. H0RZRES)/2. GetDeviceCaps(hDC. VERTRES)/2. szFace, szMessage. 72); } public: }: int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE, LPSTR lpCmd. int nShow) { KHelloWindow win; win.CreateEx(0, szProgram. szProgram, WSJWUP. 0. 0,
42 Глава 1. Основные принципы и понятия GetSystemMetrics( SM_CXSCREEN ). GetSystemMetrics( SM_CYSCREEN ). NULL, NULL, hlnst): win.ShowWi ndow(nShow); win.UpdateWindowO: return win.MessageLoopO; } В этой программе класс KHelloWindow создается как производный от класса KWindow. Виртуальная функция OnKeyDown переопределяется в нем для обработки клавиши Esc, а виртуальная функция OnDraw переопределяется для обработки сообщения WM_PAINT. Главная программа создает в стеке экземпляр класса KHelloWorld, строит полноэкранное окно, отображает его и входит в обычный цикл обработки сообщений. Где же наше сообщение «Hello, World»? Функция OnDraw выводит его в процессе обработки сообщения WM_PAINT. Итак, мы написали на C++ программу для Windows, в которой нет ни одной глобальной переменной. Hello, World, версия 4: вывод средствами DirectDraw Вторая и третья версии «Hello, World» напоминают старые DOS-программы, которые обычно захватывали весь экран и записывали данные прямо в видеопамять. Интерфейс DirectDraw, изначально разработанный компанией Microsoft для программирования быстрой графики в играх, позволяет программам работать на еще более низком уровне, обращаясь к экранному буферу и используя нетривиальные возможности современных видеоадаптеров. Ниже приведена простая программа, в которой вывод осуществляется средствами DirectDraw. // Hello4.cpp #define STRICT #define WIN32_LEAN_AND__MEAN #include <windows.h> #include <assert.h> #include <tchar.h> #include <ddraw.h> #include ". Л..\include\win.h" const TCHAR szMessage[] - _T("Hello. World !'*); const TCHAR szFace[] - _T("Times New Roman"); const TCHAR szHint[] - JCPress ESC to quit."); const TCHAR szProgram[] - J"("HelloWorld4"); // Функция CenterText копируется из Hello2.cpp class KDDrawWindow : public KWindow {
Основы программирования для Windows на C/C++ 43 LPDIRECTDRAW lpdd; LPDIRECTDRAWSURFACE lpddsprimary; void OnKeyDown(WPARAM wParam. LPARAM IParam) { if ( wParam—VKJSCAPE ) PostMessage(m_hWnd. WM_CL0SE. 0. 0); } void Blend(int left, int right, int top. int bottom); void OnDraw(HDC hDC) { TextOut(hDC. 0. 0. szHint. lstrlen(szHint)): CenterText(hDC. GetSystemMetri cs(SM_CXSCREEN) /2. GetSystemMetrics(SM_CYSCREEN)/2. szFace. szMessage. 48); Blend(80. 560. 160. 250); } public: KDDrawWindow(void) { lpdd - NULL; lpddsprimary - NULL; } -KDDrawWindow(void) { if ( lpddsprimary ) { lpddsprimary->Release(); lpddsprimary - NULL; } if ( lpdd ) { lpdd->Release(); lpdd - NULL; } } bool CreateSurface(void); bool KDDrawWindow::CreateSurface(void) { HRESULT hr; hr - DirectDrawCreate(NULL. &lpdd. NULL); if (hr!=DD_0K) return false;
44 Глава 1. Основные принципы и понятия hr = lpdd->SetCooperativeLevel(m_hWnd. DDSCLJULLSCREEN | DDSCLJXCLUSIVE); if (hr!=DD_0K) return false; hr = lpdd->SetDisplayMode(640. 480. 32); if (hr!=DD_0K) return false; DDSURFACEDESC ddsd; memset(& ddsd, 0, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; return lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL) ==DD_0K; } void inline Blend(unsigned char *dest. unsigned char *src) { dest[0] - (dest[0] + src[0])/2; dest[l] = (dest[l] + src[l])/2; dest[2] = (dest[2] + src[2])/2; void KDDrawWindow:;BlendCint left, int right, int top. int bottom) { DDSURFACEDESC ddsd; memset(&ddsd. 0. sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); HRESULT hr - lpddsprimary->Lock(NULL. &ddsd. DDLOCKJURFACEMEMORYPTR | DDLOCK_WAIT. NULL); assert(hr==DD_OK); unsigned char *screen - (unsigned char *) ddsd.IpSurface; for (int y=top; y<bottom; y++) { unsigned char * pixel - screen + у * ddsd.lPitch + left * 4; for (int x-left; x<right; x++. pixel+-4) if ( pixel[0]!=255 || pixel[1]'=255 || pixel[2]!-255 ) // He белый цвет { ::Blend(pixel-4. pixel); // Слева ::Blend(pixel+4, pixel); // Справа ;;Blend(pixel-ddsd.lPitch, pixel); // Сверху :;Blend(pixel+ddsd.lPitch, pixel); // Снизу
Основы программирования для Windows на C/C++ 45 } } lpddsprimary->Unlock(ddsd.lpSurface); } int WINAPI WinMainCHINSTANCE hlnst. HINSTANCE, LPSTR lpCmd. int nShow) { KDDrawWindow win: win.CreateEx(0, szProgram, szProgram, WSJWUP. 0. 0. GetSystemMetrics( SM_CXSCREEN ), GetSystemMetrics( SM_CYSCREEN ), NULL. NULL, hlnst); win.CreateSurfaceO; wi n.ShowWi ndow(nShow); win.UpdateWindowO; return win.MessageLoopO; } В этой простой программе с помощью DirectDraw организуется непосредственная запись в экранный буфер. Вероятно, вы заметили, что мы снова воспользовались классом KWi ndow, не добавив ни единой строки кода для создания окна, простейшей обработки сообщений и организации цикла сообщений. При работе с DirectDraw в каждом экземпляре класса KDDrawWindow, производного от KWi ndow, приходится хранить дополнительные данные, а именно указатель на объект DirectDraw и указатель на объект DirectDrawSurface; оба указателя инициализируются при вызове функции CreateSurface. Функция CreateSurface переключает экран в разрешение 640 х 480 с глубиной цвета 32 бит/пиксел и создает одну первичную поверхность DirectDraw. Интерфейсные указатели освобождаются при вызове деструктора. Функция OnDraw выводит небольшое справочное сообщение в левом верхнем углу и большое синее сообщение «Hello, World» в центре экрана; в обоих случаях, как и в предыдущем примере, используются обычные вызовы функций GDI. Впрочем, есть и отличия — после отображения текста вызывается новая функция Blend. В начале своей работы функция KDDrawWindow::Blend фиксирует в памяти экранный буфер и возвращает указатель, по которому можно напрямую работать с памятью экрана. До появления DirectDraw получить прямой доступ к экранному буферу при помощи функций GDI (и даже непосредственно в GDI) было невозможно, поскольку доступ находился под контролем драйверов графических устройств. В нашем примере используется режим с цветовой глубиной 32 бит/пиксел, поэтому каждый пиксел занимает в памяти 4 байта. Адрес пиксела в памяти вычисляется по следующей формуле: pixel = (unsigned char *) ddsd.lpSurface + у * ddsd.lPitch + left * 4;s
46 Глава 1. Основные принципы и понятия Функция сканирует прямоугольную область экрана сверху вниз и слева направо и ищет в ней пикселы, цвет которых отличен от белого (фон окрашен в белый цвет). При обнаружении не белого пиксела его цвет размывается по отношению к соседям, расположенным слева, справа, сверху и снизу. В результате размывания пикселу присваивается значение, равное среднему арифметическому значений двух пикселов. На рис. 1.1 изображен результат плавного размывания надписи «Hello, World!» на белом фоне. Hello, Wwta I Рис. 1.1. Размывание текста средствами DirectDraw Если вы еще никогда не работали с DirectDraw API, не огорчайтесь. Эта тема подробно рассматривается в главе 18. Ассемблер Все мы любим подниматься вверх — в социальном и даже в техническом смысле. Представители нашей профессии давно перешли от программирования в машинных кодах на C/Win32 API, C++/MFC, Java/AWT (Abstract Window Toolkit - классы для построения графического пользовательского интерфейса на Java)/ JFC (Java Foundation Classes — новая библиотека классов пользовательского интерфейса, превосходящая AWT по своим возможностям), и лишь некоторым невезучим личностям приходится создавать связи между абстрактными языками и машинным уровнем. Прогресс — вещь хорошая, печально другое. Делая очередной шаг вперед, мы быстро привыкаем к нему как к единственно возможному стандарту и забываем все, что было раньше. В наши дни уже никто не удивляется, когда книги по Visual C++ ограничиваются описанием MFC, а программисты спрашивают: «А как это сделать на MFC?» С каждым новым уровнем абстракции появляется новый промежуточный уровень взаимодействия программы с компьютером. Реализация нового уровня должна опираться на возможности более низких уровней — а самом низким уровнем в конечном счете является ассемблер. Даже если вы не принадлежите к узкому кругу системных программистов, глубокое понимание языка ассемблера обеспечит немалые преимущества в вашей профессиональной деятельности. При помощи ассемблера можно отлаживать программы и разбираться в принципах работы операционной системы (представьте, что у вас возникло исключение в kernel32.dll). Ассемблер поможет оптимизировать программу и добиться от нее максимального быстродействия. Ассемблер предоставляет в ваше распоряжение средства процессора, обычно недоступные в языках высокого уровня, — например, инструкции процессора, относящиеся к технологии Intel MMX (Multimedia Extensions).
Ассемблер 47 В этой книге будет рассматриваться ассемблер для процессоров Intel. Возможно, в будущих изданиях внимание будет уделено и другим процессорам. За основными сведениями о процессорах Intel обращайтесь к документу «Intel Architecture Software Developer's Manual», находящемуся на web-странице разработчиков (developer.intel.com). В дальнейшем предполагается, что вы имеете хотя бы базовое представление о процессорах семейства Intel и языке ассемблера. Обычно считается, что 16-разрядные программы работают в режиме 16-разрядной адресации, а 32-разрядные программы — в режиме 32-разрядной адресации. На процессорах Intel это неверно; и 16-, и 32-разрядные программы работают в 48-разрядном режиме логической адресации. При каждом обращении к памяти указывается 16-разрядный адрес сегмента и 32-разрядное смещение. Таким образом, логический адрес состоит из 48 бит. Процессоры Intel работают в 16- и 32-разрядном режимах. В 16-разрядном режиме максимальная длина сегмента равна 64 Кбайт, а в указателях на код и данные по умолчанию используются 16-разрядное смещение. В 32-разрядном режиме длина сегмента ограничивается значением 4 Гбайт, а в указателях на код и данные по умолчанию используется 32-разрядное смещение. Впрочем, разрядность инструкции можно изменить при помощи префикса (0x66 для операнда, 0x67 для адреса). Этот прием позволяет в 16-разрядном режиме работать с 32-разрядными регистрами, или наоборот, обращаться к 16-разрядным регистрам в 32-разрядном режиме. Режимы процессора Intel не следует путать с модулями EXE/DLL в мире Windows. Windows EXE/DLL может содержать комбинацию 16- и 32-разрядных модулей. Если вы работаете в Windows 95, загляните в файл dibeng.dll — эта 16-разрядная библиотека содержит 32-разрядные сегменты, чтобы обеспечить 32-разрядное быстродействие. Различия между 16- и 32-разрядным кодом существуют и в способе адресации. В 16-разрядных программах обычно используется сегментированная модель памяти, при которой адресное пространство делится на сегменты. Для 32-разрядных программ характерна сплошная (flat) адресация, при которой все адресное пространство рассматривается как один 4-гигабайтный сегмент. В процессах Win32 сегментные регистры процессора CS (Code Segment — сегмент кода), DS (Data Segment — сегмент данных), SS (Stack Segment — сегмент стека) и ES (Extra Segment — дополнительный сегмент) отображаются на один и тот же виртуальный адрес 0. Одним из преимуществ сплошной адресации является то, что мы можем легко сгенерировать фрагмент машинного кода в массиве данных и вызвать его как функцию. В программе Win 16 для этого пришлось бы отображать сегмент данных на сегмент кода, используя значение последнего и смещение для работы с кодом в сегменте данных. Поскольку все четыре основных сегментных регистра отображаются на одинаковый виртуальный адрес 0, программа Win32 обычно использует в качестве полного адреса только 32-разрядное смещение. Однако на уровне ассемблера сегментный регистр может комбинироваться со смещением для образования 48-разрядного адреса. Например, сегментный регистр FS, который также является регистром сегмента данных в процессорах Intel, не отображается на виртуальный адрес 0. Вместо этого он отображается на начальный адрес структуры данных программного потока (thread), поддерживаемой операционной системой; через эту структуру функции Win32 API работают с важной информацией уровня программного потока —
48 Глава 1. Основные принципы и понятия кодом последней ошибки (функции SetLastError/GetLastError), цепочкой обработчиков исключений, локальными данными потока и т. д. На ассемблерном уровне при вызове функций Win32 API используется стандартная схема передачи параметров, то есть параметры заносятся в стек справа налево. Следовательно, вызов функции окна из цикла обработки сообщений unsigned rslt = WindowProc(hWnd. uMsg. wParam. IParam); преобразуется в следующий фрагмент на ассемблере: mov push mov push mov push mov push call mov eax. eax eax, eax eax, eax eax, eax IParam wParam uMsg hWnd WindowProc rslt , eax Из возможностей процессоров Intel Pentium, недоступных на уровне C/C++, следует упомянуть одну инструкцию, которая представляет особый интерес для программистов, занятых оптимизацией своих программ. Речь идет об инструкции RDTSC (Read Time Stamp Counter). Эта инструкция возвращает количество тактов с момента запуска процессора в виде 64-разрядного целого без знака. Число возвращается в паре регистров общего назначения EDX и ЕАХ. Это означает, что на Pentium с частотой 200 МГц выполнение программы можно замерять с точностью до 5 не в течение 117 лет. На данный момент инструкция RDTSC не поддерживается в Visual C++ даже на уровне встроенного ассемблера, хотя оптимизатор, похоже, понимает, что при ее использовании изменяется содержимое регистров EDX и ЕАХ. Чтобы воспользоваться этой инструкцией, следует вставить в программу ее машинное представление OxOF, 0x31. Ниже приведен исходный текст класса-таймера, использующего инструкцию RDTSC. // Timer.h #pragma once inline unsigned _int64 GetCycleCount(void) { _asm _emit OxOF _asm _emit 0x31 } class KTimer { unsigned _int64 m_startcycle public: unsigned _int64 m_overhead;
Ассемблер 49 KTimer(void) { m_overhead = 0; StartO; m_overhead - StopO; } void Start(void) { m_startcycle - GetCycleCountO; } unsigned _int64 Stop(void) - { return GetCycleCount()-m_startcycle-m_overhead; } }: Класс KTimer хранит данные хронометража в виде 64-разрядного числа, поскольку 32-разрядная версия на компьютере с процессором в 200 мегагерц обеспечивает слишком низкую точность. Функция GetCycl eCount возвращает текущее количество тактов в виде 64-разрядного числа без знака. Результат, сгенерированный инструкцией RDTSC, соответствует формату 64-разрядного возвращаемого значения функций С. Таким образом, функция GetCycleCount представляет собой одну машинную инструкцию. Функция Start читает количество тактов в начале интервала; функция Stop останавливает хронометраж и возвращает разность. Чтобы повысить точность измерений, необходимо учесть время, потраченное на выполнение функций RDTSC. Для этого конструктор класса KTimer запускает и останавливает таймер. Полученная величина вычитается из результатов последующих измерений. В приведенном ниже примере класс KTimer используется для измерения количества тактов и времени, необходимого для создания однородной кисти. // GDISpeed.cpp #define STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <tchar.h> #include ".Л..\include\timer.h" int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE, LPSTR IpCmd. int nShow) { KTimer timer; TCHAR mess[128]; timer.StartO; Sleep(lOOO); unsigned cpuspeedlO = (unsigned)(timer.Stop()/100000); timer.StartO;
50 Глава 1. Основные принципы и понятия CreateSolidBrush(RGB(OxAA. OxAA. OxAA)); unsigned time - (unsigned) timer.StopO; wsprintf(mess, JTCPU speed *d.*d mhz\n") _T("«Timer overhead *d clock cycles\n") JTCreateSoUdBrush *d clock cycles *d ns"). cpuspeedlO / 10, cpuspeedlO % 10. (unsigned) timer.m_overhead, time, time * 10000~7 cpuspeedlO); MessageBox(NULL. mess, JC'How fast is GDI?"), MB_0K); return 0; } Результаты измерений выводятся в окне сообщения (рис. 1.2). Теперь можно с уверенностью сказать, что создание однородной кисти на компьютере с процессором Pentium 200 МГц занимает около 40 микросекунд. Рис. 1.2. Хронометраж с использованием инструкции RDTSC процессоров Intel Среда программирования Существует немало составляющих, необходимых для эффективного программирования в среде Microsoft Windows, — компьютеры, вспомогательные программы, книги, документация, обучающие курсы, информация в Интернете и опытные друзья. На первый взгляд этот список выглядит устрашающе, но для начала достаточно и малой части перечисленного. Остальное понадобится для программирования более сложных задач или повышения эффективности вашей работы. По сравнению со старыми средами программирования для DOS или 16-разрядных версий Windows, 32-разрядная среда Windows стала большим шагом вперед. Разработка и тестирование Компьютер, используемый для программирования в наши дни, должен отвечать следующим минимальным требованиям: Pentium 200 МГц с 64 Мбайт памяти, устройство для чтения компакт-дисков, 8 Гбайт свободного места на диске и сетевое подключение.
Среда программирования 51 Быстрый процессор ускоряет работу компилятора и компоновщика по превращению исходного текста в двоичный код (впрочем, у этого есть и отрицательная сторона — вы теряете хороший повод для того, чтобы отлучиться от компьютера и выпить кофе). Для использования новых инструкций (таких, как RDTSC) необходим процессор не ниже Intel Pentium или одной из совместимых моделей. Рекомендуется процессор Pentium Pro, Celeron, Pentium III или их аналоги с большим количеством инструкций, аппаратных оптимизаций и увеличенным кэшем. Если вы (или ваш начальник) можете себе это позволить, работайте на компьютере с двумя процессорами. Это позволит убедиться в том, что ваша программа не будет «тормозить» на двухпроцессорных компьютерах — например, из-за того, что разные потоки выделяют память из одной системной кучи (heap). Возможно, объем оперативной памяти является еще более важным фактором, чем скорость процессора. При работе на компьютере с 16 или 32 Мбайт памяти системная память будет все время переполнена, и процессору придется тратить такты на выгрузку неактивных и подкачку (swapping) активных страниц. В результате скорость работы система начинает зависеть от скорости обращения к диску. С возрастанием объема компиляторов, SDK (Software Development Kit) и DDK (Device Driver Kit) важную роль начинает играть свободное место на диске. Средства разработчика поставляются в комплекте с большими справочными файлами, заголовочными файлами и примерами программ. Компилятор расходует много места на построение предварительно откомпилированных заголовочных файлов, файлов с символической информацией для отладки и баз данных для просмотра объектов. Вы можете легко обратиться к какой-нибудь web-странице и загрузить с нее всевозможные утилиты или документацию. Кроме того, операционная система выгружает на жесткий диск содержимое оперативной памяти. Сетевое подключение необходимо для совместного использования и архивации данных. Для работы отладчика WinDbg, работающего на уровне ядра, необходимы два компьютера, соединенных последовательным или сетевым кабелем. Системы контроля версии и отслеживания дефектов обычно работают по сети. Для отладки драйверов уровня ядра в отладчике WinDbg вам понадобятся два компьютера; на одном будет работать отлаживаемая программа, а на другом — программа-отладчик. В главе 3 будет показано, как использовать DLL расширения отладчика WinDbg на одном компьютере для просмотра внутренних структур данных Windows NT/2000 GDI. На рабочем компьютере рекомендуется установить операционную систему Windows NT 4.0 или Windows 2000 (вместо Windows 95 или Windows 98). Платформы Microsoft Windows NT 4.0 и Windows 2000 все увереннее занимают лидирующее место среди операционных систем. По ним публикуется все больше книг, а существующий инструментарий постоянно расширяется. Кроме рабочего компьютера (или компьютеров), вам понадобятся компьютеры для тестирования (или по крайней мере временный доступ к ним). Вы пишете свои программы для пользователей Windows 95/98/NT/2000. Следовательно, вы должны убедиться в том, что они нормально работают в каждой из этих систем. Программы, для которых важно быстродействие, также необходимо протес-
52 Глава 1. Основные принципы и понятия тировать в разных конфигурациях. Бывает, что в результате оптимизации на процессоре Pentium II код начинает работать вдвое быстрее, но на Pentium трехлетней давности его работа замедляется. Компиляторы Рабочий компьютер — это еще не все. Вам также потребуется компилятор, преобразующий программу на языке программирования в машинное представление. Впрочем, современные компиляторы не ограничиваются этой функцией. Они представляют собой интегрированные среды программирования, в которые входит редактор, автоматически выделяющий синтаксические конструкции, заголовочные файлы, исходные тексты библиотек и двоичные файлы, справочную документацию, компилятор, компоновщик, отладчик, работающий на уровне исходных текстов, программы-мастера (wizards), компоненты и другие вспомогательные средства. Если вы программируете исключительно на уровне Win32 API, то в вашем распоряжении оказывается несколько вариантов: Microsoft Visual С-»--»-, Borland С-»--»-, Delphi, Microsoft Visual Basic и т. д. Но если вы хотите писать программы с использованием Windows NT/2000 DDK, особого выбора нет — вам потребуется компилятор Microsoft Visual С-»--»-. В настоящее время существует несколько версий компилятора Microsoft Visual С-»--»-. Обычно на рабочем компьютере устанавливается последняя версия компилятора с последней версией Service Pack — в них реализуются новые возможности и усовершенствования, а также исправляются найденные ошибки (впрочем, обновление иногда откладывается из-за возможных проблем с совместимостью). Примеры программ в этой книге откомпилированы в среде Microsoft Visual C++ версии 6.0. Если вы захотите воспользоваться более ранней версией, возможно, при загрузке файлов рабочей области и проектов будут выдаваться предупреждения. Наряду с операционной системой и компилятором вам также понадобятся некоторые вспомогательные средства, о которых обычно вспоминают лишь тогда, когда в них возникает необходимость. Символическая отладочная информация Windows NT/2000 помогает разобраться в том, как система работает и как она способствует обнаружению ошибок, проявляющихся только в системном коде. Наконец, бывает просто интересно узнать, какие символические имена используются в исходных текстах Microsoft. На компакт-диске VC6.0 в каталоге \vc60\vc98\debug находятся символические файлы для некоторых важных системных библиотек DLL — таких, как gdi32.dll, user32.dll и т. д. Полный набор символических отладочных файлов присутствует на компакт-диске Windows NT/2000 в каталоге \support\debug. В новых версиях Windows 2000 размер файлов с символической информацией увеличился настолько, что Microsoft пришлось разместить их на отдельном компакт-диске со вспомогательными инструментами. При установке символических файлов следует проверить тип процессора (i386 или alpha), тип и номер сборки. Они должны точно соответствовать параметрам той операционной системы, в которой производится отладка. Наличие символической информации для системных модулей является достаточно веской причиной для переноса разработок из
Среда программирования 53 Windows 95/98 в Windows NT/2000. После этого вы сможете чаще использовать в отладчике Visual C++ команду контекстного меню Go To Disassembly и не прогадаете. Например, можно установить точку прерывания при вызове Create- SolidBrush и перейти в режим ассемблера. Вместо неудобочитаемого шестнадца- теричного адреса отладчик Visual C++ покажет символическое имя CreateSolid- Brush@4. Становится видно, что функция получает 4 байта параметров, или просто одну 32-разрядную величину. Несколькими строками ниже находится вызов _CreateBrush@20; при вызове этой функции передаются 5 параметров. Таким образом, GDI объединяет вызовы специализированных функций API в более общие вызовы. Прослеживая цепочку вызовов, мы приходим к функции NtGdi Create- SolidBrush@8, вызывающей программное прерывание 0х2Е. Если вы захотите войти в обработчик прерывания, отладчик Visual C++ не позволит этого сделать. Итак, для создания однородной кисти GDI обращается к коду win32k.sys, работающему в режиме ядра. Чтобы разобраться в происходящем, совсем не нужно быть экспертом в области ассемблера — отладочные символические имена напоминают дорожные знаки, по которым вы ориентируетесь в пути. На рис. 1.3 показан пример использования отладочных символических файлов в отладчике Visual C++. *&ж Ш&ж,;. *ШМ _Crea t eSo1i dBrush@ 4: jj Ф 7 ?F4 2 0 % 9 ко г еа.х. ec<x 77F4205B push eax 7 7 F 4 2 0 5 С ризЬ eax 7 7 F 4 2 0 5 D pu-sh ea.x 77F4205E push dword ptr [esp+lOh] 77F42062 push eax * 77F42063 call CreateBrush»«20 (77f 41f ^b) 77F42068 ret 4 LNtGdiCreateSolidBrush@8: 77F42 0bB &ov «a.x , 1 02 kh 77F42070 lea edK.[esp+4] 77F42074 mt 2Eh 77F42076 ret 8 t JLwwimwwwMMwmwwwmJ ***** Я& Рис. 1.З. Использование символической информации в процессе отладки В компиляторах Microsoft существует два варианта хранения отладочной информации. В старом формате для каждого модуля создается один файл с расширением .dbg. В новом формате используются два файла с расширениями .dbg и .pdb. Ссылка на файл .dbg хранится в двоичном файле модуля. Например, gdi32.dll ссылается на файл dll\gdi32.dll в соответствующем отладочном каталоге. По этой ссылке отладчик загружает файл $SystemRoot$\symbols\dll\gdi32.dbg. Отладчик — не единственный инструмент, умеющий работать с символической информацией. Более того, он ничего не делает сам. Загрузка отладочной информации и обработка запросов осуществляется через специальный интер-
54 Глава 1. Основные принципы и понятия фейс (Image Help API) — простое модульное решение. Все программы, использующие этот интерфейс, смогут получить ту же информацию. К числу таких программ относится утилита dumpbin, но это может быть и ваша собственная программа. Утилита просмотра связей (depends.exe) перечисляет все DLL, импортируемые вашим модулем, в удобной рекурсивной форме. Это помогает получить четкое представление о том, сколько модулей загружается во время работы программы и сколько функций импортируется. Проверьте работу этой утилиты на простой программе MFC; вы будете поражены тем, сколько функций импортируется без вашего ведома. При помощи этой утилиты можно определить, будет ли программа работать в исходной версии Windows 95, которая не имела системных DLL типа urlmon.dll и экспортировала меньше функций в ole32.dll. Microsoft Spy++ (spyxx.exe) — удобная, мощная утилита для получения информации о работе Windows, о сообщениях, процессах и программных потоках. Если во время работы программы вы вдруг захотите узнать, из каких элементов состоит стандартное диалоговое окно (типа File Open) или почему не отправляется какое-нибудь сообщение, которое вы ожидаете, — попробуйте воспользоваться утилитой Microsoft Spy++. Простая и полезная утилита WinDiff (windiff.exe) позволяет сравнить две версии исходного файла или содержимое двух каталогов. Кроме того, она поможет найти различия между оригинальной и локализованной версиями ресурсных файлов. Крошечная текстовая программа pstat.exe выводит сведения о процессах, потоках и модулях, работающих в вашей системе. В ее выходных данных перечисляются все процессы и потоки с указанием времени, проведенным за выполнением кода в режиме пользователя и в режиме ядра, данных о рабочих наборах и счетчика ошибок страниц. В конце списка pstat перечисляет все системные DLL и драйверы устройств, загруженные в адресное пространство режима ядра; для каждого модуля указывается имя, адрес, размер кода и данных, а также строка версии. Обратите внимание, что реализация механизма GDI win32k.sys загружается по адресу ОхаООООООО, драйвер экрана загружается по адресу 0xfbef7000 и т. д. Утилита Process Viewer (pview.exe) выводит информацию о процессах в графическом виде. В частности, она показывает, сколько памяти выделено для каждого модуля в процессе. В инструментарий Visual Studio входят также другие полезные программы: dumpbin.exe для просмотра файлов в формате РЕ, profile.exe для простейшего измерения быстродействия, nmake.exe для компиляции с использованием make- файла, и rebase.exe для изменения базового адреса модуля (чтобы избежать затрат на его перемещение в памяти во время работы программы). Иногда вам придется просматривать заголовочные файлы, которые на первый взгляд кажутся скучными и непривлекательными. Кое-кто полагает, что заголовочные файлы создаются не для людей, а для компиляторов. Что ж, это действительно так. Но дело в том, что компьютер — очень точный и педантичный инструмент. Он беспрекословно подчиняется содержимому заголовочных файлов и ничего не знает о том, что говорится в документации или в книгах для программистов. Бывает, что документация содержит ошибки, и тогда за оконча-
Среда программирования 55 тельным ответом приходится обращаться к заголовочным файлам. Взгляните на определение TBBUTTON, приведенное в электронной документации. Эта структура состоит из 6 полей, не так ли? Но если вы определите структуру с 6 полями, компилятор выдаст сообщение об ошибке. А теперь загляните в заголовочный файл commctrl.h — оказывается, структура TBBUTTON содержит дополнительное двухбайтовое зарезервированное поле, то есть состоит из 7 полей. По заголовочным файлам можно проследить за тем, как макрос STRICT влияет на компиляцию, как версии функций API с поддержкой Unicode и без нее отображаются на экспортированные функции, сколько новых возможностей появилось в Windows 2000 и т. д. Если уж вы справитесь с заголовочными файлами, то читать исходные тексты будет намного интереснее. Чтение исходных текстов runtime-библиотеки С абсолютно необходимо — из них вы узнаете, как начинается и завершается работа вашего модуля и какие операции выполняются с памятью в системной куче при вызове malloc/free и new/delete. Кроме того, вы узнаете, почему в некоторых ситуациях встроенная версия memcpy работает медленнее, чем вызов внешней функции. Исходные тексты MFC довольно интересно читать и выполнять в пошаговом режиме. Особенно важна часть, связанная с обработкой сообщений, поскольку она объединяет C++ с пакетом Win32 SDK, ориентированным на С. Исходные тексты ATL (Active Template Library) сильно отличаются от исходных текстов MFC. Обязательно просмотрите класс CWndProcThunk, в котором переход от С к C++ осуществляется несколькими машинными инструкциями. В Microsoft Visual Studio поддерживается немало других полезных возможностей. Например, из меню File можно открыть HTML-страницу с цветовым выделением синтаксических элементов или открыть исполняемый файл для просмотра его ресурсов. Меню Edit позволяет производить поиск текста в разных режимах, а также устанавливать точки прерывания в определенных позициях программы, при обращениях к данным или получении сообщений. Команды меню Project позволяют сгенерировать листинг программы на ассемблере или подключить отладочную информацию к окончательной версии программы. При помощи команд меню Debug можно потребовать, чтобы программа прерывалась при возникновении определенных исключений, а также просмотреть список загруженных модулей. Microsoft Platform SDK Microsoft Platform SDK (Software Development Kit) представляет собой набор SDK для интеграции процесса разработки с существующими и развивающимися технологиями Microsoft. Этот пакет является наследником Win32 SDK и включает в себя BackOffice SDK, ActiveX/Internet Client SDK и DirectX SDK. Но самое ценное — то, что Microsoft Platform SDK распространяется бесплатно и регулярно обновляется. На момент написания книги Microsoft Platform SDK и другие SDK распространялись по адресу: http://msdn.microsoftxom/downloads/sdks/platform/platform.asp Итак, что же входит в SDK? Platform SDK содержит огромную подборку заголовочных файлов, библиотек, электронных документов, утилит и примеров
56 Глава 1. Основные принципы и понятия программ. Даже если у вас уже есть компилятор Microsoft Visual C++, установка последней версии Platform SDK все равно принесет пользу. Например, если к вашему компилятору прилагаются устаревшие версии заголовочных файлов и библиотек, вы не сможете пользоваться новыми функциями API, появившимися в Windows 2000. Проблема решается загрузкой Platform SDK и подключением новых заголовочных файлов и библиотек к компилятору. Можно сказать, что Microsoft Visual Studio — это интегрированный набор инструментов для разработки программ Win32 на базе компилятора Microsoft Visual C++, a Platform SDK — обширная коллекция инструментов для разработки программ Win32 с использованием внешнего компилятора C/C++. В Microsoft Visual C++ центральное место занимает компилятор C++ с runtime-библиотеками C/C++, ATL и MFC. В Platform SDK не входит ни компилятор, ни заголовочные файлы для функций C/C++, ни runtime-библиотеки. Это позволяет независимым фирмам работать с альтернативными компиляторами C/C++, заголовочными файлами и библиотеками, используя вместо решений Microsoft другую рабочую среду, — и даже программировать на Pascal, если вам удастся перевести заголовочные файлы Windows API в формат Pascal. Чтобы откомпилировать любую программу из Platform SDK, необходимо заранее установить компилятор и минимальный набор runtime-библиотек С. В Platform SDK входят десятки утилит, часть из которых присутствует и в Visual Studio. Программа qgrep.exe предназначена для поиска текста в режиме командной строки (по аналогии с командой Visual Studio Find in Files). Довольно мощная программа-монитор API (apimon.exe) работает как специализированный отладчик. Она загружает программу, перехватывает вызовы функций Windows API, регистрирует их с пометкой времени, а также трассирует параметры и возвращаемые значения. В непредвиденных ситуациях монитор API открывает окно DOS, в котором можно дизассемблировать программу, просмотреть содержимое регистров, установить точки прерывания и выполнить программу в пошаговом режиме. Программа memsnap.exe выдает сведения об использовании памяти работающими процессами, в частности о размере рабочего набора и об использовании памяти ядра. Утилита pwalk.exe выводит подробную информацию о расходовании процессом виртуальной памяти в пользовательском адресном пространстве. Программа показывает, каким образом пользовательское адресное пространство делится на сотни блоков, и сообщает основные параметры каждого блока (состояние, размер, имя секции и модуля). При двойном щелчке в строке списка выводится шестнадцатеричный дамп соответствующего блока. Программа sc (sc.exe) обеспечивает интерфейс командной строки для диспетчера служб (service control manager). Исключительно полезная программа просмотра объектов (winobj.exe) позволяет просматривать все активные объекты системы в виде иерархического дерева. К числу таких объектов относятся события, мыотексы, каналы, файлы, отображаемые на память, драйверы устройств и т. д. Например, в категории устройств присутствует строка PhysicalMemory — драйвер устройства для работы с физической памятью. Следовательно, для создания манипулятора блока физической памяти можно воспользоваться командой вида: HANDLE hRAM = CreateFileC'WW.WPhysicalMemory",...);
Среда программирования 57 Но самое интересное в Platform SDK — это, конечно, коллекция программ- примеров (если вас не пугает чтение старомодных Windows-программ, написанных на С). Вы не встретите в примерах C++, MFC, ATL или интенсивного применения runtime-функций С. Даже примеры СОМ и DirectX написаны на С, и вместо виртуальных функций C++ в них используются таблицы указателей на функции. Также в этом разделе приведены исходные тексты нескольких утилит SDK, в том числе windiff и spy. Читая эти программы, помните, что они были написаны в начале 90-х годов, причем большинство из них создавалось для Winl6 API. В этих примерах часто встречаются места, которые можно покритиковать за плохой стиль программирования — низкий уровень модульности кода, злоупотребление глобальными переменными, недостаточная проверка ошибок и явное влияние на Winl6 API. И все же из этих примеров можно вынести немало полезного. В табл. 1.1 перечислены некоторые программы, связанные с тематикой книги. Таблица 1.1. Примеры программ Platform SDK, относящиеся к графике Путь к программе Краткое описание программы \graphics\directx \graphics\gdi\complexscript \graphics\gdi\fonts \graphics\gdi\metafile \graphics\gdi\printer \graphics\gdi\setdisp \graphics\gdi\showdib \graphics\gdi\textfx \graphics\gdi\wincap32 \graphics\gdi\winnt\plgblt \graphics\gdi\winnt\wxform \graphics\gdi\video\palmap \sdktools\aniedit \sdktools\fontedit \sdktools\Jmage\drwatson \sdktools\imageedit \sdktools\winnt\walker Два мегабайта примеров DirectX Вывод сложных текстов на арабском, тайском и иврите Многосторонняя демонстрация шрифтовых функций API Загрузка, отображение, редактирование и печать расширенных метафайлов Функции печати, линии, перья, кисти Динамическое переключение разрешения экрана Обработка аппаратно-независимых растров Применение эффектов к тексту Сохранение экрана, перехватчики (hooks) Применение функции PlgBlt Демонстрация мировых преобразований Преобразование формата видеоданных (DIB) Редактор анимационных указателей мыши Редактор растровых шрифтов Программа DrWatson. Демонстрирует работу с символической таблицей, дизассемблирование, простую отладку, просмотр списка процессов, просмотр стека, создание аварийных дампов и т. д. Простой редактор растровых изображений Просмотр пространства виртуальной памяти процесса Продолжение &
58 Глава 1. Основные принципы и понятия Таблица 1.1. Продолжение Путь к программе Краткое описание программы \winbase\debug\deb Пример отладчика Win32 \winbase\debug\wdbgexts Пример расширения отладчика Win32 \winbase\winnt\service Функции API для работы со службами Ах, да! До сих пор не упомянут самый полезный инструмент Platform SDK — многооконный отладчик WinDbg, работающий на уровне исходных текстов. В отличие от встроенного отладчика Visual Studio, WinDbg может функционировать в Windows NT/2000 при отладке как пользовательских программ, так и кода, работающего в режиме ядра. Чтобы использовать его в качестве отладчика режима ядра, необходимо связать два компьютера последовательным или сетевым кабелем. Кроме того, WinDbg позволяет просматривать аварийные дампы. WinDbg более подробно рассматривается в главе 3. Если уж речь зашла об утилитах, обратите внимание на профессиональный инструментарий компании Numega. Программа BoundsChecker проверяет вызовы функций API, обнаруживая утечку памяти и ресурсов. Великолепный отладчик SoftICE/W обеспечивает отладку как в пользовательском режиме, так и в режиме ядра, поддерживает 16- и 32-разрядный код — и все это на одном компьютере. Он позволяет в пошаговом режиме перейти из кода пользовательского режима в код режима ядра, а потом вернуться обратно. Профайлер TrueTime предназначен для поиска секций кода, снижающих быстродействие программы. Наконец, vTune и компилятор C++ от компании Intel предназначены для тех, кто хочет добиться от программы максимального быстродействия и воспользоваться расширенными инструкциями Intel MMX и SIMD (Single Instruction Multiple Data). Microsoft Driver Development Kit Пакеты Microsoft Visual C++ и Platform SDK ориентируются на написание обычных программ пользовательского уровня — таких, как WordPad или даже Microsoft Word. Однако для работы операционной системы (особенно при большом количестве устройств — жестких дисков, видеоадаптеров, принтеров и т. д.) необходимы программы другого типа — драйверы устройств. Драйверы устройств загружаются в адресное пространство ядра. Вместе с функциями Win32 API становятся недоступными и структуры данных Win32 API. Они заменяются вызовами системных функций ядра и интерфейсами драйверов устройств. Для написания драйверов устройств в Windows необходим пакет Microsoft Driver Development Kit, бесплатно распространяемый компанией Microsoft (существуют DDK для Windows 95/98/NT4.0/2000): http://www.microsoft.com/ddk/ DDK, как и Platform SDK, представляет собой огромный набор заголовочных и библиотечных файлов, электронной документации, утилит и примеров программ. В DDK входят заголовочные файлы как для функций Win32 API, так и для драйверов устройств. Например, в файле wingdi.h документируются
Среда программирования 59 функции Win32 GDI API, а в файле winddi.h — интерфейс между механизмом GDI и драйверами экрана или принтера. В примерах DirectDraw файл ddraw.h документирует функции DirectDraw API, а файл ddrawint.h определяет интерфейс драйвера DirectDraw в Windows NT. Библиотечные файлы делятся по типу сборки на две категории: свободные (free) и проверяемые (checked). В DDK также входят библиотеки импортируемых функций для системных DLL ядра — например, win32k.lib для win32k.sys. В каталоге help находятся подробные спецификации интерфейсов драйверов устройств и рекомендации по разработке драйверов. Несомненно, каталоги с исходными текстами примеров имеют особую ценность. Например, 'каталог \src\video\displays\s3virge содержит свыше 2 Мбайт исходных текстов драйверов s3 VirGE для поддержки GDI, DirectDraw и трехмерной графики. ПРИМЕЧАНИЕ Разработка драйверов устройств не относится к теме этой книги — на рынке уже есть несколько хороших книг, посвященных разработке драйверов. Но в этих книгах основное внимание обычно уделяется драйверам общего назначения — таким, как драйверы ввода-вывода и драйверы файловой системы. В этой книге подробно рассматриваются вопросы программирования графики в Windows. Мы разберемся с тем, как механизм GDI реализует вызовы функций GDI/DirectDraw и в конечном счете передает их драйверам устройств. Следовательно, к нашей теме относятся драйверы экрана (включая поддержку DirectDraw), шрифтовые драйверы и драйверы принтеров. Кроме того, драйверы режима ядра позволяют обойти API пользовательского режима и сделать что-то такое, что не делается средствами Win32 API. В главе 3 показано, как простой драйвер режима ядра помогает анализировать работу механизма GDI. Исполняемый код в DDK, как и в Platform SDK, строится при помощи внешнего компилятора С. В DDK включена утилита построения проектов (build.exe), упрощающая процесс построения драйверов. Она строит целую иерархию исходных текстов для разных платформ. Вероятно, при наличии исходных текстов build.exe сможет построить целую операционную систему в режиме командной строки. При построении драйверов устройств используются особые параметры компилятора и компоновщика, не поддерживаемые в Microsoft Visual C++. Таким образом, драйверы устройств удобнее всего строить в режиме командной строки. В DDK входят и другие утилиты. Программа break.exe, работающая в режиме командной строки, подключает отладчик к процессу. Мастер настройки отладчика (dbgwiz) помогает настраивать WinDbg. Утилита gflags позволяет изменить значения десятков системных флагов. Например, вы можете включить режим пометки выделяемых блоков памяти данными владельца, чтобы обнаруживать утечку памяти. Программа poolmon.exe следит за выделением/освобождением памяти ядра. Программа regdmp выводит содержимое реестра в текстовый файл. Microsoft Developer Network Microsoft Developer Network (MSDN) — огромный архив справочной информации для программистов Microsoft Windows. MSDN содержит несколько гигабайт документации, технических статей, примеров программ, статей из журналов, книг,
60 Глава 1. Основные принципы и понятия спецификаций и вообще всего, что может понадобиться при программировании для Microsoft Windows, в том числе документацию для Platform SDK, DDK, Visual C++, Visual Studio, Visual Basic, Visual J++ и т. д. * MSDN содержит практически все, что (по мнению Microsoft) необходимо знать при программировании для Windows. Новые версии Microsoft Visual Studio используют MSDN в качестве справочной системы, что сопряжено с немалыми затратами дискового пространства. В табл. 1.2 перечислены компоненты MSDN, относящиеся к программированию графики в Windows. Таблица 1.2. Основные компоненты MSDN, относящиеся к программированию графики Platform SDK\Graphics and Multimedia Services\Microsoft DirectX Platform SDK\Graphics and Multimedia Services\GDI DDK Documentation\Windows 2000 DDK\Graphics Drivers Время от времени вам будут встречаться ссылки на материалы MSDN. Если вы читаете эту книгу без доступа к MSDN, желательно распечатать содержимое перечисленных секций. Помимо трех больших блоков документации SDK/DDK, MSDN содержит немало полезной информации по программированию графики в Windows в виде технических статей, статей Knowledge Base, спецификаций и т. д. При чтении этих материалов необходимо обращать внимание на дату написания и платформу, для которой они были написаны, поскольку полезная информация хранится вперемежку с устаревшим хламом. Частичный список статей приведен в табл. 1.3. Таблица 1.3. Дополнительные компоненты MSDN, относящиеся к программированию графики Specifications\Applications\True Type Font Specification Specifications\Platforms\Microsoft Portable Executable and Common Object Form Specification Specifications\Technologies and Languages\The UniCode Standard, Version 1.1 Technical Articles\Multimedia\Basics of DirectDraw Game Programming Technical Articles\Multimedia\Getting Started with Direct3D:A tour and Resource Guide Technical Articles\Multimedia\Texture Wrapping Simplified Technical Articles\Multimedia\GDI\*.* (десятки полезных статей) Technical Articles\Windows Platform\Memory\Give Me a Handle, and I'll Show You an Object Technical Articles\Windows Platform\Windows Management\Windows Classes in Win32 Backgrounders\Windows Platform\Base\The Foundations of Microsoft Windows NT System Architecture
Формат исполняемых файлов Win32 6i Формат исполняемых файлов Win32 Вероятно, многие вспомнят фразу «Алгоритмы + структуры данных = Программы», приписываемую Н. Вирту (N. Wirtch) — отцу семейства языков Pascal, современным представителем которого является Delphi. Однако откомпилированный двоичный код сам по себе является структурой данных, содержимое которой обрабатывается системой при загрузке программы в память для исполнения. На платформах Win32 эта структура данных называется форматом «Portable Executable», или сокращенно РЕ. Знание файлового формата РЕ заметно упрощает программирование для Windows. Это знание дает возможность понять, каким образом исходный текст превращается в двоичный код, где хранятся глобальные переменные и как они инициализируются, как работают общие переменные и т. д. Все DLL в системе Win32 имеют формат РЕ; следовательно, зная формат РЕ, вы лучше поймете, как работает механизм динамической компоновки, как происходит разрешение ссылок при импортировании и как избежать динамической смены базового адреса DLL. Методика перехвата функций API в существенной степени основывается на знании структуры таблицы импортируемых функций. Наконец, знание формата РЕ позволяет лучше понять структуру пространства виртуальной памяти в среде Win32. В этой книге есть несколько мест, в которых пригодится знание файлового формата РЕ, поэтому мы кратко рассмотрим сам этот формат и его форму после загрузки в память. Программисты пишут исходные тексты программ на С, C++, ассемблере или других языках. Эти исходные тексты затем транслируются компилятором в объектные файлы в формате OBJ. Каждый объектный файл содержит глобальные переменные (инициализированные или неинициализированные), неизменяемые данные, ресурсы, исполняемый код на машинном языке, символические имена для компоновки и отладочную информацию. Объектные файлы модуля связываются компоновщиком с библиотеками, которые сами представляют собой объединение объектных файлов. Наиболее распространенными являются runtime-библиотеки С и C++, библиотеки MFC/ATL, библиотеки импортируемых функций Win32 API или системных функций ядра Windows. Компоновщик разрешает все взаимные ссылки между объектными ссылками и библиотеками. Например, если в вашей программе вызывается библиотечная функция C++ new, то компоновщик находит адрес new в runtime-библиотеке C++ и заносит его в программу. После этого компоновщик объединяет все инициализированные глобальные переменные в одну секцию, все неинициализированные глобальные переменные — в другую секцию, весь исполняемый код — в третью секцию и т. д. Группировка разных частей объектных файлов по разным секциям выполняется по двум причинам: защита и оптимальное использование ресурсов. Неизменяемые данные и исполняемый код обычно объявляются доступными только для чтения. Это помогает программисту находить ошибки в программе, если операционная система обнаруживает попытку записи в соответствующую область памяти. Установка атрибута доступа «только для чтения» осуществляется компоновщиком. Конечно, секции глобальных переменных (инициализированных и неинициализированных) должны быть доступны как для чтения, так и для записи. В коде операционной системы Windows широко используются DLL;
62 Глава 1. Основные принципы и понятия например, для всех программ Win32 с графическим интерфейсом пользователя требуется файл gdi32.dll. С целью оптимального использования памяти секция исполняемого кода gdi32.dll хранится в памяти лишь в одном экземпляре на всю систему. Разные процессы работают с кодом DLL через файл, отображаемый на память. Это возможно благодаря тому, что исполняемый код доступен только для чтения, а значит, для всех процессов он будет одинаковым. Глобальные данные не могут совместно использоваться разными процессами, если только они не были специально помечены как общие. С каждой секцией связывается символическое имя, по которому на нее можно ссылаться в параметрах компоновщика. Код или данные, принадлежащие одной секции, обладают одинаковыми атрибутами. Память для секций выделяется постранично, поэтому на процессорах Intel размер минимального блока памяти, выделяемого для секции, равен 4 Кбайт. Некоторые часто используемые секции перечислены в табл. 1.4. Таблица 1.4. Часто используемые секции РЕ-файлов Имя Содержимое Атрибуты .text Исполняемый код .data Инициализированные глобальные данные .rsrc Ресурсы .bss Неинициализированные глобальные данные .rdata Неизменные данные .idata Каталог импорта .edata Каталог экспорта .reloc Таблица настройки адресов .shared Общие данные Код, исполнение, чтение Инициализированные данные, чтение/запись Инициализированные данные, только для чтения Чтение/запись Инициализированные данные, только для чтения Инициализированные данные, чтение/запись Инициализированные данные, только для чтения Инициализированные данные, удаляемая (discardable) память, только для чтения Инициализированные данные, общая память, чтение/запись Исполняемый код и глобальные данные в РЕ-файлах практически не структурируются — никто не хочет помогать хакерам взламывать свои программы. Но в остальных данных операционной системы время от времени приходится выполнять поиск. Например, при загрузке модуля загрузчик должен провести поиск в таблице импортируемых функций и настроить значения адресов; когда пользователь вызывает GetProcAddress, поиск производится в таблице экспорти-
Формат исполняемых файлов Win32 63 руемых функций. В РЕ-файлах для таких целей резервируется 16 специальных таблиц, называемых каталогами (directories). Чаще всего используются каталоги импорта, связанного импорта (bound import), отложенного импорта (delayed import), экспорта, настройки адресов (relocation), ресурсов и отладочной информации. Объединяем секции и каталоги, добавляем пару заголовков со служебной информацией — и получаем РЕ-файл (рис. 1.4). IMAGE DOS HEADER Заглушка DOS Сигнатура РЕ-файла £о: IMAGE FILE HEADER !Ш ш'§ <Х IMAGE_OPTIONAL_ ^ HEADER R32 Таблица секций IMAGE_SECTION_HEADER Q Секция .text (двоичный код) Секция .data (инициализированные данные) Секция .reloc (таблица настройки адресов) Секция .rsrc (константы) Секция .rdata (ресурсы) Рис. 1.4. Структура файлов формата Portable Executable РЕ-файл начинается с заголовка ЕХЕ-файла DOS (структура IMAGEDOSHEADER), потому что Microsoft хочет, чтобы программы Win32 можно было запускать в сеансе DOS. Непосредственно за IMAGEDOSHEADER следует заглушка (stub) — крошечная DOS-программа, которая генерирует программное прерывание для вывода сообщения об ошибке и завершает работу программы. После заглушки следует настоящий заголовок РЕ-файла (IMAGENTHEADERS). Обратите внимание: длина программы-заглушки не фиксируется, поэтому для определения смещения структуры IMAGENTHEADERS следует использовать значение поля ejfanew структуры IMAGE_DOS_HEADER. Структура IMAGE_NT_HEADERS начинается с 4-байтовой сигнатуры, которая должна быть равна IMAGENTSIGNATURE1. 1 Макрос, определяемый в winnt.h. — Примеч. перев.
64 Глава 1. Основные принципы и понятия В противном случае это может быть файл OS/2 или VxD. Структура IMAGE_FILE_ HEADER содержит идентификатор целевого процессора, количество секций в файле, время сборки, указатель на таблицу символических имен и размер «необязательного» заголовка. Несмотря на свое название, структура IMAGEOPTIONALHEADER не является необязательной (optional). Она встречается в каждом РЕ-файле, поскольку хранящаяся в ней информация слишком важна. В этой структуре хранится рекомендуемый базовый адрес модуля, размеры кода и данных, базовые адреса кода и данных, конфигурация кучи и стека, требования к версии ОС и подсистемы, а также таблица каталогов. РЕ-файл содержит множество адресов для ссылок на функции, переменные, имена, таблицы и т. д. Некоторые из них хранятся в виде виртуальных адресов, которые могут напрямую использоваться после загрузки модуля в память. Если модуль не удается загрузить по рекомендуемому базовому адресу, загрузчик исправляет данные в соответствии с фактическим адресом. Однако большинство адресов задается по отношению к началу заголовка РЕ-файла. Такие адреса называются «относительными виртуальными адресами» (relative virtual addresses, RVA). Обратите внимание: значение RVA не совпадает со смещением в РЕ- файле перед его загрузкой в память. Дело в том, что в РЕ-файлах секции обычно выравниваются по 32-разрядным границам, а операционная система использует выравнивание по страницам. Для процессоров Intel размер страницы равен 4096 байт. Адреса RVA вычисляются в предположении, что секции выравниваются по страницам — это уменьшает затраты ресурсов во время выполнения программы. Ниже приведен простой класс C++ для выполнения несложных операций с модулями Win32, загруженными в память. Конструктор показывает, как получить указатели на структуры IMAGEDOSHEADER и IMAGENTHEADER. В функции GetDi rectory продемонстрировано получение указателя на данные каталога. Мы усовершенствуем этот класс, чтобы он приносил практическую пользу. class KPEFile { const char * pModule; PIMAGE_DOS_HEADER pDOSHeader; PIMAGE_NT_HEADERS pNTHeader; public: const char * RVA2Ptr(unsigned rva) { if ( (pModule!=NULL) && rva) return pModule + rva; else return NULL: } KPEFile(HMODULE hModule): const void * GetDirectory(int id): PIMAGE_IMPORT_DESCRIPTOR GetImportDescriptor(LPCSTR pDHName);
Формат исполняемых файлов Win32 65 const unsigned * GetFunctionPtr(PIMAGE_IMPORT_DESCRIPTOR plmport. LPCSTR pProcName); FARPROC SetlmportAddressCLPCSTR pDHName, LPCSTR pProcName. FARPROC pNewProc); FARPROC SetExportAddressCLPCSTR pProcName. FARTPROC pNewProc); }: KPEFile::KPEFileCHMODULE hModule) { pModule = (const char *) hModule; if ( IsBadReadPtr(pModule, sizeof(IMAGE_DOS_HEADER)) ) { pDOSHeader = NULL; pNTHeader = NULL; } else { pDOSHeader = (PIMAGE_DOS_HEADER) pModule; if ( IsBadReadPtr(RVA2Ptr(pD0SHeader->e_lfanew), sizeof(IMAGE_NT_HEADERS)) ) pNTHeader = NULL: else pNTHeader = (PIMAGE_NT_HEADERS) RVA2Ptr(pD0SHeader-> ejfanew); } } // Функция возвращает адрес каталога РЕ const void * KPEFile::GetDirectory(int id) { return RVA2Ptr(pNTHeader->0ptionalHeader.DataDirectory[id]. Virtual Address); } Получив общее концептуальное представление о файловом формате РЕ, давайте рассмотрим несколько практических примеров. Каталог импорта При использовании в программе функции Win32 API (например, LoadLibraryW) генерируется двоичный код следующего вида: DWORD imp LoadLibrary@4 = 0х77Е971С9: call dword ptr[__imp_LoadLibraryW@4] Обратите внимание на любопытную подробность: компилятор создает внутреннюю глобальную переменную и использует косвенный вызов вместо прямого. Впрочем, для этого у компилятора есть довольно веские причины. Компоновщик не знает точного адреса LoadLibraryW@4 на стадии компоновки, хотя он может сделать предположение на основании одной версии kernel32.dll (указан-
66 Глава 1. Основные принципы и понятия ной в каталоге связанного импорта). Следовательно, в большинстве случаев загрузчик модуля должен найти правильный адрес импортируемой функции и внести исправления в загружаемый образ модуля. Одна и та же функция (такая, как LoadLibraryW) может вызываться в модуле многократно. По соображениям быстродействия загрузчик предпочел бы вносить исправления в минимальном количестве мест, в идеальном случае — в одном месте на каждую импортируемую функцию. Таким местом является переменная, содержащая адрес импортируемой функции. Обычно подобным переменным присваиваются внутренние имена вида imp xxx. Адреса импортируемых функций либо выделяются в отдельную секцию (как правило, ей присваивается имя .idata), либо объединяются с секцией . text для экономии места. Каждый модуль обычно импортирует по несколько функций из разных модулей. В РЕ-файле каталог импорта ссылается на массив структур IMAGE_IMPORT_ DESCRIPTOR, каждая из которых соответствует одному импортируемому модулю. Первое поле IMAGEIMPORTDESCRIPT0R содержит смещение в таблице хинтов/имен, а последнее поле содержит смещение в таблице импортируемых адресов. Две таблицы имеют одинаковую длину, а каждый элемент соответствует одной импортируемой функции. Элемент таблицы импортируемых адресов содержит порядковый номер, если установлен старший бит (импортирование по порядковому номеру), или смещение 16-разрядного хинта, за которым следует имя импортируемой функции (импортирование по имени). Таким образом, таблица хинтов/имен может использоваться для поиска в каталоге экспорта того модуля, из которого мы импортируем. В исходном РЕ-файле таблица импортируемых адресов может содержать ту же информацию, что и таблица хинтов/имен — то есть смещение хинта, за которым следует имя функции. В этом случае загрузчик находит адрес импортируемой функции и модифицирует элемент таблицы импортируемых адресов. Следовательно, после загрузки РЕ-файла таблица импортируемых адресов в действительности превращается в таблицу адресов импортируемых функций. Компоновщик также может связать модуль с некоторой библиотекой DLL, чтобы таблица инициализировалась адресами импортируемых функций для определенной версии DLL. В последнем случае таблица импортируемых адресов содержит адреса связанных импортируемых функций. В обоих случаях таблица импортируемых функций содержит внутренние переменные вида imp LoadLibrary@4. Давайте попробуем реализовать функцию KPEFile: :SetImportAddress. Эта функция изменяет адрес импортируемой функции в модуле и возвращает первоначальное значение адреса. // Функция возвращает значение поля PIMAGE_IMPORT_DESCRIPTOR // для импортируемого модуля PIMAGEJMPORT_DESCRIPTOR KPEFile: :GetImportDescriptor( LPCSTR pDllName) { // Получить IMAGE_IMPORT_DESCRIPTOR PIMAGE_IMPORT_DESCRIPTOR plmport = (PIMAGE_IMPORT_DESCRIPTOR) GetDi rectory (IMAGE JIRECTORYJNTRYJMPORT); if ( pImport==NULL )
Формат исполняемых файлов Win32 67 return NULL; while ( pImport->FirstThunk ) { if ( StricmpCpDllName. RVA2Ptr(pImport->Name))==0 ) return pimport; // Перейти к следующему импортируемому модулю pimport ++; } return NULL; // Функция возвращает адрес переменной imp_xxx // для импортируемой функции const unsigned * KPEFile::GetFunctionPtr( PIMAGE_IMPORT_DESCRIPTOR pimport, LPCSTR pProcName) { PIMAGE_THUNK_DATA pThunk; pThunk = (PIMAGE_THUNK_DATA) RVA2Ptr(pImport-> OriginalFirstThunk); for (int i=0: pThunk->ul.Function; i++) { bool match; // По порядковому номеру if ( pThunk->ul.Ordinal & 0x80000000 ) match = (pThunk->ul.Ordinal & OxFFFF) == ((DWORD) pProcName); else match = stricmp(pProcName, RVA2Ptr((unsigned) pThunk->ul.Address0fData)+2) == 0; if ( match ) return (unsigned *) RVA2Ptr(pImport->FirstThunk)+i; pThunk ++; } return NULL; } FARPROC KPEFile::SetImportAddress(LPCSTR pDllName, LPCSTR pProcName. FARPROC pNewProc) { PIMAGE_IMPORT_DESCRIPTOR pimport = GetlmportDescriptor(pDllName); if ( pimport ) { const unsigned * pfn = GetFunctionPtr(pImport. pProcName);
68 Глава 1. Основные принципы и понятия if ( IsBadReadPtr(pfn, sizeof(DWORD)) ) return NULL; // Получить исходный адрес функции FARPROC oldproc = (FARPROC) * pfn; DWORD dwWritten; // Заменить новым адресом функции HackWriteProcessMemory(GetCurrentProcess(). (void*) pfn, & pNewProc, sizeof(DWORD). & dwWritten); return oldproc; } else return NULL; } В работе SetlmportAddress используются две вспомогательные функции. Функция GetlmportDescriptor просматривает каталог импорта и ищет в нем структуру IMAGEIMPORTDESCRIPTOR для того модуля, из которого импортируется функция. Структура передается функции GetFunctionPtr, которая просматривает таблицу хинтов/имен и возвращает адрес соответствующего элемента в таблице импортируемых адресов. Например, если импортируется функция MessageBoxA из user32.dll, то функция GetFunctionPtr должна вернуть адрес imp MessageBoxA. Наконец, функция SetlmportAddress читает исходный адрес функции и заменяет его новым адресом при помощи функции WriteProcessMemory. После вызова SetlmportAddress все вызовы указанной импортируемой функции из модуля будут передаваться новой функции. Таким образом, функция SetlmportAddress позволяет организовать перехват (hooking) вызовов функций API. Ниже приведен простой пример использования класса KPEFile для перехвата вывода окна сообщения: int WINAPI MyMessageBoxA(HWND hWnd. LPCSTR pText. LPCSTR pCaption, UI NT uType) { WCHAR wText[MAX_PATH]; WCHAR wCaption[MAX_PATH]; MultiByteToWideChar(CP_ACP, MB_PREC0MP0SED. pText. -1. wText. MAX_PATH); wcscat(wText. L" - intercepted"): MultiByteToWideChar(CP_ACP. MB_PREC0MP0SED, pCaption. -1. wCaption. MAX_PATH); wcscat(wCaption, L" - intercepted"); return MessageBoxW(hWnd, wText. wCaption, uType); } int WINAPI WinMain(HINSTANCE hlnstance. HINSTANCE. LPSTR, int) { KPEFile pe(hlnstance):
Формат исполняемых файлов Win32 69 ре.SetImportAddress("user32.dl1". "MessageBoxA". (FARPROC) MyMessageBoxA); MessageBoxA(NULL. "Test". "SetlmportAddress". MB_0K); } Программа заменяет импортируемый адрес MessageBoxA в текущем модуле адресом функции MyMessageBoxA, реализованной нашим приложением, после чего все вызовы MessageBoxA поступают в MyMessageBoxA. В нашем примере эта функция добавляет в текст и заголовок дополнительное слово «intercepted» («перехвачено») и отображает окно сообщения функцией MessageBoxW. Каталог экспорта Чтобы ваша программа могла импортировать функцию/переменную из системной библиотеке DLL, эта функция/переменная должна быть соответствующим образом экспортирована. Для экспортирования функции/переменной из DLL РЕ-файл должен содержать три объекта данных — порядковый номер, адрес и необязательное имя. Вся информация, относящаяся к экспортируемым функциям, объединяется в структуру I MAGE_EXPORT_D I RECTORY, к которой можно обратиться через каталог экспорта в заголовке РЕ-файла. Хотя экспортироваться могут как функции, так и переменные, обычно экспортируются только функции. По этой причине даже в названиях полей в структурах РЕ-файлов упоминаются только функции. Структура IMAGEEXPORTDI RECTORY содержит информацию о количестве экспортируемых функций и количестве имен, которое может быть меньше общего количества функций. Большинство DLL экспортирует функции по имени. В некоторых DLL (например, comctl32.dll) одни функции экспортируются по имени, а другие — по порядковому номеру. Некоторые DLL (например, MFC DLL) экспортируют тысячи функций, поэтому для экономии места, занимаемого именами, все функции экспортируются по порядковому номеру. Библиотеки COM DLL экспортируют фиксированное количество хорошо известных функций (например, DIlRegisterServer) с одновременным предоставлением служебных интерфейсов или таблиц виртуальных функций. Некоторые DLL вообще ничего не экспортируют — в них используется только точка входа в DLL. Более интересная информация в I MAGEEXPORTD I RECTORY включает RVA таблицы адресов функций, таблицы имен функций и таблицы порядковых номеров функций. Таблица адресов содержит RVA всех экспортируемых функций. Таблица имен содержит RVA строк с именами функций, а таблица порядковых номеров содержит разности между реальным и базовым порядковыми номерами. Зная структуру таблицы экспорта, можно легко реализовать функцию Get- ProcAddress. Однако такая реализация уже существует в Win32 API (к сожалению, она не имеет Unicode-версии). Вместо этого давайте попробуем реализовать функцию KPEFile::SetExportAddress. Как было показано выше, функция SetlmportAddress модифицирует таблицу импорта модуля и изменяет адрес одной импортируемой функции в одном модуле. На другие модули процесса (в том числе и модули, загруженные процессом позднее) эти изменения не распространяются. Функция SetExportAddress
70 Глава 1. Основные принципы и понятия работает иначе. Она модифицирует таблицу экспорта модуля и поэтому влияет на все экземпляры экспортируемой функции в будущем. Ниже приведен код функции SetExportAddress. FARPROC KPEFiIe::SetExportAddress(LPCSTR pProcName. FARPROC pNewProc) { PIMAGE_EXP0RT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY) GetDi rectory (IMAGEJHRECTORYJNTRYJXPORT); if ( pExport==NULL ) return NULL; unsigned ord = 0; if ( (unsigned) pProcName < OxFFFF ) // По порядковому номеру? ord = (unsigned) pProcName; else { const DWORD * pNames - (const DWORD *) RVA2Ptr(pExport->Address0fNames); const WORD * pOrds - (const WORD *) RVA2Ptr(pExport->Address0fName0rdinals); // Найти элемент с именем функции for (unsigned i=0; i<pExport->AddressOfNames; i++) if ( stricmp(pProcName. RVA2Ptr(pNames[i]))==0 ) { // Получить соответствующий порядковый номер ord = pExport->Base + pOrds[i]; break; if ( (ord<pExport->Base) || (ord>pExport->NumberOfFunctions) ) return NULL; // Использовать порядковый номер для получения адреса. // по которому хранится RVA экспортируемой функции DWORD * pRVA - (DWORD *) RVA2Ptr(pExport->Address0fFunctions) + ord - pExport->Base; // Прочитать исходный адрес функции DWORD rslt - * pRVA; DWORD dwWritten = 0; DWORD newRVA = (DWORD) pNewProc - (DWORD) pModule; WriteProcessMemory(GetCurrentProcess(). pRVA. & newRVA. sizeof(DWORD), & dwWritten); return (FARPROC) RVA2Ptr(rslt); } Функция SetExportAddress сначала пытается найти порядковый номер заданной функции. Если порядковый номер не указан, имя функции ищется в табли-
Архитектура операционной системы Microsoft Windows 71 це имен функций. Индексирование таблицы адресов функций по порядковому номеру дает адрес, по которому хранится RVA экспортируемой функции. Затем SetExportAddress читает исходный RVA и заменяет его новым, вычисленным по новому адресу функции. В результате модификации таблицы экспорта после вызова SetExportAddress функция GetProcAddress будет возвращать адрес новой функции. При будущих загрузках DLL процессом компоновка будет осуществляться с новой функцией. Ни SetlmportAddress, ни SetExportAddress по отдельности не обеспечивают полного перехвата вызовов API процессом, однако совместное использование обеих функций в значительной степени решает эту задачу. Идея проста: мы перебираем все модули, загруженные процессом в настоящий момент, и вызываем SetlmportAddress для каждого из них. Затем вызывается функция SetExportAddress, модифицирующая таблицу экспорта. В этом случае модификация распространяется как на модули, загруженные в настоящий момент, так и на модули, которые будут загружены в будущем. На этом наше краткое знакомство с файловым форматом РЕ подходит к концу. Материал этого раздела будет использоваться при изучении виртуального пользовательского пространства в главе 3 и перехвате/отслеживании вызовов API в главе 4. Если вас действительно интересуют РЕ-файлы и отслеживание API, подумайте, не осталось ли вызовов API, на которые не распространяются последствия вызовов SetlmportAddress и SetExportAddress. Архитектура операционной системы Microsoft Windows Возьмите корпус с источником питания, материнскую плату, процессор, память, жесткий диск, устройство чтения компакт-дисков, видеоадаптер, клавиатуру и монитор, соберите в одно целое — получается компьютер. Но для того чтобы компьютер делал что-то полезное, нужны программы. Компьютерные программы условно делятся на системные и прикладные. Системные программы управляют работой компьютера и периферийных устройств, тем самым обеспечивая работу прикладных программ, которые решают реальные задачи пользователей. Наиболее фундаментальной системной программой является операционная система, которая управляет всеми ресурсами компьютера и обеспечивает удобный интерфейс для работы с прикладными программами. Оборудование, на котором мы работаем (хотя и обладает значительно большими возможностями, чем его предшественники), программируется на очень примитивном и неудобном уровне. Одной из главных задач операционной системы является упрощение программирования оборудования за счет использования четко определенных системных функций. Системные функции реализуются операционной системой в привилегированном режиме процессора; они определяют интерфейс между операционной системой и пользовательскими программами, работающими в непривилегированном режиме процессора. Microsoft Windows NT/2000 несколько отличается от традиционных операционных систем. Windows NT/2000 состоит из двух основных частей: привиле-
72 Глава 1. Основные принципы и понятия тированной части режима ядра (privileged kernel mode part) и непривилегированной части пользовательского режима (nonprivileged user mode part). Часть режима ядра ОС Windows NT/200 работает в привилегированном режиме процессора, в котором доступны все инструкции процессора и все адресное пространство. На процессорах Intel это означает работу на уровне привилегий 0 с доступом к 4 Гбайтам адресного пространства, адресному пространству ввода-вывода и т. д. Часть пользовательского режима ОС Windows NT/2000 работает в непривилегированном режиме процессора, в котором доступен лишь ограниченный набор инструкций и часть адресного пространства. На процессорах Intel код пользовательского режима работает на уровне привилегий 3 и обладает доступом только к младшим 2 Гбайтам адресного пространства процесса. Порты ввода- вывода для него недоступны. Часть режима ядра обеспечивает использование системных функций и внутренних процессов частью пользовательского режима. Microsoft называет эту часть «исполнительной» (executive). Это единственная точка входа к ядру операционной системы; по соображениям безопасности Microsoft отказалась от создания «черных ходов». Код режима ядра состоит из следующих основных компонентов: О HAL (Hardware Abstraction Layer) — программная прослойка, абстрагирующая часть режима ядра от аппаратных различий, зависимых от платформы; О микроядро (MicroKernel) — низкоуровневые функции операционной системы: планирование потоков, переключение задач, обработка прерываний и исключений, многопроцессорная синхронизация; О драйверы устройств (Device Drivers) — драйверы оборудования, файловой системы и сетевой поддержки, реализующие пользовательские функции ввода-вывода; О управление окнами и графическая система — реализация функций графического интерфейса (окна, элементы, графический вывод и печать); О исполнительная часть — базовые функции операционной системы: управление памятью, управление процессами и программными потоками, безопасность, ввод-вывод и межпроцессные взаимодействия. Часть пользовательского режима Windows NT/2000 обычно состоит из трех компонентов: О системные процессы — специальные системные процессы (например, процесс регистрации пользователя в системе и диспетчер сеансов); О службы (services) — в частности, службы ведения журнала событий и планирования; О платформенные подсистемы — предоставление функций операционной системы пользовательским программам через четко определенные интерфейсы API. В Windows NT/2000 поддерживается возможность запуска программ Win32, POSIX (Portable Operating System Interface — международный стандарт API операционной системы уровня языка С), OS/2 1.2 (операционная система компании IBM), DOS и Winl6.
Архитектура операционной системы Microsoft Windows 73 HAL Уровень HAL (Hardware Abstraction Layer) отвечает за платформенно-зависи- мую поддержку работы ядра NT, диспетчера ввода-вывода, отладчиков режима ядра и низкоуровневых драйверов устройств. Присутствие HAL снижает зависимость операционной системы Windows NT/2000 от конкретной аппаратной платформы или архитектуры. HAL обеспечивает абстрактное представление для адресации устройств, архитектуры ввода-вывода, управления прерываниями, операций DMA (Direct Memory Access), системных часов и таймеров, встроенных программ (firmware), интерфейсных средств BIOS и управления конфигурацией. При установке Windows NT/2000 поддержка HAL осуществляется модулем system32\hal.dll. Но на самом деле для разных архитектур существуют разные модули HAL; лишь один из них копируется в системный каталог и переименовывается в hal.dll. Просмотрите установочный компакт-диск Windows NT/2000, и вы найдете на нем несколько вариантов HAL — например, halacpi.dl_, halsp.dL и halmps.dl_. Сокращение ACPI означает «Advanced Configuration and Power Interface», то есть «интерфейс автоматического управления конфигурацией и питанием». Чтобы узнать, какие же возможности обеспечивает HAL в вашей системе, введите команду dumpbin hal. сП 1 /export. В полученном списке присутствуют такие экспортируемые функции, как HalDisableSystemlnterrupt, HalMakeBeep, HalSet- RealTimeClock, READ_P0RT_UCHAR, WRITE_P0RT_UCHAR и т. д. Функции, экспортируемые HAL, документируются в Windows 2000 DDK, в разделе «Kernel Mode Drivers, References, Part 1, Chapter 3.0: Hardware Abstraction Layer Routines». Микроядро Микроядро (MicroKernel) Windows NT/2000 управляет главным ресурсом компьютера — процессором. Оно обеспечивает поддержку обработки прерываний и исключений, планирования и синхронизации программных потоков, многопроцессорной синхронизации и отсчета времени. Микроядро предоставляет свои функции клиентам через объектно-базированные (object based) интерфейсы, по аналогии с объектами и манипуляторами, используемыми в Win32 API. Главными объектами, поддерживаемыми микроядром, являются диспетчерские и управляющие объекты. Диспетчерские объекты (dispatcher objects) предназначены для диспетчеризации и синхронизации. К их числу относятся события, мьютексы, очереди, семафоры, программные потоки и таймеры. Каждый диспетчерский объект находится в определенном состоянии — установленном (signaled) или сброшенном (not signaled). Микроядро содержит функции, которым состояние диспетчерских объектов передается в качестве параметров (KeWaitxxx). Программные потоки режима ядра синхронизируются ожиданием диспетчерских объектов или объектов пользовательского режима, содержащих внедренные диспетчерские объекты режима ядра. Например, у объектов событий пользовательского уровня в Win32 имеются соответствующие объекты событий уровня микроядра.
74 Глава 1. Основные принципы и понятия Управляющие объекты используются для управления операциями режима ядра (кроме операций диспетчеризации и синхронизации, управляемых диспетчерскими объектами). К числу управляющих объектов относятся асинхронные вызовы процедур (АРС, Asynchronous Procedure Call), отложенные вызовы процедур (DPC, Deferred Procedure Call), прерывания и процессы. Блокировка с ожиданием (spin lock) представляет собой низкоуровневый механизм синхронизации, определяемый на уровне ядра NT. Этот механизм используется для синхронизации доступа к общим ресурсам, особенно в многопроцессорных системах. Когда функция пытается получить ресурс в свое распоряжение, она переходит в режим ожидания до предоставления блокировки, не выполняя никакой полезной работы. В вашей системе микроядро находится в файле ntoskrnl.exe. Кроме микроядра в этом файле находится исполнительная часть. На установочном компакт-диске имеется две версии микроядра: ntkrnlmp.ex_ для многопроцессорных систем и ntkrnlsp.ex_ для однопроцессорных систем. Хотя модуль имеет расширение .ехе, в действительности он представляет собой DLL. Среди нескольких сотен функций, экспортируемых ntoskrnl.exe, примерно 60 принадлежат к микроядру. Имена всех функций, поддерживаемых микроядром, начинаются с префикса «Ке». Например, функция KeAcquireSpinLock предназначена для получения блокировки, обеспечивающей безопасную работу с общими данными в многопроцессорной системе. Функция KelnitializeEvent инициализирует структуру события уровня ядра, которая затем может использоваться функциями KeClearEvent, KeResetEvent и KeWaitForSingleObject. Объекты ядра описаны в Windows DDK, в разделе «Kernel Mode Drivers, Design Guide, Part 1, Chapter 3.0: NT Objects and Support for Drivers». Функции ядра документируются в разделе «Kernel Mode Drivers, References, Part 1, Chapter 5.0: Kernel Routines». Драйверы устройств Итак, микроядро управляет процессором; HAL управляет шиной, DMA, таймером, встроенными программами и BIOS. Но чтобы компьютер мог приносить реальную пользу, операционная система должна взаимодействовать с множеством разнообразных устройств, в том числе с видеоадаптером, мышью, клавиатурой, жестким диском, устройством чтения компакт-дисков, сетевым адаптером, параллельными и последовательными портами и т. д. Для взаимодействия с этими устройствами операционная система использует драйверы устройств. Большинство драйверов устройств в Windows NT/2000 является драйверами режима ядра; исключение составляют драйверы виртуальных устройств (VDD, Virtual Device Drivers) для приложений MS-DOS и драйверы принтеров пользовательского режима Windows 2000. Драйверы устройств режима ядра представляют собой DLL, загруженные в адресное пространство ядра в соответствии с конфигурацией оборудования и пользовательскими настройками. Интерфейс операционной системы Windows NT/2000 с драйверами устройств имеет многоуровневую структуру. Пользовательское приложение вызывает функции API — такие, как функции Win32 CreateFile, ReadFile, WriteFile и т. д. Вызовы преобразуются в вызовы функций системы ввода-вывода, поддерживаемой исполнительной частью Windows NT/2000. Диспетчер ввода-вывода вместе с
Архитектура операционной системы Microsoft Windows 75 исполнительной частью создает пакеты запросов ввода-вывода (IRP, I/O Request Packets) и передает их физическому устройству через драйвер (или через несколько драйверов, находящихся на разных уровнях). В Windows NT/2000 определены четыре типа драйверов режима ядра, имеющих разную структуру и функциональные возможности. О Драйвер верхнего уровня (Highest-Level Driver). К этой категории относятся в первую очередь драйверы файловых систем — в частности, драйверы файловой системы FAT (File Allocation Table), унаследованной от DOS, файловой системы NT (NTFS), файловой системы CD-ROM (CDFS), а также драйверы сетевого сервера и редиректор NT. Драйвер файловой системы может реализовывать физическую файловую систему на локальном жестком диске, но он может также реализовать и распределенную или сетевую виртуальную файловую систему. Например, некоторые системы контроля версий исходных программ реализуются в виде виртуальных файловых систем. Работа драйверов верхнего уровня основана на использовании драйверов более низких уровней. О Промежуточные драйверы (Intermediate Drivers) — драйверы виртуальных дисков, драйверы зеркального копирования (mirror drivers) или драйверы, относящиеся к определенной категории устройств, драйверы уровней сетевого транспорта, фильтрующие драйверы (filter drivers). Промежуточные драйверы либо обеспечивают дополнительные возможности, либо выполняют специфические операции для определенного класса устройств. Например, существует драйвер класса для обмена данными через параллельный порт. Работа промежуточных драйверов тоже основана на поддержке со стороны драйверов более низких уровней. В иерархию может входить несколько промежуточных драйверов. О Драйверы нижнего уровня (Lowest-Level Drivers), иногда называемые драйверами устройств. Примерами являются драйвер шины РпР, унаследованные драйверы устройств NT и драйвер NIC (Network Interface Controller). О Мини-драйверы (Mini-Drivers) — модули специализированной настройки более общих драйверов. Мини-драйвер не является полноценным драйвером. Он находится внутри общего «драйвера-оболочки» и используется для его настройки под конкретное оборудование. Например, Microsoft определяет универсальный драйвер принтера UniDriver. Производители принтеров могут разрабатывать для своих принтеров мини-драйверы, которые будут загружаться драйвером UniDriver для печати на конкретном принтере. Драйвер устройства не всегда соответствует физическому устройству. Драйвер устройства является удобным средством, которое позволяет программисту написать модуль, загружаемый в адресное пространство ядра. Загрузка модуля в адресное пространство ядра открывает полезные возможности, недоступные в обычных условиях. Наличие в Win32 API четко определенных файловых операций позволяет вашему приложению пользовательского режима легко взаимодействовать с драйвером режима ядра. Например, на сайте www.sysinternals.com имеется несколько очень полезных утилит для NT, которые позволяют использовать драйверы устройств режима ядра для контроля за реестром, файловой
76 Глава 1. Основные принципы и понятия системой и портами ввода-вывода. В главе 3 этой книги приведен простой драйвер режима ядра, который читает данные из адресного пространства ядра. Мы будем интенсивно использовать его для анализа структур данных графической подсистемы Windows. Хотя большинство драйверов устройств входит в стек ввода-вывода, управляемый диспетчером ввода-вывода исполнительной части, и имеет сходную структуру, некоторые драйверы устройств являются исключениями. Драйверы устройств для графического механизма Windows NT/2000 — например, драйвер экрана, драйвер принтера и драйвер видеопорта — используют другую структуру и вызываются напрямую. Windows 2000 даже позволяет драйверам принтеров работать в пользовательском режиме. Драйверы экрана и драйверы принтеров более подробно рассматриваются в главе 2. Большинство модулей, загруженных в адресное пространство ядра, представляет собой драйверы устройств. Утилита drivers из Windows NT/2000 DDK выводит список драйверов в окне сеанса DOS. В этом списке вы найдете драйвер tcpip.sys для сетевого обмена данными, драйвер мыши mouclass.sys, драйвер клавиатуры kbdclass.sys, драйвер CD-ROM cdrom.sys и т. д. Полная информация о драйверах устройств в Windows 2000 приводится в Windows 2000 DDK, раздел «Kernel-Mode Drivers, Design Guide and References». Управление окнами и графическая система При разработке ранних версий Microsoft Windows NT одним из ключевых факторов считалась безопасность, поэтому управление окнами и графическая система работали в пользовательском адресном пространстве. Это вызывало столько проблем с быстродействием, что начиная с Windows NT 4.0 компания Microsoft внесла принципиальное изменение в архитектуру системы и переместила управление окнами и графическую систему из пользовательского режима в режим ядра. Система управления окнами обеспечивает работу основных составляющих графического интерфейса Windows — оконных классов, окон, механизма обработки сообщений окнами, перехвата (hooking), свойств окон, меню, заголовков окон, полос прокрутки, указателей мыши, виртуальных клавиш, буфера обмена (clipboard) и т. д. В сущности, это аналог user32.dll уровня ядра, который реализует определения Win32 API из файла winuser.h. Графическая система реализует вывод в GDI/DirectDraw/Direct3D на физическое устройство или в память. Ее работа основана на драйверах графических устройств — таких, как драйверы экрана или драйверы принтеров. Графическая система является основным содержимым библиотеки gdi32.dll, реализующей определения Win32 API из файла wingdi.h. Кроме того, графическая система поддерживает работу драйверов экрана и принтеров — она обеспечивает полноценный механизм визуализации для растровых поверхностей нескольких стандартных форматов. Графическая система подробно рассматривается в главе 2. Система управления окнами и графическая система упакованы в одну большую DLL win32k.sys объемом около 1,6 Мбайт. Если просмотреть список функций, экспортируемых из win32k.sys, вы встретите в нем точки входа графической системы (например, EngBitBIt или PATHOBJbMoveTo), но не найдете ни одной точ-
Архитектура операционной системы Microsoft Windows 77 ки входа системы управления окнами. Дело" в том, что функции управления окнами никогда не вызываются другими компонентами ядра ОС, а функции графической системы должны вызываться драйверами графических устройств. Библиотеки gdi32.dll и user32.dll обращаются к win32k.sys через системные функции. Исполнительная часть Microsoft определяет исполнительную часть (Executive) Windows NT/2000 как совокупность компонентов режима ядра, образующих базовую операционную систему Windows NT/Windows 2000. Помимо HAL, микроядра и драйверов устройств, в исполнительную часть также входят компоненты исполнительной поддержки, диспетчера памяти, диспетчера кэша, структуры процессов, межпроцессных взаимодействий (LPC и RPC), диспетчера объектов, диспетчера ввода- вывода, диспетчера конфигурации и монитора безопасности. Каждый компонент исполнительной части поддерживает набор системных функций, которые могут вызываться из пользовательского режима (кроме диспетчера кэша и HAL) при помощи прерываний. Кроме того, каждый компонент предоставляет точку входа, доступную только для модулей, работающих в адресном пространстве ядра. Компонент исполнительной поддержки (Executive Support) реализует набор функций, вызываемых из режима ядра. Имена этих функций обычно начинаются с префикса «Ех». Главной функциональностью этого компонента является выделение памяти на уровне ядра. В Windows NT/2000 для управления динамическим выделением памяти из адресного пространства режима ядра используются два динамически расширяемых блока памяти, называемых пулами (pools). Первый из них — невыгружаемый (nonpaged) пул — гарантированно остается в физической памяти в течение всего времени. Критические фрагменты (например, обработчики прерываний) могут использовать невыгружаемый пул, не беспокоясь о возникновении прерываний, обусловленных отсутствием страниц в памяти. Второй, выгружаемый (paged) пул, имеет существенно больший размер, однако при нехватке физической памяти его содержимое может выгружаться на диск. Например, память для аппаратно-зависимых растров Win32 выделяется из выгружаемого пула при помощи функций семейства ExAllocatePoolxxx. Компонент исполнительной поддержки также обеспечивает эффективную схему выделения памяти блоками фиксированного размера — так называемые «обзорные списки» (look-aside lists), для работы с которыми используются такие функции, как ExAllocatePagedLookasideList. При загрузке системы из пулов выделяется несколько обзорных списков. Компонент исполнительной поддержки обеспечивает богатый ассортимент атомарных операций — ExInterlockedAddLargelnteger, ExInterlockedRemoveHeadList, InterlockedCompareExchange и т. д. К числу других функциональных возможностей относятся быстрые мьютексы, косвенные вызовы (callback), инициирование исключений, преобразование времени, создание уникальных идентификаторов UUID (Universally Unique Identifier) и т. д. Диспетчер памяти (Memory Manager) обеспечивает управление виртуальной памятью, управление балансовым набором (balance set), отображение виртуальной памяти на физическую и т. д. Диспетчер памяти поддерживает такие функции, как MmFreeContiguousMemory, MmGetPhysicalAddress, MmLockPageableCodeSection и т. д.
78 Глава 1. Основные принципы и понятия Диспетчер кэша (Cache Manager) обеспечивает кэширование данных для драйверов файловой системы Windows NT/2000. Функции диспетчера кэша имеют префикс «Сс». Диспетчер кэша экспортирует такие функции, как CcIsThereDirty- Data и CcCopyWrite. Функции компонента структуры процессов (Process Structure) предназначены для создания и завершения системных потоков режима ядра, а также для оповещения процессов/потоков и обработки запросов к ним. Например, диспетчер памяти может воспользоваться функцией PsCreateSystemThread для создания потока ядра, обеспечивающего запись «грязных» (dirty) страниц. Диспетчер объектов (Object Manager) управляет общим поведением объектов, поддерживаемых исполнительной частью. Исполнительная часть обеспечивает создание объектов для каталогов, событий, файлов, символических ссылок, таймеров и др. такими функциями, как ZwCreateDirectoryObject и ZwCreateFile. После того как объект создан, функции ObReferenceObject и ObDereferenceObject диспетчера объектов обновляют счетчик ссылок, функция ObReferenceObjectBy- Handle проверяет манипулятор объекта и возвращает указатель на сам объект. Диспетчер ввода-вывода (I/O Manager) транслирует запросы ввода-вывода от программ пользовательского режима или других компонентов режима ядра в правильную последовательность обращений к различным драйверам. Количество функций, поддерживаемых этим компонентом, очень велико. Например, функция IoCreateDevice инициализирует объект устройства для его использования драйвером, функция IoCallDriver передает пакеты запросов ввода-вывода следующему драйверу более низкого уровня, а функция IoGetStackLimits проверяет границу стека текущего программного потока. Исполнительная часть Windows NT/2000 также поддерживает небольшую runtime-библиотеку, аналогичную runtime-библиотеке С, но имеющую гораздо меньшие размеры. Runtime-библиотека ядра обеспечивает преобразования Unicode, поразрядные операции, операции с памятью и большими числами, обращения к реестру, преобразование времени, строковые операции и т. д. В Windows NT/2000 исполнительная часть и микроядро упакованы в один модуль ntoskrnl.exe, экспортирующий свыше 1000 точек входа. Функции, экспортируемые ntoskrnl.exe, обычно начинаются с двухбуквенного префикса — признака компонента, к которому относится данная функция. Например, префикс «Сс» означает диспетчер кэша, «1о» — диспетчер ввода-вывода, «Ке» — микроядро, «Ob» — диспетчер объектов, «Rtl» — runtime-библиотеку, «Dbg» — поддержку отладки и т. д. Системные функции Богатая функциональность, поддерживаемая ядром операционной системы Windows NT/2000, предоставляется модулям пользовательского режима через узкий «шлюз». На процессорах Intel это прерывание 0х2Е. Прерывание обслуживается функцией KiSystemService, которая находится в файле ntoskrnl.exe, но не экспортируется. Поскольку обработчик прерывания работает в режиме ядра, процессор автоматически переключается в привилегированный режим, что делает возможными обращения к адресному пространству ядра.
Архитектура операционной системы Microsoft Windows 79 Хотя при вызове используется всего один номер прерывания, нужный номер из более чем 900 системных функций Windows NT/2000 задается в регистре ЕАХ (для процессоров Intel). Программа ntoskrnl.exe поддерживает таблицу системных функций с именем KiServiceTable; в win32k.sys присутствует своя таблица W32pServiceTable. Таблицы системных функций регистрируются вызовом KeAdd- SystemServiceTable. Когда KiSystemService получает вызов системной функции, она проверяет, допустим ли индекс системной функции и доступны ли ожидаемые параметры, после чего передает вызов обработчику данной системной функции. Рассмотрим примеры системных функций в отладчике Microsoft Visual C++ с использованием отладочных символических файлов Windows 2000. Если проследить за вызовом CreateHalftonePalette в Win32GDI, вы увидите следующий фрагмент: _NtGdiCreateHalftonePalette@4: mov eax, 1021h lea edx, [esp+4] int 2Eh ret 4 Пользовательская функция Win32 GetDC реализуется следующим образом: _NtUserGetDC@4: mov eax, 118bh lea edx, [esp+4] int 2Eh ret 4 Функция ядра Win32 CreateEvent устроена посложнее. CreateEventA вызывает функцию CreateEventW, которая, в свою очередь, вызывает NtCreateEvent из ntdll.dll. Реализация NtCreateEvent выглядит так: _NtCreateEvent@20: mov eax, lEh lea edx. [esp+4] int 2Eh ret 14h Вызовы системных функций Windows NT/2000 практически полностью скрыты от программистов. В отладчике SoftICE/W компании Numega имеется команда ntcall, которая позволяет получить информацию о некоторых системных функциях ядра. За дополнительной информацией о системных функциях обращайтесь к статье Марка Руссиновича (Mark Russinovich) «Inside the Native API» на сайте www.sysinternals.com. Системные функции CGI будут более подробно описаны в главе 2. Системные процессы В операционной системе Windows NT/2000 работает несколько системных процессов, управляющих регистрацией пользователя в системе, службами и пользовательскими процессами. Список системных процессов можно просмотреть в диспетчере задач; также можно воспользоваться утилитой tlist, входящей в поставку Platform SDK.
80 Глава 1. Основные принципы и понятия Во время работы Windows NT/2000 существует три иерархии процессов. Первая иерархия состоит из единственного системного процесса, идентификатор которого всегда равен 0. Ко второй иерархии относятся все остальные системные процессы. Она начинается с процесса с именем system, который является родительским по отношению к процессу диспетчера сеанса (smss.exe). Процесс диспетчера сеанса является родителем процесса подсистемы Win32 (csrss.exe) и процесса регистрации пользователя в системе (winlogon.exe). Третья иерархия начинается с процесса диспетчера программ (explorer.exe), являющегося родителем всех пользовательских процессов. Дерево процессов Windows 2000, отображаемое командой tlist -t, выглядит следующим образом: System Process (0) System Idle Process System (8) smss.exe (124) Session Manager csrss.exe (148) Win32 Subsystem server winlogon.exe (168) logon process services.exe (200) service controller svchost.exe (360) spoolsv.exe (400) svchost.exe (436) mstask.exe (480) SYSTEM AGENT COM WINDOW lsass.exe (212) local security authentication server explorer.exe (668) Program Manager 0SA.EXE (744) Reminder IMGIC0N.EXE (760) Утилита Process Walker (pwalker.exe) выводит дополнительную информацию о каждом процессе. Process Walker показывает, что процесс System Idle Process состоит из одного программного потока с начальным адресом 0. Вполне возможно, что это не реальный процесс, а некий механизм, при помощи которого организуется период пассивного ожидания в системе. Процесс System обладает действительным адресом в адресном пространстве ядра и состоит из десятков потоков с начальными адресами, принадлежащими адресному пространству ядра. Следовательно, процесс System также является родительским для системных потоков режима ядра. Если преобразовать начальные адреса потоков этого процесса в символические имена, вы найдете немало интересных имен типа Phasel- Initialization, ExpWorkerThread, ExpWorkerThreadBalanceManager, MiDereferenceSegment- Thread, MiModifiedPageWriter, KeBalancedSetManager, FsRtlWorkerThread и т. д. Хотя все перечисленные потоки создаются исполнительной частью, потоки ядра могут создаваться и другими компонентами ядра. Но системные процессы Idle и System являются «чистыми» компонентами режима ядра, не имеющими модулей в адресном пространстве пользовательского режима. Другие системные процессы (диспетчер сеансов, процесс регистрации пользователей в системе и т. д.) являются процессами пользовательского режима, запущенными из файлов в формате РЕ. Например, файлы smss.exe, csrss.exe и winlogon.exe находятся в системном каталоге Windows.
Архитектура операционной системы Microsoft Windows 81 Службы В Microsoft Windows NT/2000 существует особая категория приложений — так называемые службы (services). Обычно это консольные программы, находящиеся под управлением SCM (Service Control Manager) и предоставляющие определенные услуги. Службы, в отличие от обычных пользовательских программ, могут запускаться автоматически во время загрузки системы, до регистрации в ней пользователя. Чтобы получить список служб, в настоящий момент работающих в вашей системе, запустите утилиту Task List (tlist.exe) с ключом - s. Ниже приведен примерный список служб и служебных программ. 200 services.exe Svcs: AppMgmt, Browser, dmserver, Dnscache. EventLog, LanmanServer, LanmanWorkstation, LmHosts, Messenger. PlugPlay. ProtectedStorage. seclogon. TrkWks 212 lsass.exe 360 svchost.exe 400 spoolsv.exe 436 svchost.exe 480 mstask.exe Svcs Svcs Svcs Svcs Svcs Poli cyAgent. RpcSs Spooler EventSystem. RasMan.SENS. Schedule SamSs Netman. NtmsSvc. TapiSrv Из этих служб для нас особый интерес представляет спулер (spooler), который обрабатывает задания печати на локальных компьютерах и передает их на принтер по сети. Служба спулера более подробно рассматривается в главе 2. Платформенные подсистемы На ранних стадиях эволюции Windows NT существовало не так уж много программ Win32, написанных специально для этой системы. По этой причине в Microsoft решили, что платформа Windows NT должна поддерживать возможность запуска программ DOS, Winl6, OS/2, POSIX (с интерфейсом в стиле UNIX) и Win32. Для запуска столь разных программ в Windows NT/2000 существует несколько разных платформенных подсистем. Платформенная подсистема (environment subsystem) представляет собой набор процессов и DLL, обеспечивающих некое подмножество функций операционной системы для прикладных программ, написанных для конкретной подсистемы. В каждой подсистеме имеется один процесс, управляющий ее взаимодействием с операционной системой (сервер). Отображение DLL на процессы приложения позволяет взаимодействовать с процессом подсистемы или напрямую с ядром через системные функции ОС. Постепенно подсистема Win32 занимает главное место среди подсистем, поддерживаемых семейством Windows NT/2000. Все операции управления окнами и графического вывода в пользовательском адресном пространстве выполняются через сервер подсистемы Win32 (csrss.exe). Прикладным программам для выполнения этих операций приходится обращаться к процессу подсистемы через механизм LPC, что отрицательно влияет на быстродействие. Начиная с Win-
82 Глава 1. Основные принципы и понятия dows NT 4.0 разработчики Microsoft переместили в DLL режима ядра, win.32k.sys, основную часть платформенной подсистемы Win32 вместе со всеми драйверами графических устройств. Библиотеки DLL подсистемы Win32 очень хорошо знакомы всем программистам Windows. Библиотека kernel32.dll управляет виртуальной памятью, вводом-выводом, кучей, процессами, программными потоками и синхронизацией; user32.dll обеспечивает управление окнами и передачу сообщений; gdi32.dll реализует графический вывод и печать; advapi32.dll отвечает за операции с реестром и т. д. Библиотеки DLL подсистемы Win32 обеспечивают прямой доступ к системным функциям ядра ОС и предоставляют полезные дополнительные возможности, не поддерживаемые системными функциями ОС. Примером возможностей Win32 API, не поддерживаемых напрямую в win.32k.sys, являются расширенные метафайлы (EMF). Работа двух других платформенных подсистем — OS/2 и POSIX — основана на использовании подсистемы Win32, хотя при первоначальном проектировании Windows NT они рассматривались наравне с Win32. Теперь платформенная подсистема Win32 превратилась в неотъемлемую, постоянно работающую часть операционной системы. Подсистемы OS/2 и POSIX запускаются лишь в том случае, если это необходимо для работы конкретных программ. Итоги В этой главе кратко описаны основы Windows-программирования на языке C++. Мы рассмотрели примеры простейших программ на C++, а также познакомились с языком ассемблера и средой программирования, форматом исполняемых файлов Win32 и архитектурой операционных систем Microsoft Windows NT/2000. Начиная с главы 2, основное внимание будет сосредоточено на программировании графики в Windows NT/2000. Впрочем, при необходимости мы будем создавать мелкие вспомогательные инструменты, упрощающие наши исследования. Полезную информацию о рассматриваемых здесь темах можно найти в Интернете — например, на web-страницах www.codeguru.com, www.codeproject.com и www.msdn.microsoft.com. На web-странице www.systeminternals.com имеется немало содержательных статей, утилит и примеров программ, которые помогут вам в исследованиях системы. Компания Intel открыла web-страницу для разработчиков, на которой можно больше узнать о процессорах Intel, оптимизации программ, шине AGP, компиляторе C++ от Intel и т. д. web-страница компании Adobe предназначена для всех, кто обладает необходимыми талантами для создания подключаемых модулей (plug-ins) и фильтров к приложениям Adobe. Свои web-страницы для разработчиков есть и у многих производителей видеоадаптеров. Примеры программ Полные тексты программ, приведенных в этой главе, находятся на прилагаемом компакт-диске (табл. 1.5).
Итоги 83 Таблица 1.5. Примеры программ из главы 1 Каталог проекта Описание Sample\ChartJ)l\Hellol Sample\Chart_01\Hello2 Sample\Chart_01\Hello3 Samp!e\Chart_01\Hello4 Sample\Chart_01\GDISpeed Sample\Chart__01\SetProc Программа «Hello, World» — запуск браузера Программа «Hello, World» — вывод текста на рабочем столе Программа «Hello, World» — простой класс окна Программа «Hello, World» — размывание текста средствами DirectDraw Использование ассемблера для хронометража Простой перехват функций API посредством модификации каталогов импорта/экспорта в РЕ-файле
Глава 2 Архитектура графической системы Windows Графическая система является неотъемлемой частью всех современных операционных систем, которые все шире используют интуитивно понятный графический интерфейс для того, чтобы стать доступнее для среднего пользователя. К их числу принадлежит и Windows NT/2000. Глава 1 завершилась кратким описанием архитектуры операционной системы Windows NT/2000. Эта глава посвящена графической системе как отдельному компоненту операционной системы. В ней рассматриваются компоненты графической системы и связи между ними — GDI API, DirectDraw API, OpenGL API, графический механизм, драйверы экрана и печати, система печати и спулинга. Мы также проанализируем вертикальную структуру графической системы Windows, а именно системные DLL пользовательского режима, обеспечивающие вызов системных функций, механизм режима ядра и драйверы графических устройств, создаваемые независимыми фирмами. Глава завершается примером простого драйвера принтера, который генерирует выходные данные в виде HTML-страницы. Компоненты графической системы Windows Интерфейс прикладных программ Windows — а проще говоря, Windows API — представляет собой громадный набор взаимосвязанных функций, предоставляющих различные услуги прикладным программам. С точки зрения программиста, Win32 API делится на несколько групп в соответствии с типом предоставляемых услуг. О Базовые функции Windows, обычно называемые сервисом ядра, — отладка, обработка ошибок, библиотеки динамической компоновки (DLL), процессы,
Компоненты графической системы Windows 85 потоки, файлы, ввод-вывод, межпроцессные взаимодействия, безопасность и т. д. О Функции пользовательского интерфейса, обычно называемые пользовательским сервисом, — управление окнами, очереди сообщений, диалоговые окна, элементы управления, стандартные элементы управления, стандартные диалоговые окна, ресурсы, пользовательский ввод, командный интерпретатор и т. д. О Графические и мультимедийные функции — управления цветом, DirectX, GDI, Video for Windows, Still Image, OpenGL, Windows Media и т. д. О Функции COM, OLE и ActiveX — COM (Component Object Model), автоматизация, Microsoft Transaction Server, OLE (Object Linking and Embedding) и т. д. О Функции баз данных и обмена сообщениями — DAO (Data Access Objects), SQL Server, MAPI (Messaging API) и т. д. О Сетевые и распределенные функции — Active Directory, очередь сообщений, сетевые средства, RPC, маршрутизация и удаленный доступ, сервер SNA (Systems Network Architecture), TAPI (Telephony API) и т. д. О Функции Интернета, интра- и экстрасетей — Internet Explorer, Microsoft Agent, NteShow, сценарии, Site Server и т. д. О Функции настройки и управления системой — конфигурация, настройка, управление системой и т. д. Каждая группа функций поддерживается определенным набором компонентов операционной системы. К их числу относятся DLL платформенной подсистемы Win32, драйверы пользовательского режима, системные функции и драйверы режима ядра. По каждой группе можно было бы написать объемистую книгу с информацией, необходимой для ее эффективного использования. Группа графических и мультимедийных функций Win32 API настолько велика, что для ее описания на должном уровне потребовалось бы несколько толстых книг. Книга, которую вы сейчас читаете, посвящена очень важному подмножеству этой группы — а именно, GDI и DirectDraw. Давайте поближе познакомимся с компонентами, обеспечивающими работу графических и мультимедийных функций. Графический прикладной интерфейс Win32 реализован на нескольких платформах — это Windows 95/98, WinCE, Windows NT и новая система Windows 2000. Раньше системы семейства NT отличались лучшей поддержкой GDI, поскольку в них использовались полноценные 32-разрядные реализации, а системы семейства Windows 95 обеспечивали лучшую поддержку игрового программирования. Однако новая операционная система Windows 2000 взяла все лучшее из обоих семейств. В Windows 2000 были внесены существенные изменения по поддержке аппаратного ускорения DirectX/OpenGL, появился новый интерфейс STI (Still Image), драйверы принтеров пользовательского режима и т. д. В этой книге наше внимание будет сосредоточено на архитектуре графической и мультимедийной системы Windows 2000, причем время от времени будут подчеркиваться ее отличия от Windows 95/98 и Windows NT 3.5/4.0.
86 Глава 2. Архитектура графической системы Windows При взгляде на рис. 2.1 становится видно, что графическая и мультимедийная система Windows NT/2000, как и операционная система в целом, состоит из нескольких уровней. Верхний блок изображает прикладные программы, взаимодействующие с набором 32-разрядных системных DLL пользовательского режима через Win32 API. Уровень системных DLL содержит уже знакомые библиотеки: gdi32.dll (графический интерфейс), user32.dll (пользовательский интерфейс и управление окнами), kernel32.dll (услуги базовых служб Windows) и т. д. Большинство модулей этого уровня поддерживается операционной системой, но некоторые компоненты имеют поддержку со стороны драйверов пользовательского режима, реализованных производителями оборудования. Ниже расположен шлюз для вызова системных функций, через который вызываются обработчики, находящиеся в части режима ядра. Исполнительная часть Windows NT/2000, работающая в адресном пространстве ядра, предоставляет общую поддержку графической и мультимедийной системы в виде графического механизма, диспетчера ввода-вывода, драйвера видеопорта и т. д. Она нуждается в поддержке со стороны драйверов устройств, предоставленных разработчиками оборудования, которые взаимодействуют с различными аппаратными компонентами (шиной, видеоадаптером, принтером и т. д.) через уровень HAL. Пользовательские приложения Win32 о О Драйвер принтера Спулер Процессор, монитор, провайдер b 9 5 Q. (D CQ >S га Q- о О) га Е со с; 3 га а. Вызов системной функции (О о "О г Win о о о ■о > _| О с о Q- О MCD о. 8 >S Дра Пользовательский режим Системные функции Режим ядра Диспетчер ввода-вывода Видеопорт (AGP) N Драйвер 11 шины Видеоминипорт (VPE, DxApi, TV) Драйвер Still Image Сервер MCD Графический механизм (DirectDraw, DDML) Драйвер экрана (DirectDraw, Direct3D, MCb) Шрифтовой драйвер Шрифты Драйвер принтера Драйвер порта принтера HAL Шина, монитор, камера, сканер, принтер и сетевое оборудование Рис. 2.1. Архитектура графической и мультимедийной системы в Windows 2000 Теперь «пройдемся» по части пользовательского режима по горизонтали. GDI (Graphics Device Interface, интерфейс графических устройств) и ICM (Image Color Management, система управления цветом) обеспечивают аппаратно-неза-
Компоненты графической системы Windows 87 висимый интерфейс графического программирования для приложений. При выводе на принтер GDI общается с драйвером принтера, который в Windows 2000 может работать в пользовательском режиме. Работа драйверов принтеров пользовательского режима в значительной степени зависит от функций, поддерживаемых графическим механизмом. Заданиями печати управляет специальный системный процесс — спулер. В его работе используются специализированные компоненты, которые могут модифицироваться производителем оборудования, в том числе процессор печати (print processor), монитор печати (print monitor) и провайдер печати (print provider). DirectX добавляет в эту схему относительно новый набор системных DLL Win32, реализующих СОМ-интерфейсы DirectX. Фактическое взаимодействие с реализацией DirectX в адресном пространстве ядра происходит через GDI. В DirectX входят следующие компоненты: DirectDraw, DirectSound, Direct- Music, Directlnput, DirectPlay, DirectSetup, AutoPlay и Direct3D. В этой книге из всех компонентов DirectX рассматривается только DirectDraw. Ниже GDI и DirectDraw будут описаны существенно более подробно. А пока давайте кратко познакомимся с другими компонентами, которые не войдут в книгу. Мультимедиа Мультимедийная часть Win32 API является развитием мультимедиа-средств, впервые появившихся в Windows 3.1. К их числу принадлежит MCI (Media Control Interface), аудиовывод, операции ввода-вывода в мультимедийных файлах, управление джойстиком и мультимедийные таймеры. Интерфейс MCI управляет всеми носителями информации с линейным воспроизведением; в нем предусмотрены функции загрузки, паузы, воспроизведения, записи, остановки, продолжения и т. д. Поддерживаются три типа аудиовывода: CD-аудио, MIDI (Musical Instrument Digital Interface) и оцифрованный (waveform) сигнал. Мультимедийные функции Win32 определяются в файле mmsystem.h; библиотека импортируемых функций содержится в winmm.lib и winmm.dll. Работа winmm.dll основана на устанавливаемых драйверах устройств пользовательского режима для каждого мультимедийного устройства. Главной экспортируемой функцией драйвера мультимедиа-устройства, который представляет собой 32-разрядную DLL, является функция DriverProc, обрабатывающая сообщения от системы мультимедиа - DRV_0PEN, DRVJNABLE, DRV_C0NFIGURE, DRV_CL0SE и т. д. ПРИМЕЧАНИЕ Чтобы узнать, какие мультимедийные драйверы доступны, откройте файл mmdriver.inf в каталоге %SystemRoot%\system32. В нем перечислено около десятка драйверов. Например, драйвер mmdrv.dll обеспечивает низкоуровневые операции с оцифрованным сигналом, поддержку MIDI и AUX (Auxiliary Output Device, дополнительного устройства вывода). Диспетчер сжатия аудиоданных (Microsoft Audio Compression Manager) находится в файле msacm32.drv, а файл ir32_32.dll содержит кодек Indeo — компрессор/декомпрессор видеоданных, разработанный компанией Intel и использующий алгоритм сжатия оцифрованного сигнала с поддержкой ММХ.
88 Глава 2. Архитектура графической системы Windows Возможно, вас интересует, как драйверы пользовательского режима могут управлять устройствами? Сами по себе не могут. В работе мультимедийных драйверов пользовательского режима используется специальный класс драйверов режима ядра, называемых потоковыми драйверами ядра (kernel streaming drivers), способных управлять оборудованием напрямую. Мультимедийная часть Win32 постепенно замещается соответствующими компонентами DirectX, обладающими расширенными возможностями и более высоким быстродействием. Например, DirectSound обеспечивает запись и воспроизведение звука в формате оцифрованного сигнала; DirectMusic позволяет сохранять и воспроизводить цифровые сэмплы, в том числе и в формате MIDI; Directlnput поддерживает широкий круг устройств ввода, включая мышь, клавиатуру, джойстик и другие игровые манипуляторы, а также устройства с активной обратной связью (force-feedback). Одна из мультимедийных функций, часто используемых общими приложениями Windows, предназначена для создания таймеров с высоким разрешением — это функция timeGetTimeO. Она обеспечивает точность до 1 миллисекунды, что обычно превышает точность функции GetTickCount (1 миллисекунда в Windows 95, 15 миллисекунд в Windows NT/2000). В программах Win32 функция QueryPerformanceCounter обеспечивает точность, на порядки превышающую точность функций timeGetTime и GetTickCount (если процессор поддерживает счетчики высокого разрешения). На компьютерах с процессором Intel Pentium счетчиком высокого разрешения является счетчик тактов процессора, упоминавшийся в главе 1. Следовательно, на 200-мегагерцовом процессоре измерения производятся с точностью до 5 наносекунд. Впрочем, вызов QueryPerformanceCounter такой точности pie обеспечивает; для чтения счетчика используется обращение к ядру ОС через системную функцию. Video for Windows Как и все мультимедийные средства Win32, Video for Windows имеет долгую историю, начинающуюся в эпоху Windows 3.1. Video for Windows (VFW) обеспечивает поддержку Win32 API для обработки видеоданных. Точнее говоря, поддерживается AVI (Audio-Video Interleaved), операции чтения, записи, позиционирования и редактирования файлов, диспетчер сжатия видеоданных, видеозахват и DrawDib API. Многие возможности VFW были заменены DirectShow — одним из компонентов DirectX. DrawDib API содержит такие функции, как DrawDibDraw, DrawDibGetBuffer, Draw- DibUpdate и т. д. По своим возможностям этот интерфейс API напоминает функцию Win32 StretchDIBits, но он поддерживает такие дополнительные возможности, как выбор нужного декодера, потоковую обработку данных и (предположительно) более высокое быстродействие. Первые две возможности обеспечиваются устанавливаемыми драйверами мультимедиа-устройств, обслуживающими разные потоки данных; третья возможность, конечно, не идет в сравнение с возможностями DirectDraw. В Win32 поддержка VFW обеспечивается заголовочным файлом vfw.h, библиотечным файлом vfw32.lib и DLL msvfw32.dll. Реализация VFW основана на использовании мультимедийной части Win32.
Компоненты графической системы Windows 89 ПРИМЕЧАНИЕ Функции DrawDib все еще рекламируются как средство быстрого вывода графических изображений, не использующее GDI и записывающее данные прямо в видеопамять. Звучит неплохо, но сейчас это уже перестает быть правдой, особенно в Windows NT/2000. В Windows NT/2000, где прямой доступ к видеопамяти возможен только через драйвер DirectX режима ядра, DrawDibDraw выводит DIB при помощи функции GDI и потому работает медленнее, чем функция вывода DIB из GDI. Still Image Still Image (STI) — новый интерфейс Microsoft для получения цифровых статических изображений с таких устройств, как сканеры и цифровые камеры. Он доступен только в Windows 98 и Windows 2000. Разумеется, STI заменяет более старый стандарт TWAIN. (Кстати, интересно, почему его не назвали Direct Image? Наверное, скоро назовут.) Относительная новизна этого стандарта позволила Microsoft такую роскошь, как реализация STI с использованием СОМ-интерфей- сов вместо традиционных функций Win32 API. Microsoft STI состоит из монитора событий, поставляемых производителем оборудования мини-драйверов пользовательского режима, и панели управления сканером или камерой. Монитор событий на системном уровне следит за устройствами ввода статических изображений и их событиями. Кроме того, он ведет список зарегистрированных приложений по обработке статических изображений, которые могут автоматически запускаться при обнаружении события. Мини-драйвер обнаруживает события от конкретного устройства и оповещает о происходящем монитор событий. Кроме того, он передает данные изображения из драйвера режима ядра в пользовательский режим. При помощи панели управления сканером/камерой пользователь ассоциирует устройства ввода статических изображений с приложениями, в которых предусмотрена их поддержка. Приложение панели управления сканером/камерой (sticpl.dll), монитор (stimon.dll, stisvc.exe) и приложения обработки статических изображений — все они используют СОМ-объект STI (CLSIDSti), реализующий интерфейс IStill Image, экземпляр которого создается функцией StiCreatelnstance. СОМ-объект STI реализуется в библиотеке sti.dll, использующей СОМ-интерфейсы IStiDevice и IStiDeviceControl для управления мини-драйверами. В Windows 98/2000 STI API поддерживается заголовочным файлом sti.h, библиотечным файлом sti.lib, упомянутыми выше DLL и ЕХЕ, а также драйверами соответствующих устройств пользовательского режима и режима ядра. OpenGL Последним компонентом пользовательского режима, изображенным на рис. 2.1, является OpenGL — стандарт программирования двумерной/трехмерной графики, разработанный в Silicon Graphics, Inc. Его главной целью является визуализация двумерных/трехмерных объектов в кадровом буфере (frame buffer). OpenGL позволяет программисту описывать объекты в виде совокупности вершин, каждая из которых определяется координатами, цветом, нормалью, координатами текстуры и флагом края (edge flag). Таким образом, при помощи функ-
90 Глава 2. Архитектура графической системы Windows ций OpenGL можно описывать отдельные точки, отрезки линий и трехмерные поверхности. Графические средства OpenGL позволяют задавать трансформации, коэффициенты уравнений освещенности, способы сглаживания (antialiasing) и операторы обновления пикселов. Перед конечным воспроизведением данных в буфере кадра процесс визуализации OpenGL проходит несколько стадий. На стадии вычислений кривые и поверхности аппроксимируются при помощи полиномиальных команд. На второй стадии (операции с вершинами и примитивная сборка) выполняются преобразования, вычисляется освещенность и происходит отсечение вершин. На третьей стадии (растеризации) генерируется последовательность адресов буфера кадра и связанных с ними значений. На последней стадии (фрагментарных операций) в окончательном буфере кадра производится буферизация глубины, выполняется альфа-наложение, применение масок и другие операции уровня пикселов. Как видно на примере Windows NT/2000, компания Microsoft добавила в свою реализацию OpenGL некоторые дополнительные возможности. Реализуется полный набор команд OpenGL, библиотеки OpenGL Utility (GLU) и OpenGL Programming Guide Auxiliary Library, расширение для окна (Window extension, WGL), формат пикселов уровня окна и двойная буферизация. OpenGL использует три заголовочных файла в подкаталоге gl каталога заголовочных файлов вашего компилятора: gl.h, glaux.h и glu.h. WGL определяется в заголовочном файле GDI wingdi.h. OpenGL использует библиотечные файлы opengl.lib и gdJ32.lib, а также runtime-DLL opengl32.dll и gdJ32.dll. Для повышения быстродействия OpenGL реализация позволяет драйверам, предоставленным производителями оборудования, выполнять специализированную оптимизацию и производить прямой доступ к оборудованию. Для удобства работы драйверов OpenGL Microsoft поддерживает архитектуру мини-клиента (MCD). OpenGL.dll загружает mcd32.dll — клиентскую DLL, предоставляемую операционной системой, и необязательный драйвер OpenGL пользовательского режима, предоставляемый производителем оборудования. Чтобы найти свой драйвер OpenGL, проведите в реестре поиск строки OpenGLDrivers. Клиент MCD и драйвер OpenGL пользовательского режима используют функцию GDI Ext- Escape для отправки команд графическому механизму и драйверу в режиме ядра. Для поддержки MCD-части необходим драйвер экрана, обеспечивающий оптимизацию OpenGL, с поддержкой сервера MCD уровня ядра в mcdsrv32.dll. В наши дни производители видеоадаптеров довольно часто поддерживают аппаратное ускорение DirectDraw, Direct3D и OpenGL в одном пакете. Всегда интересно видеть, как разные архитектуры (в данном случае GDI и OpenGL) используются для похожих целей. Первоначально GDI проектировался как простой интерфейс графического программирования, ориентированный на стандартное оборудование РС-инду- стрии того времени — а именно, 16- и 256-цветные видеоадаптеры EGA и VGA, а также черно-белые принтеры. Постепенно в GDI добавилась поддержка аппа- ратно-независимых растров, цветных принтеров, векторных шрифтов, шрифтов TrueType и ОрепТуре, 32-разрядного пространства логических координат, градиентных заливок, альфа-каналов, поддержка работы на нескольких мониторах или терминалах и т. д. Эволюция GDI продолжается и сейчас. GDI работает как на миниатюрных устройствах типа блокнотных компьютеров (palmtop), так
Компоненты графической системы Windows 91 и на мощных рабочих станциях. Основными целями при проектировании GDI (и Windows API в целом) были быстродействие, обратная совместимость и независимость от оборудования. С другой стороны, OpenGL проектировался как высокопроизводительный пакет двумерной/трехмерной графики для построения реалистических изображений. Из-за интенсивного использования вычислений с плавающей точкой для OpenGL необходим производительный компьютер с большим объемом памяти и мощным процессором. Такие эффекты, как освещение, размывание, сглаживание и туман на мониторе VGA с 256 цветами будут неэффективны. Хотя интерфейс OpenGL проектировался как аппаратно- независимый, он в первую очередь ориентирован на воспроизведение изображения в кадровом буфере, поэтому печать на принтерах высокого разрешения связана с некоторыми сложностями. Кстати, в Windows NT/20000 GDI предлагает решение проблем с печатью в OpenGL — команды OpenGL записываются в специальном формате EMF, а затем воспроизводятся на принтере высокого разрешения. Из-за сложности построения двумерных/трехмерных изображений OpenGL является графическим интерфейсом более высокого уровня, чем GDI. Программы OpenGL обычно описывают сцену в трехмерном пространстве при помощи вершин, отрезков линий и многоугольных поверхностей, определяют атрибуты, источники света и углы просмотра, после чего поручают дальнейшую техническую работу механизму OpenGL. В GDI приложение конструирует изображение, вызывая нужную последовательность команд с правильными параметрами. Если вы захотите создать трехмерное изображение, GDI не поможет в вычислении глубины изображения и удалении скрытых поверхностей. Даже непосредственный режим (Immediate Mode) Direct3D по сравнению с OpenGL относится к низкоуровневым интерфейсам. Windows Media Windows Media является новым дополнением графической/мультимедийной системы Win32, состоящим из Windows Media Services, Windows Media Encoder, Windows Media Player Control и Windows Media Format SDK. Компонент Windows Media Services содержит элементы ActiveX и СОМ-ин- терфейсы, позволяющие авторам Web-страниц использовать потоковую аудио- и видеоинформацию, а также управлять ее широковещательной рассылкой. Windows Media Encoder прежде всего отвечает за преобразование разных типов мультимедийного содержимого в потоки или файлы формата Windows Media, которые затем доставляются средствами Windows Media Services. Файлы-контейнеры ASF (Advanced Streaming Format) могут содержать данные, соответствующие разным форматам исходного носителя. Windows Media Player Control — элемент ActiveX для воспроизведения мультимедиа в приложениях и Web-страницах. Средства пакета Windows Media Format SDK обеспечивают возможность чтения, записи и редактирования файлов Windows Media (аудио и видеоданных, а также сценариев).
92 Глава 2. Архитектура графической системы Windows Компоненты режима ядра Графические и мультимедийные компоненты пользовательского режима могут взаимодействовать с ядром операционной системы двумя способами. В GDI, DirectDraw, Direct3D и OpenGL вызовы пользовательского режима проходят через библиотеку gdi32.dll, предоставляющую интерфейс к сотням системных функций. Для взаимодействия с драйверами видеопорта и мультимедийными драйверами вызовы пользовательского режима используют обычный интерфейс API файлового ввода-вывода, входящий в базовый сервис Windows. Вызовы системных функций файлового ввода-вывода обрабатываются диспетчером ввода-вывода исполнительной части режима ядра, который обращается к соответствующим драйверам. Вызовы GDI, DirectDraw, Direct3D и OpenGL проходят через графический механизм, который передает их драйверам конкретных устройств. К числу модулей операционной системы относятся ntoskrnl.exe (передача системных функций, диспетчер ввода-вывода), win32k.sys (графический механизм), mcdsvr32.dll (сервер MCD) и hal.dll (HAL). Исполнительная часть ядра Windows NT/2000, ntoskrnl.exe, является самой важной составляющей ядра ОС. В графической системе она в основном отвечает за передачу вызовов функций графической системы графическому механизму, поскольку в последнем используется тот же механизм вызова системных функций, что и другие системные функции. HAL предоставляет в распоряжение драйвера графического устройства средства для таких операций, как чтение и запись аппаратных регистров. Благодаря этому другие компоненты ядра в меньшей степени зависят от платформы. За дополнительными сведениями об исполнительной части и HAL обращайтесь к главе 1. Драйверы режима ядра Графическая и мультимедийная система Windows NT/2000 работает с конечными устройствами через несколько уровней драйверов, предоставленных производителем оборудования. Самую важную роль играет драйвер экрана, который должен обеспечивать поддержку GDI, DirectDraw, Direct3D и MCD для OpenGL. Драйвер экрана всегда работает в сочетании с мини-драйвером видеопорта, который, в частности, управляет аппаратными портами. Мини-драйвер видеопорта также необходим для поддержки VPE (расширение видеопорта для DirectX) и мини-порта DxApi. Другой, менее известной разновидностью драйверов является шрифтовой драйвер, поставляющий глифы шрифтов графическому механизму. Например, программа ATM (Adobe Type Manager) использует в качестве шрифтового драйвера библиотеку atmfd.dll. Файлы шрифтов загружаются в адресное пространство ядра графическим механизмом и шрифтовыми драйверами. Драйвер принтера напоминает драйвер экрана с несколькими дополнительными функциями. В отличие от других драйверов драйверы принтеров не взаимодействуют со своим устройством (то есть принтером) напрямую. Вместо этого они передают поток данных, готовых к печати, спулеру в пользовательском
Архитектура GDI 93 режиме. Спулер передает данные процессору печати, а затем монитору печати, который использует средства файлового ввода-вывода для обращения к драйверу ввода-вывода режима ядра. Windows 2000 позволяет реализовать драйвер принтера как в виде DLL пользовательского режима, так и в виде DLL режима ядра. К числу других драйверов режима ядра, используемых графической и мультимедийной системами, принадлежат драйверы мультимедиа-устройств (например, драйвер звуковой карты) и устройств ввода статических изображений (драйвер сканера или цифровой камеры). Потоковые драйверы ядра (аудио- и видеоданные, видеозахват) и драйверы устройств ввода статических изображений подробно описаны в Windows 2000 DDK. Качество драйверов устройств режима ядра имеет принципиальное значение для стабильности всей операционной системы. Драйвер режима ядра обладает доступом для чтения и записи ко всему адресному пространству ядра и всеми привилегированным инструкциям процессора. Ошибки в драйвере режима ядра могут легко привести к порче важных структур данных, поддерживаемых операционной системой, и сбою всей системы. Следовательно, любые приложения, содержащие драйверы режима ядра (например, антивирусные программы), должны тщательно тестироваться для уменьшения риска. Компания Microsoft включила в поставку Windows 2000 утилиту проверки драйверов (verifier.exe в каталоге system), которая упрощает процесс проверки драйверов разработчиками. В этом разделе была описана архитектура графической и мультимедийной систем Windows NT/2000 — сложная, но имеющая четкую структуру иерархия DLL, драйверов пользовательского режима, DLL режима ядра и драйверов режима ядра. Значительно сложнее разобраться в логике ее работы — например, во время печати управление несколько раз передается между кодом пользовательского режима и кодом режима ядра. За подробностями следует обращаться к MSDN, DDK и другой справочной документации, а наше внимание будет сосредоточено на нескольких компонентах, которые используются в большинстве обычных приложений Windows. В оставшихся разделах этой главы мы посмотрим, как устроены GDI, DirectDraw, драйвер экрана и система печати, включая драйвер принтера. Архитектура GDI Прикладной интерфейс GDI (Graphics Device Interface) был разработан компанией Microsoft для того, чтобы предоставить прикладным программам аппарат- но-независимый интерфейс к графическим устройствам — экрану монитора, принтеру, плоттеру или факсу. Реализация GDI для Win32 API, поддерживаемая в Windows 95, 98, NT и 2000, ушла далеко вперед от реализации в Windows 3.1. В операционных системах Windows NT/2000 имеет место полноценный 32- разрядный графический механизм, поэтому GDI API в этих системах обладает большими возможностями, чем в Windows 95/98, которые используют 16-разрядный графический механизм, унаследованный от Windows 3.1. Впрочем, есть
94 Глава 2. Архитектура графической системы Windows и исключения: Windows 95 поддерживает ICM, a Windows NT 4.0 — нет. Новая система Windows 2000 поддерживает ICM версии 2.0. В Windows 98 в GDI даже были добавлены такие новые возможности, как альфа-наложение. Microsoft планирует выпустить новое расширение Win32 GDI с кодовым названием GDI+, которое обеспечивает улучшенный объектно-ориентированный интерфейс к графической системе и обладает гораздо большими возможностями. Функции, экспортируемые из GDI32.DLL GDI поддерживает сотни графических функций, вызываемых Windows-программами. Большинство этих функций экспортируется библиотекой gdi32.dll подсистемы Win32. Модуль управления окнами, user32.dll, интенсивно использует функции GDI для вывода меню, значков, полос прокрутки и рамок окон. Некоторые графические функции экспортируются из user32.dll, что делает их доступными для прикладных программ. В Windows 2000 gdi32.dll экспортирует 543 точки входа. Для просмотра функций, экспортируемых модулем, проще всего воспользоваться утилитой dumpbin, входящей в поставку DevStudio. Ниже приведен фрагмент выходных данных команды dumpbin gdi32.dll/export. 543 number of functions 543 number of names ordinal hint RVA name 1 О 00027В89 AbortDoc 2 1 00027У19 AbortPath 3 2 0001FE0B AddFontMemResourceEx 4 3 0001CE3D AddFontResourceA 5 4 0001FCCC AddFontResourceExA 6 5 00020095 AddFontResourceExw 7 6 0001FE4F AddFontResourceTracking 8 7 00020085 AddFontResourceW 9 8 000264DE AngleArc 533 214 00028106 WidenPath 534 215 00031B4C XFORMOBJ_bApplyXForm 535 216 0000F9FE XFORMOBJJGetXform 536 217 00031A98 XLATE0BJ_cGetPalette 537 218 00031AB4 XLATEOBJ_hGetColorTransform 538 219 00031AA6 XLATEOBJJXlate 539 21A 0002BD2A XLATE0BJ_piVector 540 21B 000014F9 blnitSystemAndFontDirectoriesW 541 21C 0000143B bMakePathNameW 542 21D 000015AA cGetTFFromFOT 543 21E 00026A1F gdiPIaySpoolStream Группы функций GDI При таком количестве функций необходимо как-то классифицировать Win32 GDI API, чтобы понять структуру GDI. В MSDN функции GDI API разбиваются на 17 групп, дающих неплохое представление о функциональных возможностях GDI.
Архитектура GDI 95 О Растры. Функции создания и отображения аппаратно-зависимых растров (DDB, Device-Dependent Bitmaps), аппаратно-независимых растров (DIB, Device-Independent Bitmaps), DIB-секций, пикселов и заливок. О Кисти. Функции создания и модификации объектов кистей в GDI. О Отсечепие. Функции, определяющие границы области вывода в контексте устройства. О Цвет. Управление палитрой. О Координаты и преобразования. Функции работы с режимами отображения, функции отображения логических координат в физические, а также функции мировых преобразований (world transformation). О Контексты устройств. Функции создания контекстов устройств (Device Context, DC), чтения/записи атрибутов и выбора объектов GDI. О Заполненные фигуры. Функции вывода замкнутых областей и их периметров. О Шрифты и текст. Функции установки и перечисления шрифтов в системе, а также вывода текстовых строк. О Линии и кривые. Функции вывода прямых линий, эллиптических дуг и кривых Безье. О Метафайлы. Функции построения и воспроизведения метафайлов формата Windows или расширенных метафайлов. О Вывод на несколько мониторов. Функции, позволяющие использовать несколько мониторов на одном компьютере. Эти функции экспортируются из user32.dll. О Графический вывод. Функции, управляющие обработкой сообщения о перерисовке и измененной областью окна. Некоторые из этих функций экспортируются из user32.dll. Э Траектории. Функции для объединения последовательности линий и кривых в объект GDI, называемый траекторией (path), и использования этого объекта при выводе. О Перья. Функции для работы с атрибутами вывода линий. О Печать и спулер. Функции передачи команд графического вывода на такие устройства, как принтеры и плоттеры, и управления этим классом задач. Функции спулера обеспечиваются спулером Win32, содержащим несколько системных DLL и модулей, модифицируемых производителями оборудования. О Прямоугольники. Функции для работы со структурой RECT. Экспортируются из user32.dll. Э Регионы. Функции для создания из серии точек объекта GDI, называемого регионом (region), и выполнения операций с этим объектом. Кроме хорошо документированных функций, входящих в классификацию, в GDI входит немало других, малоизвестных функций. Одни документируются в DDK; другие не документируются, но используются системными DLL; третьи не документируются и не используются. Ниже приведена примерная классификация таких функций. О Драйвер принтера пользовательского режима. Функции поддержки новой возможности Windows 2000 — драйверов принтеров пользовательского режима.
96 Глава 2. Архитектура графической системы Windows В сущности, эти вспомогательные функции для обращения к точкам входа механизма GDI режима ядра, документированным в DDK. Например, драйвер принтера пользовательского режима в Windows 2000 может вызвать функцию GDI EngTextOut, которая реализуется одноименной функцией win32k.sys. О OpenGL. Функции поддержки WGL — например, SwapBuffers, SetPixel Format и GetPixel Format, описанные в документации OpenGL для Windows. О EUDC. Функции поддержки символов, определяемых пользователем (end- user-defined characters); при помощи этих функций пользователи могут добавлять в шрифты новые символы. Функции EUDC документируются в разделе International Features Platform SDK, в категории Window Base Services. GDI экспортирует такие функции, как EnableEUDC, EudcLoadLinkW и т. д. О Поддержка других системных DLL. Функции, используемые только другими системными DLL. Например, user32.dll вызывает функции GDI GdiDllInitia- lize, GdiPrinterThunk, GdiProcessSetup и т. д.; ddraw.dll вызывает GdiEntryl, GdiEntry2 и т. д.; служба спулера spoolsrv.exe вызывает GdiGetSpool Message и GdilnitSpool; wow32.dll вызывает GdiQueryTable и GdiCleanCacheDC. О Прочие недокументированные функции. Недокументированные функции, об использовании которых ничего не известно, — например, GdiConvertDC, GdiCon- svertBitmap, SetRelAbs и т. д. Рисунок 2.2 иллюстрирует наше представление об архитектуре клиентской стороны GDI. Верхний уровень соответствует категориям функций (документированные или недокументированные); под ним находятся сотни функций, разделенные на основные группы. На нижнем уровне расположены вызовы системных функций. Документированные функции Win32 GDI API Недокументированные или частично документированные функции 2L Вызовы системных функций GDI Рис. 2.2. Группы функций GDI
Архитектура GDI 97 Вызовы системных функций GDI По сравнению с DLL разных подсистем Win32 модуль gdi32.dll относительно невелик. В Windows 2000 размер gdi32.dll составляет всего 223 килобайта — меньше, чем comdig.32.dll, wow32.dll, icm32.dll, advapi32.dll, user32.dll и kernel32.dll. Это объясняется тем, что большинство возможностей GDI реализуется обращениями к механизму GDI через системные функции Windows NT/2000. Microsoft не предоставляет открытой документации по системным функциям Windows NT/2000. Хотя существуют утилиты, отображающие часть системных вызовов (Numega SoftICE/W), а также независимая документация (статья Марка Руссиновича по адресу www.sysinternals.com/ntdll.htm), не существует никаких официальных документов по системным функциям графической системы или управления окнами, или по системным функциям, поддерживаемым графическим механизмом. При помощи отладочных символических файлов и Image Help API нетрудно написать программу для перечисления всех символических имен в DLL — например, в gdi32.dll. К числу этих символических имен будут принадлежать имена экспортируемых функций, имена импортируемых функций и даже имена глобальных переменных. В Image Help API входит функция SymEnumerateSymbols, которая позволяет вызвать заданную пользователем функцию косвенного вызова (callback function) для каждого символического имени в модуле. Зная символическое имя, можно определить его адрес в образе модуля и прочитать двоичный код, начинающийся с этого адреса. Сравнивая этот код с шаблоном вызова системной функции, можно найти все функции GDI, из которых вызываются системные функции. Программа SysCall делает все, о чем говорится выше, и выводит список всех функций, использующих системные функции DLL подсистемы Win32. Вы можете вывести информацию о вызовах системных функций из user32.dll, ntdll.dll или gdi32.dll. Ниже приведен фрагмент списка из 351 (для Windows 2000) вызова системной функции из gdi32.dll, отсортированного по индексам системных функций. syscal1(0x1000. 1) gdi32.dllINtGdiAbortDoc syscall(0x1001. 1) gdi32.dllINtGdiAbortPath syscal1(0x1002. 6) gdi32.dl1!NtGdiAddFontResourceW syscal1(0x1003. 4) gdi32.dl1!NtGdiAddRemoteFontToDC syscall(0x1004. 5) gdi32.dllINtGdiAddFontMemResourceEx syscal1(0x1005. 2) gdi32.dl1!NtGdiRemoveMergeFont syscall(0x1006. 3) gdi32.dll .'NtGdiAddRemoteMMInstanceToDC syscal1(0x1007. 12) gdi32.dllINtGdiAlphaBlend syscall(0x1008. 6) gdi32.dllINtGdiAngleArc syscall syscall syscall syscall syscall syscall syscall syscall syscall (0x1125. (0x1126. (0x1128. (0x1129. (0x112a. (0xlle5. (0x1244. (0x1245. (0x1246. 11) gdi32.dll 2) gdi32.dll 1) gdi32.dll 1) gdi32.dll 1) gdi32.dll 3) gdi32.dll 3) gdi32.dll 6) gdi32.dll 4) gdi32.dll NtGdiTransparentBlt NtGdi UnloadPrinterDri ver NtGdiUnrealizeObject NtGdiUpdateColors NtGdiWidenPath NtUserSelectPalette NtGdi EngAssoci ateSurface NtGdi EngCreateBi tmap NtGdi EngCreateDevi ceSurface
98 Глава 2. Архитектура графической системы Windows syscal1(0x1247. 4) gdi32.dll!NtGdiEngCreateDeviceBitmap syscal1(0x1248, 6) gdi32.dl1!NtGdiEngCreatePalette syscal1(0x1280. 1) gdi32.dll!NtGdiEngCheckAbort syscal1(0x1281. 4) gdi32.dll!NtGdiHT_Get8BPPFormatPalette syscal1(0x1282. 6) gdi32.dl1!NtGdiHT_Get8BPPMaskPalette syscal1(0x1283. 1) gdi32.dl1!NtGdiUpdateTransform 356 total syscalIs found В списке приводится индекс вызываемой системной функции, количество передаваемых параметров, а также имя модуля и функции, из которой производится вызов. Программа SysCall также отображает адреса функции, которые здесь не приводятся для экономии места. Центральной частью программы SysCall является класс KlmageModule. Работа этого класса основана на использовании Win32 Image Help API — интерфейса, предназначенного для обработки загружаемых образов исполняемых файлов Win32. Класс загружает и выгружает модули с отладочными символическими файлами, выполняет преобразование между именами и адресами, а также перечисляет символические имена. Список вызовов системных функций реализуется перечислением всех символических имен внутри модуля и проверкой по стандартному шаблону вызова системной функции. От Win32 GDI API к системным функциям механизма GDI Сравнивая два списка (функций, экспортируемых GDI32, и системных функций, вызываемых из GDI32), нетрудно догадаться или по крайней мере сделать обоснованное предположение относительно того, как функции Win32 GDI отображаются на системные функции win32k.sys. Например, функция печати AbortDoc наверняка вызывает NtGdi AbortDoc, системную функцию с индексом 0x1000; функция поддержки драйверов принтера пользовательского режима, EngBitBIt — это простой псевдоним для NtGdi EngBitBIt, поскольку обе функции имеют одинаковые адреса. Некоторые функции Win32 API существуют в простой версии, которой проще пользоваться, и в расширенной версии с поддержкой дополнительных возможностей. Например, такую пару составляют функции AddFontResource и AddFont- ResourceEx. Логично предположить, что для этих функций Microsoft не создает двух разных системных вызовов — просто AddFontResource вызывает AddFontRe- sourceEx. Функции, получающие строковые параметры, обычно существуют в Win32 API в двух версиях: имя ANSI-версии заканчивается символом «А», а имя Unicode-версии заканчивается символом «W». Системная функция NT/2000 существует только в Unicode-версии, поскольку базовой кодировкой ОС является именно Unicode. Возможно, вы обратили внимание на то, что трем вызовам Add- FontResourceXXX соответствует единственная системная функция, NtGdiAddFontRe- sourceW. Сравнение списка экспортируемых функций GDI со списком системных функций GDI показывает, что некоторые области функциональности GDI реализуются чисто на пользовательском уровне клиента GDI, без промежуточных обращений к механизму GDI. Хорошим примером являются операции с мета-
Архитектура DirectX 99 файлами Windows и расширенными метафайлами, для которых в списке системных вызовов не обнаруживается ни малейшего следа. То же относится и к функциональным возможностям, основанным на использовании EMF — например, функций печати EMF в обход спулера GdiStartDocEMF, GdiStartPageEMF, Gdi- PlayPageEMF и т. д. В списке также отсутствуют различные функции Win32 API, предназначенные для чтения и записи системных атрибутов — например, GetBkMode, SetText- Color и т. д. Вероятно, ближайшими системными вызовами являются более общие NtGdiGetDCDword и NtGdiGetAndSetDCDword. Как выяснится позднее, некоторые атрибуты контекстов устройств для упрощения доступа хранятся в памяти пользовательского режима, а другие хранятся в структуре данных режима ядра. Подведем итог: DLL подсистемы Win32 gdi32.dll реализует Win32 GDI в основном за счет простого отображения вызовов функций Win32 API в вызовы системных функций, реализуемые графическим механизмом GDI в файле win32k.sys. Некоторые области (работа с метафайлами и расширенными метафайлами, печать EMF в обход спулера) относятся к числу действительно новых возможностей, обеспечиваемых gdi32.dll без прямой поддержки со стороны механизма GDI. Клиентские библиотеки GDI также обеспечивают реализацию других системных компонентов — DirectDraw, Direct3D, OpenGL, печати и спулинга. Архитектура DirectX Хотя для большинства прикладных программистов вполне хватало быстродействия и возможностей GDI API, компания Microsoft довольно долго боролась за то, чтобы привлечь на свою сторону и программистов игр. В играх прежде всего нужна быстрая графика, для которой аппаратно-независимые API типа Windows GDI совершенно не приспособлены. Microsoft пыталась внедрить DrawDIB API (часть Video for Windows), WinG (небольшая библиотека, ускоряющая вывод растровых изображений), WinToon (механизм работы с анимированными спрай- тами), Game SDK и, наконец, остановилась на DirectX1. Интерфейс DirectX был разработан Microsoft для программирования нового поколения компьютерных игр с быстрой графикой и мультимедиа-приложений. В DirectX также входит интерфейс DDI (Device Driver Interface), определяющий возможности, которые должны быть реализованы в драйверах экрана, предоставляемых производителем оборудования. Таким образом, DirectX ориентируется на две важные цели. На интерфейсном уровне DirectX предоставляет разработчикам игр/приложений мощный аппаратно-независимый интерфейс API без снижения быстродействия. Прикладные программисты могут использовать новые возможности устройств, не беспокоясь о непосредственной работе с оборудованием. На уровне драйверов устройств DirectX позволяет фирмам-производителям оборудования сконцентрировать внимание на аппаратных нововведениях и легко вывести их на рынок через тонкую прослойку драйверов 1 Пакет Game SDK является первой версией DirectX, смена названия объяснялась маркетинговыми соображениями. — Примеч. перев.
100 Глава 2. Архитектура графической системы Windows с поддержкой DirectX. Интерфейс DirectX DDI обеспечивает производителей оборудования необходимыми рекомендациями, которые легко интегрируются в DirectX. Компоненты DirectX DirectX состоит из нескольких основных компонентов, связанных с различными областями игрового и мультимедийного программирования. В настоящее время в Direct входят следующие компоненты. О DirectDraw — быстрый интерфейс двумерной графики, поддерживающий прямой доступ к видеопамяти, быстрый блиттинг (пересылку битовых блоков), работу вторичным буфером и переключение буферов, управление палитрой, отсечение, оверлеи и цветовые ключи. DirectDraw можно рассматривать как подмножество GDI, разработанное специально для быстрого вывода графики. О DirectSound — ускорение записи и воспроизведения оцифрованного звука (цифровые сэмплы) с низколатентным микшированием и прямым доступом к звуковым устройствам. О DirectMusic — преобразование музыкальных данных, генерируемых в пакетном виде, в оцифрованные сэмплы при помощи аппаратного или программного синтезатора. Оцифрованные сэмплы затем передаются DirectSound в виде потоковых аудиоданных. О DirectPlay — упрощение взаимодействия по модему или сети между игроками в многопользовательских играх. DirectPlay обеспечивает универсальный способ взаимодействия между приложениями DirectX, не зависящий от используемого протокола, транспорта или вида сетевых услуг. О Direct3D обеспечивает два уровня API для работы с трехмерной графикой в играх — непосредственный режим (Immediate Mode) и абстрактный режим (Retained Mode). Непосредственный режим Direct3D представляет собой низкоуровневый API трехмерной графики, который идеально подходит для опытных программистов, занимающихся переносом существующих игр и мультимедийных приложений в DirectX. Абстрактный режим Direct3D представляет собой высокоуровневый API, позволяющий легко реализовать приложения с трехмерной графикой; он основан на использовании непосредственного режима Direct3D. Direct3D поддерживает переключаемый буфер глубины, равномерную закраску и закраску Гуро, освещение сцены несколькими разнотипными источниками света, а также работу с материалами и текстурами, трансформациями и отсечением. В настоящее время разработка абстрактного режима Direct3D прекращена, и в будущем ему на смену придет новая технология. О Directlnput обеспечивает поддержку интерактивных устройств ввода — мыши, клавиатуры, джойстика, устройств с активной обратной связью и других игровых манипуляторов. О DirectSetup — простой API для установки компонентов DirectX. Игровые и мультимедийные приложения часто используют режим Автозапуска (Autoplay),
Архитектура DirectX 101 в котором установочная программа или игра автоматически запускается при вставке компакт-диска. О DirectShow — воспроизведение сжатых аудио- и видеоданных в различных форматах, в том числе MPEG, QuickTime, AVI и WAV. Существует возможность добавления новых форматов за счет подключения новых модулей, называемых фильтрами; они находятся под управлением диспетчера фильтров DirectShow. О DirectAnimation обеспечивает создание анимационных эффектов в различных средах, в том числе HTML, VBScript, JScript, Java и Visual C++. Векторная и растровая графика, спрайты, трехмерные геометрические фигуры, видео и звук объединяются в анимационный интерфейс API. DirectAnimation также содержит несколько клиентских элементов Media Player, свойства и методы которых предназначены для управления воспроизведением мультимедиа на web-странице или в приложении. На рис. 2.3 изображена архитектура DirectX, из которой для экономии места были исключены некоторые мелкие компоненты. На нижнем уровне DirectX обращается к GDI для вызова системных функций. На базе этих системных функций построены DirectDraw, DirectSound, DirectMusic, непосредственный и абстрактный режим Direct3D. Функциональность всех перечисленных компонентов предоставляется через набор СОМ-интерфейсов. DirectShow и DirectAnimation строятся поверх этих базовых компонентов DirectX, их работа также зависит от различных фильтров. На верхнем уровне находятся игры, мультимедийные приложения, апплеты Java, web-страницы и т. д. Каждый компонент DirectX представлен одной или несколькими DLL подсистемы Win32 с легко узнаваемыми именами. Например, ddraw.dll и ddrawex.dll реализуют DirectDraw API; d3dim.dll реализует API непосредственного режима Direct3D; d3drm.dll реализует API абстрактного режима Direct3D. В отличие от традиционного интерфейса Win32 API, состоящего из сотен функций, доступ к DirectX API осуществляется через интерфейсы модели СОМ (Component Object Model). СОМ-интерфейс представляет собой группу семантически связанных функций с заранее определенными типами параметров и возвращаемых значений. В парадигме программирования языка С СОМ-интерфейс может рассматриваться как таблица функций; в мире C++ СОМ-интерфейс является аналогом абстрактного базового класса. СОМ-интерфейсы реализуются СОМ-классами. Но поскольку в идеологии СОМ реализация должна быть четко отделена от интерфейса, клиентские программы могут создавать только экземпляры СОМ-классов (также называемые СОМ-объектами) и выполнять операции с ними через интерфейсы СОМ. После публикации СОМ-интерфейс «замораживается». Это означает, что определение интерфейса изменять нельзя, хотя можно свободно изменять его реализацию. Чтобы предоставить приложению новые возможности, существует только один путь — спроектировать и опубликовать новые интерфейсы. Из-за этого встречаются интерфейсы с именами IDirectDraw, IDirectDraw2 и IDirectDraw7. На рис. 2.3 изображена лишь часть СОМ-интерфейсов, поддерживаемые некоторыми компонентами DirectX. Большинство компонентов DirectX определяет слишком много интерфейсов, которые не поместятся на рисунке.
102 Глава 2. Архитектура графической системы Windows Игры DirectX, мультимедийные приложения, апплеты Java, HTML-страницы и т. д. Подключаемые модули браузера Элементы Media Player Клиентские элементы DirectAnimation DirectAnimation (danim.dll) DirectDraw (ddraw.dll, ddrawex.dll) Диспетчер фильтров DirectShow Фильтр источника Фильтр преобразования Фильтр воспроизведения! DirectSound (dsound.dll) IDirectMusic | IDirectMusicLoader | IDirectMusicColiection | IDirectMusicComposer 1 (more) 1 DirectMusic (dmusic.dil) IDirect3D3 IDirect3DDevice IDirect3DExecuteBuffer IDirect3DLight (more) Непосредственный режим Direct3D (d3dim.dll) IDirect3DRM3 IDirect3DRMDevice3 IDirect3DRMLight | IDirect3DRMMaterial2 (more) 1 Абстрактный режим Direct3D (d3dim.dll) GDI и прочий сервис ОС Рис. 2.3. Основная архитектура DirectX Архитектура DirectDraw Главной темой этой книги является программирование двумерной графики в Windows — другими словами, GDI и DirectDraw. Информацию об остальных компонентах DirectX можно почерпнуть из документации MSDN, других книг и ресурсов Интернета, а мы перейдем к рассмотрению архитектуры DirectDraw. DirectDraw можно рассматривать как специализированную версию GDI. Первая стадия специализации заключается в том, что вывод направляется только на видеоадаптер, а не на принтер, плоттер или любое другое из существующих графических устройств. Второй стадией является сокращение функциональных возможностей, поддерживаемых GDI. В DirectDraw нет прямой поддержки режимов отображения, мировых преобразований, шрифтов и текста, линий и кривых; работа осуществляется только с растровыми изображениями. Последней стадией является реализация ограниченного подмножества с учетом аппаратного ускорения и добавлением возможностей, имеющих важное значение для игр и мультимедийного программирования. DirectDraw реализует семь основных интерфейсов, два из которых существуют в нескольких версиях.
Архитектура DirectX 103 О IDirectDraw — базовый интерфейс DirectDraw, на основе которого могут создаваться другие объекты DirectDraw. Последней версией является IDirectDraw7. Интерфейсы IDirectDraw обеспечивают создание других объектов DirectDraw, управление поверхностями, выбор разрешения и глубины цвета, получение информации о состоянии экрана, выделение памяти и т. д. При вызове Direct- DrawCreate создается объект DirectDraw, который поддерживает различные интерфейсы IDirectDraw. О Интерфейс IDirectDrawSurface обеспечивает все операции вывода в DirectDraw. Последней версией является IDirectDrawSurface7. В этот интерфейс входят операции с поверхностями — получение информации о возможностях, блокировка и ее снятие, выбор палитры, отсечение и т. д. При блокировке поверхности память видеоадаптера отображается в виртуальное адресное пространство приложения, что позволяет организовать прямой доступ к ней, связать контекст устройства GDI с поверхностью DirectDraw и осуществлять вывод на поверхности средствами GDI. Еще важнее то, что IDirectDrawSurface поддерживает блиттинг между поверхностями и переключение поверхностей с аппаратным ускорением. Чтобы выполнить более сложные операции, вам придется либо реализовать их самостоятельно, либо обратиться к GDI. О Интерфейс IDirectDrawPalette поддерживает создание и непосредственные операции с цветовой палитрой на 256-цветном экране. О Интерфейс IDirectDrawClipper управляет отсечением поверхностей DirectDraw с использованием списков отсечения (clip lists), представленных структурами RGNDATA GDI API. Поскольку DirectDraw не поддерживает создание списков отсечения, в вашем распоряжении остается богатый ассортимент операций с регионами, существующих в GDI. О Интерфейс IDirectDrawColorControl управляет цветом поверхностей и оверлеев за счет регулировки яркости, контраста, оттенка, насыщенности и гамма- коррекции. О Интерфейс IDirectDrawGammaControl управляет процессом гамма-коррекции, в ходе которого значения цветов в кадровом буфере преобразуются в цвета, передаваемые аппаратному цифро-аналоговому преобразователю (DAC, digital- to-analog converter). О Интерфейс IDirectDrawVideoPort обеспечивает передачу видеоданных с аппаратного видеопорта на поверхность DirectDraw. С его помощью программист может управлять оборудованием через видеопорт. На рис. 2.4 представлена архитектура DirectDraw с компонентами как пользовательского режима, так и режима ядра. Компонентом DirectDraw пользовательского режима является библиотека ddraw.dll, связанная с gdi32.dll и mcd32.dll (OpenGL). Вызовы функций DirectDraw проходят через gdi32.dll и приводят к вызову системных функций, предварительная обработка которых производится диспетчером системных функций в адресном пространстве режима ядра. Диспетчер передает вызов графическому механизму (win32k.sys), после чего вызов передается либо драйверу экрана, предоставленному производителем оборудования, либо драйверу видеопорта. DirectDraw не является однозначной заменой GDI, поскольку его ориентация на экранный вывод и ограниченность функций
104 Глава 2. Архитектура графической системы Windows могут заставить приложения DirectDraw использовать поддержку GDI, особенно при выводе кривых, операциях с регионами, шрифтами и текстом. Глубокое понимание реализации GDI также поможет имитировать работу GDI средствами DirectDraw. DirectDraw HEL (ddraw.dll) GDI32 (gdi32.dll) MCD (mcd32.dll) Вызов системных функций (gdi32.dll) Диспетчер системных функций (ntoskrnl.exe) Graphics Engine (win32k.sys) Драйвер видеопорта Драйвер экрана (DirectDraw HAL) Рис. 2.4. Архитектура DirectDraw В документации Microsoft и в других документах DirectDraw нередко изображается рядом с GDI, причем оба интерфейса напрямую работают с оборудованием через аппаратно-зависимый абстрагирующий уровень. В некоторых книгах даже утверждается, что при работе с DirectDraw вам GDI уже не понадобится. В действительности API DirectDraw реализуется библиотекой ddraw.dll, взаимодействие которой с графическим механизмом и затем с драйверами устройств обеспечивает gdi32.dll. Библиотека ddraw.dll импортирует ряд важных недокументированных функций, экспортируемых GDI, — почти все функции от GdiEntryl до GdiEntryl5. В разделе «Компоненты графической системы Windows» упоминалась программа SysCall, предназначенная для вывода списка системных функций, вызываемых в системной DLL (например, gdi32.dll). Если взглянуть на такой список для GDI32, вы обнаружите в нем десятки вызовов системных функций DirectDraw и Direct3D — например, NtGdiDdCreateSurface, NtGdiD3dTexture- Swap и NtGdoD3dDrawPrimitives2. Разумеется, в самом интерфейсе GDI эти функции не используются. Возможно единственное объяснение: GDI экспортирует эти функции в интерфейсы DirectDraw/Direct3D для ddraw.dll и других DLL DirectX через недокументированные точки входа. Реализация DirectDraw состоит из нескольких уровней. Верхний уровень поддерживает СОМ-интерфейсы DirectDraw, стандартные экспортируемые функции
Архитектура системы печати 105 СОМ-объектов (DUGetCI assObject и т. д.) и специальные функции создания объектов DirectDraw (DirectDrawCreate и т. д.). Средний уровень, HEL (Hardware Emulation Layer), эмулирует все или некоторые возможности DirectDraw, не поддерживаемые на аппаратном уровне. Нижний уровень, называемый HAL (Hardware Abstraction Layer), взаимодействует непосредственно с видеоадаптером. Но какие DLL реализуют DirectDraw HEL и DirectDraw HAL? Оказывается, DirectDraw HEL является важной частью ddraw.dll, 32-разрядной DLL пользовательского режима. В этом можно убедиться несколькими способами. Для начала взгляните на размер ddraw.dll — 248 Кбайт, чуть больше, чем gdi32.dll. Из этого можно сделать вывод, что ddraw.dll — нечто большее, чем тонкая прослойка API. Затем посмотрите на список импортируемых функций ddraw; вы найдете в нем такие функции GDI, как CreateDIBSection, StretchDIBits, PatBIt, BitBlt и т. д. Следовательно, ddraw использует функции GDI в процессе вывода. Наконец, воспользуйтесь какой-нибудь программой, выводящей списки символических имен в файлах с отладочной информацией, — например, отладчиком Visual C++. Вы найдете в ddraw.dll такие имена, как HELBU, HELInitializeSpecialCases и general- AlphaBlt. В списке также встречается немало имен с префиксом «mmx», относящихся к расширенному набору мультимедийных инструкций процессоров Intel. Следовательно, ddraw.dll обеспечивает специальную оптимизацию DirectDraw HEL для MMX. Реализация DirectDraw HEL в пользовательском режиме заметно упрощает использование программного кода как в Windows NT/2000, так и в Windows 95/98. Кроме того, применение инструкций вещественных вычислений и инструкций ММХ, которые недостаточно хорошо поддерживаются режимом ядра ОС, сопряжено с определенными трудностями. Не стоит и говорить, что с увеличением доли кода пользовательского режима DirectDraw реже «подвешивает» систему. DirectDraw HAL представляет собой обычный драйвер устройства, который предоставляется разработчиком видеоадаптера, поддерживающего интерфейс DDI DirectDraw. Помните, что уровень DirectDraw API и HEL в пользовательском режиме не имеют прямого доступа к уровню DirectDraw HAL, который в Windows NT/2000 работает в режиме ядра. Чтобы добраться до DirectDraw HAL, приходится пройти через системные функции GDI, обрабатываемые механизмом GDI (win32k.sys). В этой книге будет приведена более подробная информация о DirectDraw. В разделе «Обращение к адресному пространству режима ядра» главы 3 исследуются внутренние структуры данных DirectDraw. В разделе «Отслеживание СОМ-интерфейсов DirectDraw» главы 4 освещается процесс мониторинга интерфейсов DirectDraw. Наконец, описанию DirectDraw посвящена вся глава 18. Архитектура системы печати Интерфейс Win32 GDI API задумывался как аппаратно-независимый API, способный выводить прямые, кривые, растровые изображения и текст на любом графическом устройстве, для которого имеется соответствующий драйвер. Однако принтеры составляют особый класс графических устройств и заслуживают
106 Глава 2. Архитектура графической системы Windows особого внимания. Ниже перечислены важнейшие отличия принтеров от других графических устройств. О Пользователи обычно печатают не одну страницу, а целый документ, задавая при этом специальные параметры — качество печати, размер бумаги, режим двусторонней печати, количество копий и т. д. GDI содержит специальный принтерный API для постраничной печати, а также структуру DEVM0DE для определения всех параметров печати. Такие аспекты, как разбиение документа на страницы и выбор размеров полей, находятся под контролем приложения. О Принтер обычно обладает гораздо большим разрешением (от 300 до 2400 dpi), чем экран монитора (от 75 до 120 dpi). Это приводит к увеличению объема обрабатываемых данных и возможной нехватке памяти для одновременного воспроизведения всей страницы. Механизм GDI позволяет драйверу принтера принимать данные небольшими частями (полосами) посредством спулин- га EMF (расширенных метафайлов). О Принтер обычно работает медленно, совместно используется несколькими участниками рабочей группы и не всегда подключается к локальному компьютеру. Спулер системы Windows следит за тем, чтобы приложения как можно раньше завершали свою часть вывода, чтобы принтер мог обслуживать несколько заданий печати и чтобы группы пользователей совместно работали с принтером в локальном окружении, по сети и даже по адресу URL. О Принтеры «говорят» на разных языках — PCL (принтеры HP), ESC/P (принтеры Epson), PostScript (принтеры с поддержкой PostScript) и HPGL (плоттеры). В этом отношении они принципиально отличаются от видеоадаптеров, работающих с растровыми изображениями. Microsoft предоставляет несколько «универсальных» драйверов, которые могут настраиваться производителями оборудования в соответствии со специфическими требованиями их устройств. В архитектуре печати Windows NT/2000 центральное место занимает спулер печати (print spooler), поддерживаемый GDI и драйвером принтера. Чтобы создать новое задание печати, пользовательское приложение обращается к точкам входа API, экспортируемым GDI и DLL клиента спулера. GDI и спулер (с помощью драйвера принтера) обрабатывают задание печати и посылают данные на устройство создания жестких копий, будь то лазерный или струйный принтер, плоттер или факс. Графические команды передаются GDI в виде вызовов GDI API, которые обычно сохраняются в расширенном метафайле (EMF). EMF и другой файл с текущими параметрами печати передаются системному процессу службы спулера (spools.exe). На этой стадии печать документа на уровне приложения завершается. Пользователь может продолжить работу с приложением, а дальнейшая печать документа будет осуществляться спулером. Сначала спулер направляет задание провайдеру печати, который обслуживает конкретный принтер. Локальные принтеры обслуживаются локальным провайдером печати (localspl.dll), а сетевые принтеры обслуживаются провайдером печати сетей Windows (win32spl.dll). Если принтер подключен к удаленному компьютеру, то файлы спулера пересылаются на удаленный компьютер сетевыми службами ОС, где они поступают к спулеру в виде задания для локального компьютера. Архитектуру системы печати в Windows NT/2000 иллюстрирует рис. 2.5.
Архитектура системы печати 107 Локальная система WinNT/2000 System Приложение -о см СО "О О) о о го &? 5 "О >> — с о о о 1- О- I </> а> .Е с * 5- Интерфейсная DLL драйвера печати и RPC Служба спулера (spoolsv.dll) Маршрутизатор спулера (spoolsv.dll) Провайдер печати для сетей Windows (win32spl.dll) Интерфейсная DLL драйвера печати Пользовательский режим Kernel Mode Сервер WinNT/2000 Служба спулера (spoolsv.dll) Маршрутизатор спулера (spoolsv.dll) Локальный провайдер печати (localspl.dll) Процессор печати (localspl.dll) GDI (gdi32.dll) Драйвер принтера пользовательского режима Языковой монитор Монитор порта Файловый ввод-вывод" Системные функции Графический механизм Шрифтовой драйвер Шрифты Драйвер принтера режима ядра Диспетчер ввода/вывода Сетевые драйверы Драйверы порта принтера Рис. 2.5. Архитектура системы печати в Windows NT/2000 Когда локальный провайдер печати наконец получает задание, оно передается процессору печати. Процессор печати проверяет формат файла спулера. Для файлов EMF каждая страница воспроизводится в GDI. В результате команды GDI разбиваются на графические примитивы, определяемые в интерфейсе DDI, которые затем передаются драйверу принтера. Драйвер принтера преобразует графические примитивы в низкоуровневые данные языка принтера — например, PCL, ESC/P или PostScript. Низкоуровневые данные возвращаются процессору печати. Когда процессор печати получает низкоуровневые данные, готовые к отправке на принтер, он передает их языковому монитору (language monitor). Языковой монитор пересылает данные монитору порта (port monitor), который использует API файловой системы для записи данных в аппаратный порт. Работа встроенного кода (firmware) на стороне принтера нас не интересует; мы считаем, что в результате длинной цепочки программных компонентов, описанной выше, ваш документ будет успешно напечатан. Перейдем к более подробному рассмотрению компонентов системы печати Windows NT/2000.
108 Глава 2. Архитектура графической системы Windows Клиент спулера Win32 Клиентская библиотека DLL спулера Win32 (winspool.drv) предоставляет пользовательским приложениям доступ к API спулера. Пользовательское приложение использует API спулера для обращения с запросами к принтерам и заданиям печати, получения и определения настроек принтера, загрузки интерфейсной DLL драйвера принтера для вывода диалогового окна настройки параметров печати и т. д. Например, функции OpenPrinter, WritePrinter и ClosePrinter, входящие в API спулера, могут использоваться для отправки данных непосредственно на принтер в обход стандартной процедуры вывода через GDI и драйвер принтера. API спулера определяется в заголовочном файле winspool.h, а его библиотечным файлом является winspool.lib. Таким образом, winspool.drv загружается в процесс приложения, когда возникает необходимость в выводе на печать. Клиентская DLL спулера помогает GDI определить, как должна происходить обработка задания. Для обычных заданий GDI генерирует файл EMF и передает его клиенту спулера, который использует механизм RPC для передачи задания системному процессу службы спулера. Служба спулера Спулер Windows NT/2000 реализуется в виде службы (service) — процесса, обладающего особыми привилегиями и особой ответственностью в системе. Служба спулера запускается при загрузке операционной системы. Именно по этой причине принтер иногда начинает автоматически печатать при перезагрузке системы, если при завершении работы в системе оставались необработанные задания печати. Команда net stop spooler останавливает процесс спулера, а команда net start spooler перезапускает его. После остановки спулера перестает работать мини- приложение Принтеры в панели управления. Служба спулера экспортирует интерфейс на базе RPC (Remote Procedure Call) для клиентской DLL спулера, которая используется приложением для управления принтерами, драйверами принтеров и заданиями печати. Сама служба спулера представляет собой маленький ЕХЕ-файл (spoolsv.exe). Большая часть вызовов ее функций передается провайдеру печати через маршрутизатор спулера. Служба спулера является системным компонентом, который невозможно заменить. Маршрутизатор спулера Принтер, на котором вы печатаете, не всегда подключается к вашему компьютеру — он может находиться где-то в сети Microsoft, на сервере Novell или вообще в другой точке земного шара (в этом случае печать осуществляется по адресу URL). При помощи DLL маршрутизатора (spoolss.dll) служба спулера передает задания печати провайдеру, который знает, куда следует отправить задание. По составу экспортируемых функций библиотека spoolss.dll напоминает winspool.drv. Например, функции AddPrinter, OpenPrinter, EnumJobW и т. д. встреча-
Архитектура системы печати 109 ются в обеих библиотеках. В процессе спулинга вызовы часто передаются от одного модуля к другому, затем к третьему и т. д., до тех пор, пока они не достигнут места назначения. Главная функция маршрутизатора проста — поиск ргужного провайдера печати и дальнейшая пересылка информации. Поиск осуществляется по имени или манипулятору (handle) принтера с использованием настроек принтера в системном реестре. Когда пользовательское приложение обращается с вызовом OpenPrinter к клиентской DLL спулера (winspool.drv), этот вызов передается системной службе спулера (spoolsv.exe). Последняя вызывает маршрутизатор спулера, который вызывает функцию OpenPrinter каждого провайдера печати до тех пор, пока один из них не вернет манипулятор, означающий, что провайдер печати опознал имя принтера. Этот манипулятор возвращается приложению, чтобы последующие вызовы сразу направлялись нужному провайдеру печати. Маршрутизатор спулера является системным компонентом, который невозможно заменить. Провайдер печати Провайдер печати отвечает за передачу заданий печати на локальный или удаленный компьютер. Кроме того, он управляет операциями с очередью заданий печати — такими, как запуск, остановка и перечисление заданий. В отличие от маршрутизатора и службы спулера, в системе может присутствовать несколько провайдеров печати. Производители принтеров даже могут создавать собственные провайдеры печати при помощи Windows NT/2000 DDK. В поставку операционной системы входит несколько провайдеров печати. О Локальный провайдер печати (localspl.dll) управляет локальными заданиями печати или заданиями, отправленными с удаленных клиентов на локальный компьютер. В конечном счете каждое задание обрабатывается локальным провайдером печати, который передает задание процессору печати. В Windows 2000 процессор печати, используемый по умолчанию, реализуется в DLL локального провайдера печати. О Провайдер печати сетей Windows (win32spl.dll) передает задания печати удаленному серверу Win32. О Провайдер печати Novell Netware (nwprovau.dll) передает задания печати на серверы печати Novell Netware. Поскольку файлы в формате EMF не обрабатываются серверами Novell, перед отправкой на сервер Novell задания печати должны быть преобразованы в низкоуровневые (RAW) данные. О Провайдер печати HTTP (inetpp.dll) передает задания печати по адресам URL. Все провайдеры печати должны реализовывать некоторый набор обязательных функций, перечисленных в DDK, чтобы маршрутизатор спулера мог работать с ними по одним правилам. Другие функции являются необязательными. В каталоге src\print\pp Windows 2000 DDK приведен исходный текст примерного провайдера печати. Главная точка входа провайдера называется Initialize- PrintProvider. Доступ к другим точкам входа осуществляется через таблицу функций, возвращаемую Initial izePrintProvider.
110 Глава 2. Архитектура графической системы Windows Локальный провайдер печати должен реализовать полный набор функций провайдера печати, включая отмену спулинга заданий печати, обращения к интерфейсной DLL драйвера принтера и вызов процессоров печати для обработки задания. Процессор печати Процессор печати отвечает за преобразование спулерных файлов задания печати в данные низкоуровневого формата, которые могут передаваться на принтер. Кроме того, они вызываются для выполнения управляющих операций с заданиями печати — для приостановки, возобновления и отмены запросов. Процессор печати вызывается локальным провайдером печати. Спулерные файлы заданий печати в Windows NT/2000 обычно хранятся в формате EMF. GDI помогает преобразовать заявку приложения на выполнения графических операций в формат EMF pi быстро записать этот файл на диск, чтобы приложение могло возобновить свою нормальную работу. Спулерные файлы обычно хранятся в каталоге $SystemRoot$\spool\printers. Для заданий печати в формате EMF спулер генерирует два файла. Файл с расширением .shd содержит параметры задания — имя принтера, имя документа, имя порта, а также копию структуры DEVM0DE. Файл с расширением .spl содержит недокументированный заголовок, внедренные шрифты и одну страницу в формате EMF для каждой печатаемой страницы документа. В поставку Windows 2000 входят два стандартных процессора печати: О процессор печати Windows (в localspl.dll) поддерживает различные форматы спулера, включая NT EMF, RAW и TEXT; О процессор печати Macintosh (в sfmpsprt.dll) поддерживает формат PSCRIPT1. Формат EMF является обычным файловым форматом спулинга для всех приложений Windows. Файл в формате EMF обычно занимает существенно меньше места, чем низкоуровневые данные, готовые к передаче на принтер. EMF- файлы обычно генерируются GDI с минимальным участием драйвера принтера. Если вы печатаете по сети, пересылка заданий печати в формате EMF приводит к уменьшению сетевого трафика. Кроме того, клиентский компьютер получает возможность продолжить нормальную работу, пока сервер занимается преобразованием EMF в низкоуровневые данные принтера. В процессе построения EMF-файла GDI обращается к удаленному компьютеру с запросом о доступности шрифтов. Если некоторые шрифты отсутствуют на удаленном компьютере, они внедряются в файл .shd, пересылаются на удаленный компьютер и устанавливаются на нем. Если выбрать тип данных RAW, то принтерные данные будут сгенерированы на клиентском компьютере вместо сервера; это приведет к увеличению сетевого трафика, но также и к снижению затрат памяти и вычислительных мощностей сервера. Спулерный файл в формате PostScript для принтеров с поддержкой PostScript считается относящимся к типу RAW, поскольку он не требует дополнительных преобразований перед отправкой на принтер. Спулерные файлы в формате TEXT состоят исключительно из текста в кодировке ANSI. За воспроизведение текстовых строк в формате, который поддер-
Архитектура системы печати 111 живается принтером, отвечает процессор печати. Для этого он обращается с запросами к GDI и драйверу принтера. Вероятно, печать в формате TEXT может пригодиться в DOS-приложениях. Формат PSCRIPT1 и процессор печати sfmpsprt не предназначены для принтеров PostScript. Вернее, файл в формате PSCRIPT1 имеет формат PostScript, но процессор печати sfmpsprt преобразует его в формат RAW для вывода на принтер. Следовательно, sfmpsprt в действительности является интерпретатором PostScript. Процессор печати не имеет прямого доступа к спулерным файлам, а их форматы не документированы. Для преобразования файлов в низкоуровневые данные принтера процессор печати пользуется услугами GDI и API клиента спулера (winspool.drv). Ваш выбор не ограничивается двумя процессорами печати, предоставляемыми Microsoft. В Windows NT/2000 DDK входит полная документация и работающий пример процессора печати. В каталоге src\print\genprint Windows 2000 DDK находится пример процессора печати для формата EMF. Вы можете откомпилировать его, скопировать двоичный файл в каталог $SystemRoot$\system32\ spoo!\prtprocs\w32x86, написать маленькую программу с вызовом AddPrintProcessor для его установки, а затем повозиться с собственным процессором печати в отладчике. Windows NT 4.0 DDK содержит более полный пример процессора печати с поддержкой форматов EMF, RAW и TEXT. Главными точками входа процессора печати являются функции OpenPrint- Processor, PrintDocumentOnPrinterProcessor и ClosePrintProcessor. Функция OpenPrint- Processor инициализирует процессор печати для приема задания; PrintDocumentOn- PrinterProcessor обрабатывает задание печати, a ClosePrintProcessor освобождает память, выделенную в OpenPrintProcessor. Для спулерных файлов в формате EMF в Windows NT 4.0 GDI имеется единственная функция GdiPlayEMF, которая воспроизводит весь документ. Windows 2000 поддерживает более обширный, но все же несколько ограниченный API, позволяющий процессору печати обращаться с запросами к отдельным страницам EMF-файла, изменять порядок воспроизведения страниц, объединять несколько логических страниц в одну физическую страницу и задействовать мировые преобразования координат при воспроизведении EMF-файлов. Например, в реализации PrintDocumentOnPrintProcessor можно использовать следующие функции: О GdiGetPageCount — получить количество страниц в документе; при вызове эта функция ожидает завершения спулинга всего документа в формате EMF; О GdiStartPageEMF — начать воспроизведение физической страницы; О GdiGetSpoolPageHandle — найти последнюю EMF-страницу; О GdiPlayPageEMF — воспроизвести четыре логических страницы так, чтобы каждая из них занимала четверть физической страницы; О GdiEndPageEMF — завершить воспроизведение физической страницы с обращением к драйверу принтера. Вызов описанной последовательности функций приводит к результату, который называется «кратной печатью в обратном порядке» — другими словами, документ печатается от конца к началу, и на одной физической странице печатает-
112 Глава 2. Архитектура графической системы Windows ся несколько (в данном случае 4) логических страницы. При такой архитектуре процессора Windows 2000 вам не придется реализовывать эти средства форматирования документов в каждом драйвере принтера. Достаточно иметь один процессор печати, который выполнит необходимые предварительные действия для всех совместимых драйверов. Процессор печати для формата EMF в Windows NT 4.0 реализован в виде отдельной DLL, winprint.dll. В Windows 2000 его функциональность интегрирована в localspl.dll — PrintDocumentOnPrintProcessor входит в список экспортируемых функций localspl.dll. Чтобы сменить процессор печати для драйвера принтера, перейдите на страницу свойств драйвера и выберите вкладку Advanced; вы найдете на ней кнопку Print Processor... От процессора печати данные могут идти в нескольких направлениях. Для данных в формате RAW процессор печати вызывает функцию WritePrinter (см. пример winprint в Windows NT 4.0, файл raw.c). В этом случае данные передаются непосредственно языковому монитору. Для данных в формате TEXT процессор печати вызывает StartDoc и отправляет графические команды GDI драйверу принтера. В этом случае за вызов WritePrinter отвечает драйвер. Для данных в формате EMF процессор печати вызывает функцию GdiEndPageEMF, которая использует механизм воспроизведения EMF для передачи записанных графических команд драйверу принтера. В этом случае функция WritePrinter также вызывается драйвером принтера. Языковой монитор и монитор порта Мониторы печати (print monitors) отвечают за передачу низкоуровневых данных печати от спулера к правильному драйверу порта. Мониторы печати делятся на два типа — языковые мониторы (language monitors) и мониторы портов (port monitors). Термин «язык» в данном случае не относится ни к английскому языку, ни к языку программирования C++. Он означает особую категорию языков заданий печати (например, PJL), понятных для встроенных программ принтера. Главной целью языкового монитора является обеспечение двустороннего взаимодействия между спулером печати и принтером, подключенным к компьютеру кабелем, обеспечивающим возможность двусторонней связи. Прямой канал (от компьютера к принтеру) в основном предназначен для отправки на принтер данных печати. Обратный канал (от принтера к компьютеру) обеспечивает обратную связь. Спулер, драйвер принтера и даже пользовательское приложение могут обратиться с запросом о точных возможностях и состоянии принтера (например, объеме установленной и доступной памяти, установленных дополнительных модулях, количестве чернил в картридже и т. д.). Языковой монитор может предоставить необходимую информацию посредством стандартного вызова Devi се- IoControl. Второй важной функцией языкового монитора является вставка команд управления принтером в поток данных печати. Монитор порта работает на более низком уровне, чем языковой монитор; он обеспечивает канал взаимодействия между спулером и драйверами порта режима ядра, которые фактически обращаются к аппаратным портам ввода-вывода, к которым подключаются принтеры. Монитор порта как DLL пользовательского
Архитектура системы печати 113 режима не имеет прямого доступа к оборудованию. Для взаимодействия с драйверами в ядре ОС он использует обычные функции API файловой системы — CreateFile, WriteFile, ReadFile и DeviceloControl. Монитор порта также отвечает за управление логическими портами принтеров на вашем компьютере; например, localmon.dll обеспечивает поддержку всех СОМ- и LPT-портов на вашем локальном компьютере. Таким образом, когда ваше приложение записывает данные в порт LPT1, оно не взаимодействует с драйвером физического порта напрямую. В действительности приложение общается с каналом (pipe), созданным спулером и находящимся под управлением монитора порта. Если воспользоваться утилитой Winobj, входящей в SDK, вы увидите, что «\DosDevices\LPTl» представляет собой символическую ссылку для «\Device\NamedPipe\Spooler\LPTl». В Windows 2000 входит несколько мониторов печати: pjlmon.dll для разнообразных принтеров HP с поддержкой языка управления заданиями PJL; tcpmon.dll для управления сетевым портом; faxmon.dll для драйвера факса и sfmmon.dll. В DDK также включены примеры исходных текстов языкового монитора и монитора порта, чтобы производители оборудования могли создавать собственные мониторы печати. Процесс спулера изнутри В этом разделе кратко описана архитектура системы печати Windows NT/2000, причем основное внимание уделяется спулеру. Дополнительная информация об API печати приведена в главе 17, а драйверы принтеров более подробно рассматриваются ниже, в разделе «Драйверы принтеров». 1. Если вам захочется поближе познакомиться с системным процессом спулера, в котором происходят столь захватывающие события, это нетрудно сделать при помощи Visual Studio. Выполните следующие простые действия. 2. Нажмите клавиши Ctrl+Alt+Del; на экране появляется диалоговое окно Windows Security. Выберите вариант Task Manager. 3. В списке процессов выберите службу спулера (spoolsv.exe), щелкните на ней правой кнопкой мыши и выберите команду Debug; вы переходите в режим отладки системного процесса службы спулера. 4. Просмотрите список модулей VC 6.0. Вы найдете в нем клиентскую библиотеку DLL спулера, маршрутизатор, провайдеров печати, процессоры печати, языковые мониторы, мониторы портов и другие модули, не упоминавшиеся выше. 5. Запустите задание печати из панели управления, проследите за тем, как загружаются интерфейсные DLL драйвера принтера и драйвер принтера пользовательского режима и как создаются и завершаются программные потоки. Например, в Windows 2000 в качестве драйвера принтера пользовательского режима широко используется Microsoft UniDriver (unidrv.dll); его интерфейсная DLL называется unidrvui.dll. 6. Если вы закроете Visual Studio, завершая тем самым процесс службы спулера, не забудьте перезапустить его командой net start spooler.
114 Глава 2. Архитектура графической системы Windows На рис. 2.6 показана часть модулей, загруженных процессом службы спулера после создания задания печати. Этот процесс загружает 55 модулей. Компоненты драйверов принтеров, предоставленные производителем оборудования, обычно загружаются во время печати и выгружаются после ее завершения. шшшшшшш ;р?1 ■^■1!.й.;^ imiitfmiiMimfift -lnniififiiiuihiMimi Ш^< *Г 1 .4 f^ffi^^ffiff^.frft^S'': ^Л^^кГ^^^^ ^'.'/.'$Н'&. М tcpmon.dll usbmon.dll msfaxmon.dll sfmpsprt.dll rnr20.dll winrnr.dll nwprovau.dll mpr.dll win32spl.dll clbcatq.dll oleaut32.dll inetpp.dll icmp.dll UNIDRVUI.DLL UNIDRV.DLL mscms.dll DAWINNT50\system32Stcpmon.dll 40 DAWINNT50Ssystem32\usbmon.dll 41 DAWINNT50\system32Vnsfaxmon.dll 42 DAWINNT50\system32\spool\prtprocs\w32x86\sfmpspr... 43 DAWINNT50\system32\rnr20.dll 44 DAWINNT50\system32Winmr.dll 45 \WINNT50\system32\nwprovau.dll 4G \WINNT50\system32\mpr.dll 47 \WINNT50\system32\win32spl.dll 48 \WINNT50\syslem32\clbcatq.dll 49 \WINNT50\system32\oleaut32.dll 50 \WINNT50\system32\inetpp.dll 51 DAWINNT50\system32\icmp.dll 52 DAWINNT50\sy$tem32\spool\drivers\w32x86\3\UNID... 53 DAWINNT50\system32\spool\drivers\w32x86\3\UNID... 54 DAWINNT50\system32\mscms.dll 55' Л- f\CK*? »>■ ■n *•-! -jf^ Рис. 2.6. Модули, загруженные процессом службы спулера Графический механизм В разделе «Компоненты графической системы Windows» была приведена диаграмма с архитектурой графической системы Windows NT/2000. На этой диаграмме имеется большой блок с надписью «Графический механизм». В предыдущих обсуждениях GDI, DirectDraw и Direct3D говорилось о том, что все они вызывают системные функции gdi32.dll, обрабатываемые графическим механизмом. Давайте поближе познакомимся с графическим механизмом Windows NT/2000 — опорой GDI, шлюзом к драйверам графических устройств и вспомогательной поддержкой для работы этих драйверов. Графический механизм Windows NT/2000 «скрыт» в DLL режима ядра, в которой также реализована функциональность управления окнами — то есть в wln32k.sys. В первоначальной реализации Windows NT главным фактором при выборе архитектуры операционной системы была безопасность, из-за чего пред-
Графический механизм 115 почтение отдавалось компактным, простым и стабильным решениям. До появления Windows NT 4.0 графический механизм и система управления окнами были реализованы в виде DLL пользовательского режима, являвшейся частью процесса подсистемы Win32 (csrss.exe). Когда приложение вызывало функцию управления окнами или графического вывода, на самом деле оно через механизм LPC обращалось к процессу подсистемы Win32. Последний обращался к графическому механизму или системе управления окнами из своего программного потока и возвращал результат приложению. Переключение процессов и потоков в этой архитектуре приводило к значительным затратам памяти и ресурсов процессора. В Windows NT 4. и новой Windows 2000 графический механизм и система управления окнами были перемещены в режим ядра. Теперь user32.dll и gdi32.dll вызывают системные функции, которые ntoskrnl передает win32k.sys без переключения процессов и потоков. Таким образом, win32k.sys можно рассматривать как опорную реализацию на уровне ядра двух важных модулей системы Windows: user32.dll и gdi32.dll. Библиотека wln32k.sys велика (1640 Кбайт в Windows 2000) — она даже больше ntoskrnl.exe (1465 Кбайт в Windows 2000). Внутренняя архитектура win32k.sys внешнему миру практически неизвестна. Microsoft документирует только одно: интерфейс DDI, используемый драйвером графического устройства. win32k.sys экспортирует около 200 функций — не так уж много по сравнению с 1200 функциями ntoskrnl.exe. По адресу www.sysinternal.com приведен полный листинг исходных текстов ядра Windows 2000 beta 1, реконструированный на основе отладочной сборки ОС. Однако это относится только к ntoskrnl.exe; для win32k.sys ничего похожего не существует. К счастью, документация по интерфейсу DDI (то есть интерфейсу между графическим механизмом и драйверами графических устройств) в Microsoft NT/2000 DDK написана очень хорошо. На рис. 2.7 показано, как графический механизм может выглядеть с архитектурной точки зрения (основанной на документации DDK и собственных исследованиях автора). Графический механизм имеет многоуровневую архитектуру. На верхнем уровне находится таблица системных функций, которая образует единственную точку входа из приложений пользовательского режима. Расположенные под ней интерфейсы DirectDraw, Direct3D и GDI общаются с драйвером экрана по более короткому пути. Для обычных вызовов GDI имеется уровень GDI API, который преобразует конструкции GDI в примитивы, понятные для механизма отображения DIB и драйверов графических устройств. Уровень GDI API использует диспетчер манипуляторов (handle manager) GDI для управления внутренними структурами данных, механизм визуализации для воспроизведения примитивов GDI на растровых поверхностях, поддерживаемых GDI, модуль масштабирования, драйверы для трех типов шрифтов GDI и другие компоненты. Графический механизм Windows NT/2000, в отличие от механизма Windows 95/98, обладает достаточной мощностью для воспроизведения всех примитивов DII на поверхностях стандартного формата DIB без помощи драйверов графических устройств. Для нестандартных растровых поверхностей графический механизм обращается к драйверам устройств и поручает им воспроизведение примитивов DDL Драйверы могут обратиться к графическому механизму с встречным запросом — например, затребовать дополнительную информацию, дать указание,
116 Глава 2. Архитектура графической системы Windows чтобы графический механизм разбил команды на более мелкие и даже попросить у механизма визуализации помочь с выводом. Рис. 2.7. Архитектура графического механизма Windows 2000 Ниже описаны отдельные компоненты графического механизма Windows 2000. Системные функции графического механизма Как упоминалось в разделе «Архитектура GDI», gdi32.dll содержит сотни графических функций, используемых библиотеками компонентов подсистемы Win32 (а именно, GDI, DirectDraw, Direct3D и OpenGL) для обращений к графическому механизму, находящемуся в ядре ОС. В тексте даже была приведена программа для вывода списка вызовов системных функций из этих DLL. Реальная обработка вызовов системных функций осуществляется модулем win32k.sys. В нем находится таблица системных функций, в которую входят системные функции графического механизма вместе с системными функциями системы управления окнами. В процессе инициализации win32k.sys таблица регистрируется диспетчером системных функций ОС, что обеспечивает быструю передачу вызовов системных функций графическому механизму. Если на вашем компьютере установлены отладочные символические файлы Windows NT/2000, то для просмотра символических имен win32k.sys можно воспользоваться программой dumpbin. Это довольно мощная утилита, которая может вызываться для РЕ-файлов Win32, объектных файлов и отладочных символических файлов. Ниже приведены две команды, позволяющие получить список всех символических имен в файле win32k.sys и провести в нем поиск слова Service (системная функция). dumpbin symbols\sys\win32k.dbg /all > tmpfile grep Service tmpfile
Графический механизм 117 Обнаруживается довольно интересное имя, _W32pServiceTable — это начальный адрес таблицы указателей на все обработчики системных функций win32k.sys. Программа SysCall (см. раздел «Архитектура GDI») позволяет перечислить элементы таблицы, преобразовать их в символические имена и вывести в окне. Запустите программу SysCall и выберите команду View ► System Call Tables ► Win32k.sys system call table — появляется список из 639 (в Windows 2000) обработчиков системных функций графического механизма и системы управления окнами (рис. 2.8). \~iai к( £&? %Ш ,i ' ' Ь:\UINNT50\System32\win32k.sys loaded p:\WINNT50\symbols\sys\win32k.dbg loaded. syscall(1000; syscall(100i; syscall(1002; syscall(1003; syscall(1004; syscall(1005; syscall(1006; syscall(1007; syscallClOOe1 syscall(1009 syscall(100a syscall(100b; syscall(100c syscall(100d syscall(100e syscall(100f syscall(1010; syscall(1011 |syscall(1012 ) NtGdiAbortDoc ) NtGdiAbortPath i NtGdiAddFontResource¥ i NtGdiAddRemoteFontToDC t NtGdiAddFontMemResourceEx i NtGdiRemoveMergeFont ) NtGdiAddRemoteMMInstanceToDC i NtGdiAlphaBlend i NtGdiAngleArc i NtGdiAnyLinkedFonts i NtGdiFontlsLinked ) NtGdiArcInternal ) NtGdiBeginPath ) NtGdiBitBlt ) NtGdiCancelDC i NtGdiCheckBitmapBits i NtGdiCloseFigure ) NtGdiColorCorrectPalette i NtGdiCombineRgn \ - --', - iinuii J£| Рис. 2.8. Список системных функций графического механизма ПРИМЕЧАНИЕ ■ Другим крупным поставщиком системных функций в Windows NT/2000 является исполнительная часть, находящаяся в файле ntoskrnl.exe. Ее функции вызываются через DLL подсистемы Win32 ntdll.dll. Они обеспечивают поддержку базового сервиса Win32, обычно называемого сервисом ядра, функции которого экспортируются главным образом из kernel32.dll. Исполнительная часть использует системные функции с идентификаторами, меньшими 0x1000, а остальные идентификаторы используются графическим механизмом и системой управления окнами. Программа SysCall позволяет получить список системных функций в ntdll.dll и содержимое таблицы системных функций в ntoskrnl.exe. В отличие от поиска всех мест, из которых вызываются системные функции, вывод содержимого таблицы системных функций для программы SysCall является элементарной задачей. Все, что для этого требуется, — непрерывно читать из загруженного образа win32k.sys содержимое адресов, начиная с W32pServiceTable,
118 Глава 2. Архитектура графической системы Windows и преобразовывать их в символические имена при помощи отладочного символического файла. После описания системных функций GDI (инициирующих прерывание 0х2Е) список обработчиков системных функций графического механизма (то есть фрагментов, обслуживающих прерывание 0х2Е для разных индексов функции) выглядит знакомо — разве что для win.32k.sys этот список упорядочен по индексу системной функции и заполнен. Для парных функций из gdi32.dll и win.32k.sys Microsoft использует одинаковые имена. Например, функция NtGdiAbortDoc с индексом 0x1000 присутствует в обеих таблицах. Имена функций графического механизма за редкими исключениями начинаются с NtGdi, а имена функций системы управления окнами — с NtUser. Как правило, для каждой системной функции графического механизма удается легко найти прототип среди функций GDI, DirectDraw, Direct3D, OpenGL или функций поддержки драйвера принтера. В остальных случаях системные функции могут предназначаться только для внутреннего использования. Примеры: О системная функция NtGdiAbortDoc, конечно, реализует функцию GDI AbortDoc; О системная функция NtGdiDdBIt имеет отношение к интерфейсу DirectDraw IDirectDrawSurface; О системные функции NtGdiDoBanding и NtGdiGetPerBandlnfo используются при печати страниц по полосам; О системные функции NtGdi Created ientObj и NtGdi Del eteClientObj на первый взгляд выглядят загадочно, но после прочтения главы 3 вы поймете, для чего они нужны; О системные функции NtGdiGetServerMetafileBits и NtGdiGetSpoolMessage явно используются в работе спулера. Механизм графической визуализации Перейдем к фундаменту всего графического механизма Windows NT/2000 — механизму графической визуализации (graphics render engine, GRE). После знакомства с ним вам будет гораздо проще понять, как работает графический механизм в целом. В Windows NT/2000 компания Microsoft включила полноценные средства визуализации для всех стандартных DIB-форматов, к числу которых относятся DIB с цветовой глубиной 1, 4, 8, 16, 24 и 32 бит/пиксел. Если устройство вывода использует один из этих форматов DIB, графический механизм не нуждается в помощи драйверов устройств для рисования линий, заливок, растров или текста. Напротив, драйвер графического устройства может прибегнуть к услугам графического механизма для реализации графических вызовов GDI. При желании драйвер устройства может построить изображение самостоятельно — например, для достижения быстродействия, сравнимого с DirectDraw/Direct3D, или при использовании особых аппаратных конфигураций. В результате уменьшается сложность обычных драйверов графических устройств, повышается стабильность операционной системы и ускоряется разработка продуктов как производителями оборудования, так и самой компанией Microsoft.
Графический механизм 119 GRE используется как графическим механизмом, так и драйверами графических устройств; в нем сосредоточена большая часть функций, экспортируемых графическим механизмом. Эти функции подробно документированы в разделе «GDI Functions for Graphics Drivers» Windows NT/2000 DDK. GRE API сильно отличается от Win32 GDI API. Ниже перечислены некоторые общие концепции, используемые GRE и графическим механизмом в целом. О Операции с растрами на уровне GDI. Поддерживаются все стандартные форматы DIB, в том числе несжатые DIB с цветовой глубиной 1, 4, 8, 16, 24 и 32 бит/пиксел, а также сжатые DIB с цветовой глубиной 4 и 8 бит/пиксел в кодировке RLE (Run Length Encoding). DIB может храниться в памяти как в прямом (bottom-down), так и в перевернутом (top-down) виде. Память для графических данных DIB может выделяться из адресного пространства ядра или из адресного пространства пользовательского процесса. Функция Епд- CreateBitmap, экспортируемая из win32k.sys, создает растр, находящийся под управлением GDI, и возвращает его манипулятор. О Координатное пространство. Для повышения точности вывода без применения вещественных вычислений GRE может работать с дробными координатами в формате с фиксированной точкой 28.4 (другими словами, старшие 28 бит определяют знаковую целую часть, а младшие 4 бита — дробную часть). Это так называемые «FIX-координаты», используемые при рисовании линий и кривых. В других компонентах API координаты представляются 28-разрядными целыми числами со знаком. Все вызовы графических функций проходят предварительную трансформацию координат, поэтому в GRE отсутствуют понятия оконных координат и области просмотра (viewport), расширенных и мировых преобразований. Максимальный размер DIB- поверхности равен 227 х 227 пикселам, то есть 1,42 км х 1,42 км при разрешении 2400 dpi. О Поверхности. GRE обеспечивает полный контроль лишь для одного типа поверхностей — растров, управляемых GDI (GDI-managed bitmaps). Драйверы устройств могут создавать поверхности, управляемые устройствами (device- managed), в формате DIB или других форматах при помощи функции Епд- CreateDeviceSurface. Затем драйвер управляет графическим выводом на такие поверхности. Примером поверхности Win32, управляемой устройством и не относящейся к формату DIB, являются аппаратно-зависимые растры (device- dependent bitmap). О Перехват и возврат вызовов. По умолчанию GRE производит весь вывод на поверхностях DIB, управляемых GDI. Однако драйвер устройства может перехватывать вызовы некоторых графических функций, чтобы реализовать их по-своему. Флаг fl Hook функции EngAssociateSurface определяет функции, перехватываемые драйвером. Например, драйвер может предоставить собственную функцию DrvBitBlt, для чего при вызове EngAssociateSurface передается флаг H00K_BITBLT. В результате запросы на выполнение блиттинга будут передаваться DrvBitBlt. Однако при вызове DrvBitBlt может определить, что операция слишком сложна. В этом случае драйвер возвращает запрос GDI, вызывая EngBitBlt.
120 Глава 2. Архитектура графической системы Windows О Графические примитивы. Графический механизм сводит многочисленные графические функции Win32 к небольшому количеству примитивов, которые и поддерживаются GRE. Ниже приведена краткая сводка примитивов: □ EngLineTo и EngStrokePath — все операции вывода линий и кривых; □ EngFillPath и EngPaint — заливка замкнутой области кистью; □ EngStroke и Fill Path — заливка замкнутой области кистью и обводка контура; □ EngBitBlt, EngPlgBlt, EngStretchBlt, EngStretchBltROP, EngCopyBits, EngAlphaBlend и EngTransparentBlt — вывод растров; □ EndGradientFill — градиентная заливка областей; □ EngTextOut — весь вывод текста. Кроме упрощенных графических примитивов GRE использует совершенно новый набор структур данных. Речь идет вовсе не о манипуляторах GDI; в действительности GRE существует ниже (или, если хотите, позади) прослойки манипуляторов GDI. Ниже перечислены некоторые С++-подобные классы объектов, используемых GRE: О CLIP0BJ — область отсечения; О PATH0BJ — траектория GDI (а также все кривые, преобразованные в траектории); О PAL0BJ и XLATE0BJ — преобразование цветов; О BRUSH0BJ - кисти и перья GDI; О F0NT0BJ — реализованный шрифт; О STR0BJ — позиции глифов в выводимом тексте; О XF0RM0BJ — используется для преобразования координат при работе с F0NT0BJ; О SURF0BJ — графическая поверхность. Хотя по сравнению с Win32 API GRE обладает гораздо более простым интерфейсом, основные проблемы связаны с реализацией всех мельчайших деталей. Только представьте себе, сколько разновидностей функций блиттинга вам понадобится — по меньшей мере 36. Обратитесь к отладочной информации win32k.sys — вы найдете в ней такие символические имена, как bSrcCopySRLE4D32, vSrcS24D32, vSrcCopyS24D8, vSrcCopyS32D16, vSrcCopyS4D4Identify, BltLnkSrcCopyMsk32 и т. д. Похоже, первое имя, bSrcCopySRLE4D32, принадлежит функции для копирования 4-битных растров-источников, сжатых в кодировке RLE, в 32-разрядный растр-приемник. Третье имя, vSrcCopyS24D8, наводит на мысль о функции для копирования 24-битного источника в 8-битный приемник. Не забывайте о существовании 16 бинарных и 256 тернарных растровых операций, не говоря уже о кватернар- ных операциях, объединяющих две тернарные операции. Структуры данных графического механизма GDI, как и многие другие компоненты Win32 API, скрывает свою реализацию от программистов при помощи манипуляторов (handles) объектов GDI. Этот уровень абстракции чрезвычайно полезен для определения более или менее
Графический механизм 121 общего интерфейса Win32 API вместо разных реализаций для Win32s, Windows 95/98 и Windows NT/2000. Как и в любой абстрактной прослойке, кто-то в конечном счете должен управлять манипуляторами GDI, обеспечивая возможность их использования как в системных DLL пользовательского режима, так и в режиме ядра. Модуль, управляющий манипуляторами GDI, называется диспетчером манипуляторов GDI (GDI handle manager). Следующий вопрос посложнее — как организовано управление структурами данных в DLL пользовательского режима и в механизме режима ядра и как информация Win32 GDI о контекстах устройств, логических перьях, логических кистях, шрифтах, регионах, траекториях и т. д. преобразуется в структуры данных графического механизма? Знание структур данных, используемых в работе GDI, поможет программисту лучше понять реализацию GDI API и оптимизировать свои программы. Мы исследуем эту тему в главе 3 и попытаемся найти ответы на поставленные вопросы. Преобразование в примитивы Между Win32 GDI API и графическими примитивами, поддерживаемыми GRE (которые также образуют интерфейс DDI с драйверами устройств), существует заметный разрыв. За преобразование вызовов Win32 GDI API в примитивы GRE отвечает уровень GDI API. Ниже перечислены некоторые отличия, требующие преобразований. О Система координат. Win32 API поддерживает гибкую систему координат с преобразованиями между областями просмотра и окнами, режимами отображения и мировыми преобразованиями, тогда как GRE/DDI работает в физических координатах, масштаб которых определяется размером поверхности вывода. Координаты в вызовах функций Win32 API должны преобразовываться в физические координаты графической поверхности. О Эллиптические кривые. В интерфейсе GRE/DDI не поддерживаются эллиптические кривые вроде кругов или эллипсов — эти кривые должны преобразовываться в кривые Безье. В главе 3 приведен пример представления эллипса четырьмя кривыми Безье. В процессе преобразования графический механизм нуждается в эмуляции вычислений с плавающей точкой. Координаты с фиксированной точкой, используемые интерфейсом DDI, повышают точность конечного результата. О Преобразование кривых в траектории. Интерфейс DDI работает только с прямыми линиями и траекториями, поэтому все кривые нуждаются во внутреннем преобразовании в объекты траекторий. Графический механизм имеет богатый набор внутренних функций для операций с объектами траекторий. Если поверхность управляется устройством, от драйвера устройства требуется обязательная поддержка минимального набора функций — DrvPaint, DrvCopyBits, DrvTextOut и DrvStrokePath. Остальные функции разбиваются графическим механизмом на совокупность этих операций. Ниже перечислены некоторые из действий, выполняемых GDI.
122 Глава 2. Архитектура графической системы Windows О При выводе косметических линий и кривых функция DrvStrokePath должна поддерживать сплошные и стилевые косметические линии с закраской однородной кистью и отсечением. В реализации DrvStrokePath драйвер может вызывать служебные функции объектов PATH0BJ и CLIP0BJ для разбиения параметров до линий толщиной в 1 пиксел и прямоугольников отсечения. Если траектория или область отсечения окажется слишком сложной, драйвер может переадресовать вызов графическому механизму, который разбивает вызов до линий толщиной в 1 пиксел с заранее вычисленным отсечением. Для разбиения стилевых линий и кривых Безье GDI аппроксимирует их отрезками прямых линий. О Геометрические линии обладают атрибутами толщины, стилем соединения (join-style) и завершением (end-cap). Если драйвер устройства не справляется с выводом такой линии, он преобразует вызов функции в более простые вызовы DrvFillPath или DrvPaint. В этом случае операция вывода линии преобразуется в заливку области. О Для заливки областей драйвер должен поддерживать DrvPaint. Реализация DrvPaint может воспользоваться служебными функциями CLIP0BJ для разбиения сложной области отсечения на совокупность прямоугольников отсечения. О Для функций блиттинга драйвер должен поддерживать функцию DrvCopyBits, которая бы выполняла блочную пересылку графических данных на стандартный DIB или из него, а также на растр в формате устройства, с произвольным отсечением. DrvCopyBits выполняет простое копирование без растяжения, зеркального отражения или применения растровых операций. Если драйвер устройства ограничивается поддержкой DrvCopyBits, графический механизм должен самостоятельно эмулировать нужную операцию в памяти и применить DrvCopyBits к результату. Связь между графическим механизмом и драйверами устройств чем-то напоминает связь «родитель — потомок». Графический механизм обеспечивает всю поддержку, необходимую для драйверов устройств. Драйверы могут делать все, что угодно, чтобы превзойти быстродействие графического механизма, но когда возникают затруднения, они обращаются к графическому механизму за помощью. Шрифтовые драйверы В Windows NT 4.0/2000 существует особая разновидность драйверов графических устройств, поставляющих системе контуры или растровые изображения глифов шрифтов. Такие драйверы называются шрифтовыми драйверами (font drivers). На системном уровне в ОС Windows поддерживаются шрифты трех типов, основанные на применении разных технологий. Растровые шрифты представляют собой растровые изображения символов, символы векторных шрифтов строятся из отрезков прямых, а шрифты TrueType основаны на кривых Безье и хитроумном механизме разметки (hinting). Соответственно, в своей внутренней работе графический механизм использует три шрифтовых драйвера — для растровых шрифтов, для векторных шрифтов и для шрифтов TrueType.
Драйверы экрана 123 В системе также могут использоваться внешние шрифтовые драйверы. Например, в драйвер принтера может входить шрифтовой драйвер, снабжающий графическую систему информацией о шрифтах принтера. Другой пример — шрифтовой драйвер ATM (Adobe Type Manager) atmfd.dll, входящий в поставку Windows 2000. Шрифтовой драйвер ATM обеспечивает поддержку шрифтов ATM, основанных на технологии Adobe для работы со шрифтами PostScript. Драйверы экрана Драйвер экрана в Windows NT/2000 относится к категории драйверов графических устройств. Как правило, драйверы графических устройств представляют собой DLL режима ядра, загружаемые в адресное пространство ядра. Они отвечают за итоговую реализацию графических вызовов, передаваемых пользовательским приложением устройству. В Windows NT/2000 драйверы устройств всегда являются DLL режима ядра. Только драйверы принтеров в Windows 2000 могут быть реализованы как DLL пользовательского режима. Интерфейс между графическим механизмом GDI и драйвером графического устройства называется DDI (Device Driver Interface), то есть «интерфейс драйвера устройства». Почему в данном случае используется обобщенный термин «драйвер устройства», хотя речь идет только о драйверах графических устройств? Вероятно, потому, что в прежние времена графические драйверы составляли единственную заметную категорию драйверов, поставляемых разработчиками оборудования. Драйвер видеопорта и мини-драйвер видеопорта У каждого драйвера экрана существует парный ему мини-драйвер видеопорта, работающий в режиме ядра. Префикс «мини» говорит о том, что существует другой, «макси»-драйвер, управляющий работой мини-драйвера. В данном случае мини-драйвером видеопорта управляет драйвер видеопорта. Драйвер видеопорта и мини-драйвер видеопорта управляют всем взаимодействием системы с видеоадаптером, включая инициализацию и распознавание карты, отображение на память, обращения к регистрам видеоадаптера и т. д. Мини-драйвер видеопорта может отображать регистры видеоадаптера в пространство памяти драйвера, что позволяет работать с ними через стандартный механизм обращения к памяти. В системе Windows 2000, спроектированной с расчетом на поддержку DirectX на уровне драйвера видеоадаптера, мини-драйвер видеопорта также отвечает за поддержку DirectX. Например, одним из важнейших преимуществ DirectX перед GDI является то, что пользовательскому приложению предоставляется прямой доступ к буферу видеопамяти. Такая возможность достигается при помощи мини-драйвера видеопорта, который отображает буфер в область виртуальных адресов, доступную для пользовательских приложений. Драйвер экрана взаимодействует с мини-драйвером видеопорта посредством вызовов функции EngDeviceloControl графического механизма, которые переда-
124 Глава 2. Архитектура графической системы Windows ются диспетчером ввода-вывода ядра NT драйверу видеопорта, а затем поступают к мини-драйверу видеопорта. Назначение драйвера экрана Хотя прямой доступ к видеооборудованию предоставляется мини-драйвером видеопорта, обычно он находится под управлением драйвера экрана. Задачи, решаемые драйвером экрана, делятся на четыре класса. О Предоставление и запрет доступа к ресурсам графического оборудования, включая отображение видеопамяти, банки и внеэкранную кучу, аппаратный курсор мыши, аппаратную палитру, кэш кистей и аппаратную поддержку DirectDraw/Direct3D/OpenGL (если она присутствует). Как правило, драйвер экрана передает запросы драйверу видеопорта. О Передача механизму GDI сведений о возможностях оборудования и драйвера через специальные структуры данных GDI. О Создание и актуализация поверхностей. Это могут быть как DIB-поверхно- сти, управляемые GDI, так и DIB-поверхности, управляемые устройством, а также поверхности других форматов, управляемые устройством. О Реализация основных (или всех) графических операций с поверхностью посредством создания перехватчиков. Драйвер экрана также может создать поверхность, управляемую GDI, и разрешить графическому механизму работать с ней напрямую. Инициализация драйвера экрана Драйвер экрана обычно представляет собой DLL режима ядра, которая импортирует функции только из графического механизма (win32k.sys). Главная точка входа драйвера экрана, по смещению совпадающая с DllMainCRTStartup, обычно называется DrvEnableDriver. Функция DrvEnableDriver обычно вызывается GDI после загрузки драйвера. Драйвер выполняет простую проверку версии и возвращает механизму GDI таблицу функций, в которой перечислены все поддерживаемые им функции DDL Таблица возвращается в виде структуры DRVENABLEDATA. Каждая функция DDI, поддерживаемая драйвером экрана Windows NT/2000, обладает заранее определенным индексом. В Windows 2000 в общей сложности определяется 89 функций DDL Например, у DrvEnableDriver существует парная функция DrvDisableDriver, имеющая индекс 8. После получения таблицы функций графический механизм обычно вызывает DrvEnablePDEV, чтобы драйвер создал экземпляр физического устройства и вернул важную информацию о графическом оборудовании и драйвере. При вызове DrvEnablePDEV графический механизм передает драйверу копию структуры DEVM0DEW, описывающей характеристики графического устройства. Для видеоадаптеров DEVM0DEW определяет частоту развертки, разрешение и особые режимы (например, вывод в оттенках серого или чересстрочный (interlaced) вывод). Для принтеров DEVM0DEW содержит еще более важную информацию о типе и размере бумаги, ориентации, качестве печати, количестве копий и способе подачи.
Драйверы экрана 125 DrvEnablePDEV создает экземпляр структуры PDEV (Physical DEVice), определяемой драйвером и содержащей закрытые данные драйвера. Функция возвращает манипулятор структуры PDEV, по которому механизм GDI ссылается на данный экземпляр физического устройства при последующих вызовах. Кроме того, DrvEnablePDEV заполняет структуру GDI INFO, из которой GDI получает информацию о разрешении устройства, физическом размере, цветовом формате, битах DAC, коэффициенте вертикального сжатия, размере палитры, порядке цветовых плоскостей, размере и формате полутонового узора, частоте обновления и т. д. Информация также возвращается в структуре DEVINF0, описывающей графические возможности драйвера, шрифты по умолчанию, количество шрифтов устройства и формат смешивания цветов. Флаги графических возможностей сообщают графическому механизму, поддерживает ли устройство обработку кривых Безье, геометрическое расширение, типы заполнения многоугольников (ALTERNATE или WINDING), печать EMF, сглаживание текста, аппаратную растеризацию шрифтов, JPEG, загрузку гамма-таблиц, аппаратную поддержку альфа-курсора и т. д. После вызова DrvEnablePDEV графический механизм производит собственную внутреннюю инициализацию физического устройства и в завершение вызывает DrvCompleteDEV, сообщая тем самым, что физическое устройство готово к работе. При завершении использования PDEV вызывается функция DrvDisablePDEV, которая обычно освобождает память, выделенную для физического устройства. Прежде чем GDI начнет вывод на устройство, графический механизм вызывает DrvEnableSurface, чтобы драйвер создал графическую поверхность. Если видеоадаптер работает с кадровым буфером в стандартном формате DIB, он создает поверхность, управляемую GDI, путем вызова EngCreateBitmap. В противном случае драйвер экрана самостоятельно выделяет память для поверхности и при помощи EngCreateDeviceSurface сообщает графическому механизму размер и совместимый формат поверхности. В любом случае драйвер устройства затем вызывает EngAssociateSurface, указать, какие вызовы графических операций DDI должны перехватываться драйвером. При этом используется информация из таблицы функций, возвращаемой при вызове DrvEnableDriver. Завершив работу с поверхностью, графический механизм вызывает DrvDisableSurface, чтобы разрешить драйверу освободить все выделенные ресурсы. Вывод на поверхность, перехват и возврат После успешного создания поверхности графический механизм передает драйверу устройства графические вызовы в соответствии с установленными битами возможностей и флагами перехвата (hooking flags) для поверхности. В табл. 2.1 перечислены все операции вывода DDI, которые могут перехватываться при выводе на поверхность. Как видно из таблицы, у каждой графической операции DDI в механизме GRE имеется аналог с точно совпадающими параметрами, предназначенный для выполнения этой операции на стандартной поверхности в формате DIB. Ниже перечислены варианты реализации графических вызовов DDI драйвером. О Для поверхностей в стандартном формате DIB драйвер может отказаться от перехвата графических функций DDI и поручить обработку всех графических операций GRE. Так, пример драйвера из Windows 2000 DDK не перехватывает ни одной функции (ddk\src\video\displays\framebuf).
126 Глава 2. Архитектура графической системы Windows Таблица 2.1. Перехватываемые графические операции Windows 2000 Индекс H00K_BITBLT H00KSTRETCHBLT H00K_PLGBLT H00KTEXT0UT H00KPAINT HOOKJTROKEPATH H00KJILLPATH H00K_STR0KEANDFILLPATH H00KLINET0 H00KC0PYBITS HOOKMOVEPANNING H00K_SYNCHR0NIZE H00K_STRETCHBLTR0P HOOKSYNCHRONIZEACCESS H00K_TRANSPARENTBLT H00K_ALPHABLEND H00K_GRADIENTFILL Функция графического механизма EngBitBIt EngStretchBlt EngPlgBlt EngTextOut EngPaint EngStrokePath EngFillPath EngStrokeAndFillPath EngLineTo EngCopyBits EngMovePanning EngSynchronize EngStretchBltROP EngSynchroni zeAccess EngTransparentBlt EngAlphaBlend EngGradientFill Функция драйвера DrvBitBlt DrvStretchBlt DrvPlgBlt DrvTextOut DrvPaint DrvStrokePath DrvFillPath DrvStrokeAndFillPath DrvLineTo DrvCopyBits DrvMovePanning DrvSynchronize DrvStretchBltROP DrvSynchroni zeAccess DrvTransparentBlt DrvAlphaBlend DrvGradientFill О Если поверхность управляется устройством, драйвер может перехватывать несколько обязательных примитивов и предоставить графическому механизму разбивку всех остальных вызовов на примитивы. Драйвер также может перехватывать все графические вызовы для применения аппаратного ускорения или оптимизированной программной реализации. Скажем, пример драйвера s3virge из Windows 2000 DDK обеспечивает оптимизированную ассемблерную реализацию для некоторых видов блиттинга между экранным буфером и внеэкранными DIB. В этом случае вызовы DrvBitBlt должны перехватываться. О Функция-перехватчик, реализуемая драйвером, может вызывать системные функции графического механизма для разбиения сложной области отсечения на более простые группы прямоугольников. Драйвер также может возвращать вызовы GRE, когда он не справляется с поставленной задачей. Например, упоминавшийся выше драйвер s3virge для выполнения блиттинга между двумя внеэкранными DIB возвращает вызов графическому механизму.
Драйверы экрана 127 Дополнительные возможности драйвера Помимо инициализации/завершения и перехвата графических вызовов DDI, драйвер экрана также должен представить графическому механизму точки входа для выполнения следующих операций: О управление растрами устройств: DrvEnableDeviceBitmap и DrvDisableDeviceBitmap; О управление палитрой: DrvSetPalette; О реализация кистей, смешение цветов (dithering) и поддержка ICM (Image Color Management): DrvRealizeBrush, DrvDitherColor, DrvIcmCreateColorTransform, DrvIcmCheckBitmapsBits и т. д.; О обходные обращения к GDI: DrvEscape, DrvDrawEscape (например, для сквозной передачи данных PostScript); О управление мышью: DrvSetPointerShape, DrvMovePointer; О получение информации о шрифтах и поддержка шрифтовых драйверов: Drv- QueryFont, DrvQueryFontTree, DrvQueryFontData, DrvQueryFontFile, DrvQueryTrueType- FontTable и т. д.; О печать: DrvQuerySpoolType, DrvStartDoc, DrvEndDoc, DrvStartPage, DrvEndPage и т. д. (печать подробно рассматривается в главе 3); О поддержка OpenGL: DrvSetPixelFormat, DrvSwapBuffers и т. д.; О поддержка DirectDraw/Direct3D: DrvEnableDirectDraw, DrvGetDirectDrawInfo и DrvDi sableDi rectDraw. Многие из перечисленных функций являются необязательными и реализуются драйвером графического устройства лишь при наличии у устройства соответствующих аппаратных возможностей. Поддержка DirectDraw/Direct3D на уровне драйвера экрана Если драйвер экрана поддерживает DirectDraw/Direct3D, он должен экспортировать точку входа DrvGetDirectDrawInfo, через которую начинается взаимодействие графического механизма с аппаратной поддержкой DirectDraw. Когда приложение DirectDraw/Direct3D создает экземпляр объекта DirectDraw, графический механизм сначала вызывает DrvGetDirectDrawInfo, чтобы получить от драйвера экрана информацию о поддержке DirectDraw. DrvGetDirectDrawInfo возвращает GDI сведения о поддержке DirectDraw/Direct3D, включая описание аппаратных возможностей и список поддерживаемых форматов. Информация об аппаратных возможностях (аппаратный блиттинг, масштабирование, поддержка альфа-канала, отсечение, цветовые ключи, оверлеи, палитры, поддержка Direct3D и т. д.) кодируется в структуре DDHALINFO. Информация о форматах видеопамяти возвращается в виде массива структур VIDE0MEM0RYINF0. Каждый формат кодируется 32-разрядным значением FOURCC, которое служит для обозначения типа носителя в мультимедийном API. DirectDraw использует FOURCC для описания форматов пикселов, поддерживаемых видеоадаптерами, и форматов пикселов в сжатых текстурах.
128 Глава 2. Архитектура графической системы Windows Затем графический механизм вызывает функцию DrvEnableDirectDraw, тем самым приказывая драйверу включить аппаратную поддержкуБ1гес1Вга\у. Функция DrvEnableDirectDraw заполняет три структуры адресами функций косвенного вызова для интерфейсов IDirectDraw, IDirectDrawSurface и IDirectDrawPalette. Нечто похожее происходит в главной точке входа драйвера экрана, DrvEnableDriver, возвращающей индексированный список функций косвенного вызова для реализации DDL Каждая из трех структур данных, возвращаемых DrvEnableDirectDraw, соответствует одному из основных компонентов интерфейса DirectDraw (см. описание DirectDraw API). В структуру входит поле флагов, определяющее поддерживаемые функции косвенного вызова, и указатели на все функции косвенного вызова данного интерфейса. Например, структура DDCALLBACKS относится к реализации DirectDraw и содержит информацию о девяти функциях косвенного вызова. Если в поле dwFlags присутствует флаг DDHALCB32CREATESURFACE, то поле Create- Surface содержит адрес функции косвенного вызова, которой обычно присваивается имя DdCreateSurface. В действительности интерфейс IDirectDraw содержит более девяти методов. Часть методов реализуется внутри клиентской DLL DirectDraw Win32, а остальные функции уровня драйвера возвращаются в других структурах (например, DD_NTCALLBACKS). Поддержка Direct3D со стороны драйвера экрана обозначается несколькими флагами в структуре DDHALINFO, возвращаемой DrvGetDirectDrawInfo. Флаг DDCAPS3D в поле ddCaps.dwCaps означает, что обслуживаемое драйвером устройство поддерживает ускорение трехмерной графики. Флаги поля ddCaps.ddsCaps (например, DDSCAPS3DDEVICE, DDSCAPSTEXTURE и DDSCAPSZBUFFER) описывают трехмерные возможности для поверхности видеопамяти. Кроме того, DDHALINFO содержит указатель на структуру D3DNTHALCALLBACKS, содержащую адреса функций косвенного вызова DDI для Direct3D. В табл. 2.2 перечислены некоторые структуры, в которых передаются сведения о функциях поддержки DirectDraw/Direct3D в драйвере экрана. Таблица 2.2. Функции косвенного вызова DDI, обеспечивающие поддержку DirectDraw/Direct3D (неполный список) Структура Функции DD_CALLBACKS DdDestroyDriver, DdCreateSurface, DdSetColorKey, DdSetMode, DdWai tForVerti cal Bl ank, DdCanCreateSurface, DdCreatePalette, DdGBetSeal Line, DdMapMemory DD_SURFACECALLBACKS DdDestroySurface, DdFli p, DdSetCli pLi st, DdLock, DdUnlock, DdBlt, DdSetColorKey, DddAddAttachedSurface, DdGetBltStatus, DdGetFlipStatus, DdUpdateOverlay, DdSetOverlayPositions, DdSetPalette DD_PALETTECALLBACKS DdDestroyPalette, DdSetEntri es DD_NTCALLBACKS DdFreeDriverMemory, DdSetExclusiveMode, DdFlipToGDISurface DD_C0L0RC0NTR0LCALLBACKS DdColorControl DD_MISCELLANEOUSCALLBACKS DdGetAvai1Dri verMemory
Драйверы принтеров 129 Структура Функции D3DNTHAL_CALLBACKS D3dContextCreate, D3dContextDestroy, D3dContextDestroyAl1, D3dSceneCapture, D3dTextureCreate, D3dTextureDestroy, D3dTextureSwap, D3dTextureGetSurf D3DNTHAL_CALLBACKS3 D3dClear2, D3dValidateTextureStageState, D2dDrawPrinritives2 Даже при кратком знакомстве с интерфейсами GDI DDI и DirectDraw/ Direct3D DDI становится очевидным, что они имеют абсолютно разную архитектуру. Ниже перечислены наиболее принципиальные различия. О Интерфейс GDI DDI работает на более примитивном уровне, нежели Direct- Draw/Direct3D DDI. Следовательно, перед обращением к интерфейсу GDI DDI графическому механизму приходится выполнить большое количество предварительных операций, тогда как путь DirectDraw/Direct3D, ведущий от Win32 API, проще и прямее. О В поддержке GDI DDI драйвер экрана всегда может получить помощь от графического механизма, вернув ему полученный вызов. В интерфейсе Direct- Draw/Direct3D DDI программная эмуляция выполняется в клиентской DLL пользовательского режима, поэтому драйвер экрана не сможет воспользоваться помощью уровня драйвера режима ядра. О Для получения информации о драйверных функциях косвенного вызова в интерфейсе GDI DDI используется простой и легко расширяемый способ, тогда как в DirectDraw/Direct3D DDI функции косвенного вызова описываются массивом структур данных. Итак, мы рассмотрели процесс инициализации драйвера экрана для поддержки GDI, DirectDraw и Direct3D и даже кое-что узнали об основных точках входа и функциях косвенного вызова драйвера. Мы еще вернемся к этой теме и посмотрим, как эти функции косвенного вызова используются графическим механизмом, при исследовании внутренних структур данных графической системы Windows (глава 3) и отслеживании работы GDI/DirectDraw (глава 4). Драйверы принтеров Драйвер экрана, описанный в предыдущем разделе, составляет всего лишь один из классов драйверов графических устройств, поддерживаемых ОС. Другой важный класс драйверов графических устройств управляет работой устройств создания жестких копий — принтеров, плоттеров, факсов и т. д. Драйверы этих графических устройств имеют одинаковую структуру, поэтому все, что будет сказано о драйвере принтера, в принципе относится и к драйверу факса. В соответствии с технологией вывода устройства создания жестких копий делятся на три класса. О Текстовые устройства — традиционные устройства строчной печати, способные выводить только обычный текст. В среде Windows, ориентированной на графический интерфейс пользователя в режиме WYSIWYG, они встречают-
130 Глава 2. Архитектура графической системы Windows ся довольно редко. Драйвер устройства передает текстовому устройству текстовый поток с минимальным форматированием (разрывы строк и страниц). О Растровые устройства — к этой категории относятся матричные принтеры, факсы и большинство струйных принтеров. Драйвер устройства должен уметь преобразовывать графические команды DDI в растровое изображение и кодировать его на языке принтера (PCL3, ESC/2 и т. д.). О Векторные устройства — лазерные принтеры, плоттеры, принтеры PostScript, а также некоторые современные модели DeskJet. Хотя на некоторых из этих устройств непосредственная печать происходит в растровом режиме, все они принимают входные данные в векторном формате, а растровое преобразование производится самим принтером. Драйвер векторного устройства обычно преобразует графические команды DDI в команды на языке принтера — например, PCL5, PCL6, HPGL, HPGL/2 или PostScript. Векторные устройства наряду с векторными данными обычно принимают и растровые данные. Полный драйвер принтера для Windows NT/2000 состоит из нескольких компонентов, из которых обязательными являются лишь первые два: О DLL графического вывода, которая (как и драйвер экрана) получает графические команды DDI, переводит их на язык принтера и отправляет данные спулеру; О интерфейсная DLL, обеспечивающая пользовательский интерфейс к параметрам конфигурации принтера и спулеру для управления установкой, конфигурацией и выводом сообщений об ошибках; О необязательный процессор печати помогает процессу спулера передавать задания на печать; О необязательный языковой монитор обеспечивает двустороннюю связь между спулером и пользователем; О необязательный монитор порта передает готовые к печати данные драйверам аппаратных портов. Управляющие драйверы принтеров от Microsoft Компания Microsoft создала несколько стандартных драйверов, которые могут использоваться производителями принтеров для подключения специализированных драйверов в виде модулей (вместо разработки полноценных драйверов). О Универсальный драйвер принтера (Unidrv) предназначен для принтеров без поддержки PostScript — например, матричных принтеров, DeskJet и LaserJet. Производителю принтера остается лишь предоставить мини-драйвер для Unidrv, который в минимальном варианте представляет собой текстовый GPD-файл с описанием возможностей принтера, параметров, условных ограничений и команд принтера. Архитектура Unidrv допускает использование подключаемых модулей (plug-ins). Модуль визуализации обеспечивает нестандартную обработку графических команд, полутоновые преобразования и построение данных, готовых к передаче на принтер. Модуль пользовательского интерфейса позволяет настраивать страницы свойств принтера, структуру DEVM0DE и процесс обработки событий печати.
Драйверы принтеров 131 О Драйвер принтера PostScript (Pscript) предназначен для принтеров с поддержкой PostScript. Мини-драйвер PostScript состоит из текстового PPD-файла с описанием характеристик принтера, двоичного NTF-файла с описанием шрифтов принтера, модуля визуализации и модуля пользовательского интерфейса. О Драйвер плоттера представляет собой стандарт Microsoft для поддержки плоттеров, совместимых с языком HPGL/2 (Hewlett-Packard Graphics Language). Мини-драйвер плоттера представляет собой двоичный PCD-файл с описанием характеристик плоттера. Модули для него не создаются, поскольку язык HPGL/2 имеет достаточно жесткую структуру. Windows 2000 DDK содержит полный исходный текст драйвера плоттера от Microsoft. В стандартных драйверах Microsoft использована очень интересная архитектура, управляемая данными. Эти драйверы поддерживают тысячи моделей всевозможных принтеров, представленных на рынке, а отличия между ними часто нивелируются до небольших различий в файлах данных. GPD-файл драйвера Unidrv во всех подробностях описывает ориентацию листа, входной лоток, размер бумаги, разрешение, режим печати, тип носителя, цветовой режим, качество печати, полутоновые преобразования, конфигурационные ограничения, команды конфигурации принтера и команды печати. Нередко бывает так, что при выпуске новой модели принтера производителю остается лишь обновить GPD- файл и внести в него сведения о новом режиме печати с повышенным разрешением. GPD-файлы пишутся на достаточно выразительном языке с поддержкой простейших типов данных (целые числа, пары, строки и списки) и даже переменных с командами выбора. И все же интересно, почему Microsoft не воспользовалась стандартными языками типа Lisp или Prolog, которые обладают большими возможностями и легче обрабатываются? Графическая библиотека DLL драйвера принтера Графическая DLL драйвера принтера очень похожа на драйвер экрана, рассматривавшийся в разделе «Драйверы экрана». Главное отличие заключается в том, что в драйвере принтера должны присутствовать дополнительные точки входа для управления документами и страничным выводом, но зато не нужны точки входа для поддержки курсора мыши, DirectDraw и Direct3D. Разумеется, драйвер принтера должен поддерживать основные функции, отвечающие за инициализацию драйверов графических устройств, — а именно, DrvEnableDriver, DrvEnablePDEV, DrvCompletePDEV, DrvDisablePDEV, DrvEnableSurface, DrvDisableSurface и, наконец, DrvDisableDriver. Драйвер принтера также должен обеспечивать разбивку печатного документа на отдельные страницы и запросы, специфические для конкретного принтера. В табл. 2.3 перечислены дополнительные точки входа, которые должны или могут поддерживаться принтером. У драйверов принтеров Windows 2000 есть одна интересная особенность — они могут существовать не только в виде DLL режима ядра, но и в режиме пользовательских DLL. Microsoft прикладывает особые усилия к выводу драйверов принтеров из адресного пространства ядра в пользовательское адресное пространство. Но помните: драйвер принтера может импортировать функции графического механизма win32k.sys, недоступные на уровне GDI. Для решения этой
132 Глава 2. Архитектура графической системы Windows проблемы Windows 2000 GDI экспортирует подмножество функций графического механизма, чтобы драйвер принтера пользовательского режима мог работать с ними напрямую. Графический механизм особым образом передает вызовы, обращенные к драйверу принтера, из режима ядра в пользовательский режим, после чего GDI преобразует обращения к механизму от драйвера из пользовательского режима обратно в режим ядра. Драйверы принтеров, работающие в пользовательском режиме, обладают рядом преимуществ, в числе которых — снижение стоимости разработки, повышенная гибкость используемых средств Win32 API и, что еще важнее, — снижение степени вмешательства в ядро ОС. Если драйвер принтера работает в пользовательском режиме, то DLL драйвера должна экспортировать функции DrvEnableDriver, DrvDisableDriver и DrvQueryDriver, причем функция DrvQueryDriverlnfo должна выдавать соответствующую информацию при обработке запроса DRVQUERYUSERMODE. Все стандартные драйверы принтеров Microsoft Windows 2000 являются драйверами пользовательского режима. Таблица 2.3. Специализированные точки входа драйвера принтера Точка входа Назначение DrvQueryDriverlnfo (необязательна) DrvQueryDeviceSupport (необязательна) DrvStartDoc DrvEndDoc DrvStartPage DrvSendPage DrvStartBanding DrvQueryPerBandlnfо (необязательна) DrvNextBand (необязательна) Интерпретация запроса зависит от драйвера. В настоящее время используется для получения информации от драйверов пользовательского режима Интерпретация запроса зависит от устройства. В настоящее время используется для запросов о поддержке JPEG и PNG Сообщает драйверу о готовности GDI к передаче документа Сообщает драйверу о завершении передачи документа Сообщает драйверу о готовности GDI к передаче графических команд новой страницы Сообщает драйверу о завершении передачи графических команд страницы — драйвер может передать обработанные данные спулеру У драйвера запрашивается информация о том, где на странице должна начинаться разбивка на полосы У драйвера запрашивается структура PERBANDINF0 с информацией о размере и разрешении полосы Сообщает драйверу о завершении передачи графических команд полосы — драйвер может передать обработанные данные спулеру Главной точкой входа драйвера принтера по-прежнему остается DrvEnable- Printer. Когда приложение вызывает CreateDC, чтобы создать контекст устройства для принтера, графический механизм проверяет, загружен ли драйвер принтера. Если драйвер не загружен, он загружается, после чего вызывается функция DrvEnableDriver.
Драйверы принтеров 133 Функция DrvEnablePDEV драйвера принтера вызывается графическим механизмом, когда приложение вызывает функцию CreateDC для принтера. По сравнению с драйвером экрана эта функция сложнее, поскольку ей приходится учитывать многочисленные параметры печати, переданные в структуре DEVM0DE. В частности, драйверу приходится регулировать разрешение вывода в зависимости от выбранного качества печати, переключать размеры бумаги для альбомного (landscape) режима, вычислять размеры области вывода на основании размера бумаги и передавать информацию о полях. В структуре DEVM0DE могут передаваться и другие параметры печати — режим двусторонней печати, разбор по копиям, количество экземпляров, количество страниц на листе, а также специализированные параметры, обеспечиваемые другими компонентами системы печати. Например, в Windows 2000 двусторонняя печать, разбор по копиям, печать нескольких эк- земпляров-и режим печати нескольких страниц на листе реализуются стандартным процессором печати Windows 2000, благодаря чему базовый драйвер принтера должен обеспечивать лишь вывод отдельной страницы. Драйвер растрового принтера может создать поверхность, управляемую графическим механизмом, при вызове DrvEnableSurface и затем потребовать, чтобы графический механизм выполнял весь вывод (или его большую часть). Однако при этом возникает проблема — принтер работает на значительно большем разрешении, чем экран монитора, поэтому одновременное воспроизведение всей страницы потребует чрезмерных затрат памяти. Например, страница формата Letter в разрешении 300 х 300 dpi состоит из 300 х 300 х 11,5 х 8 пикселов — получается около 8 мегапикселов. При одноцветной печати на 300 dpi страничный растр занимает около 1 мегабайта, а при печати на цветном принтере с разрешением 600 dpi и 24-битным цветом размер страничного растра приближается к 96 мегабайтам. Чтобы свести огромные затраты памяти до разумного уровня, графический механизм позволяет разделить страницу на горизонтальные полосы. Если разделить страницу формата Letter на равные полосы шириной в 1 дюйм, 96 мегабайт уменьшаются до 4,17 мегабайта. В этом случае драйвер должен при помощи функции EngMarkBandingSurface сообщить графическому механизму о том, что при выводе поверхности используется разбивка. Векторному принтеру не нужно воспроизводить сразу всю страницу в виде растра; вместо этого он последовательно транслирует графические команды DDI в команды принтера. Как правило, создается поверхность, управляемая устройством, и драйвер перехватывает графические команды DDL На векторных принтерах разбивка обычно не применяется. После загрузки драйвера и создания структур данных для физического устройства и поверхности GDI при содействии графического механизма передает весь документ драйверу принтера. Процесс вывода выглядит примерно так: DrvStartDocCDocName. Jobld) for (int i = 0; i<nPageNo; i++) { DrvStartPageO: DrvStartBandingO; for (BOOL bMoreBands=TRUE; bMoreBands; ) { DrvQueryPerBandInfo(); for (c=0: c<nCommands; C++) )
134 Глава 2. Архитектура графической системы Windows DrawCcommandCc]): bMoreBands = DrvNextBandO; } DrvSendPageO; } DrvEndDocO: Вывод всего задания печати производится между вызовами DrvStartDoc и Drv- EndDoc, а вывод отдельной страницы — между вызовами DrvStartPage и DrvEndPage. Для каждой страницы сначала вызывается функция DrvStartBanding, которая сообщает драйверу о начале разбивки на полосы. Графический механизм вызывает функцию DrvQueryPerBandlnfo для получения геометрической информации о выводимой полосе, после чего перебирает команды GDI, хранящиеся в файле, и передает все команды, задействованные в текущей полосе. После завершения полосы GDI переходит к следующей полосе и т. д. до завершения всей страницы. Графические команды DDI либо воспроизводятся GRE непосредственно на поверхности, управляемой GDI, либо передаются функциям, предоставленным драйвером. Для векторных устройств преобразованные команды могут передаваться спулеру при каждой операции графического вывода. Для растровых устройств полученный растр проходит полутоновое преобразование в соответствии с цветовой глубиной принтера, делится на несколько цветовых плоскостей (например, в схеме CYMK), сжимается и кодируется на языке принтера. При передаче данных спулеру в качестве параметра указывается манипулятор, переданный драйверу при вызове DrvEnablePDEV. Функция EngWriteSpooler использует этот манипулятор для общения со спулером. Драйвер принтера также может поддерживать функции DrvEscape и DrvDraw- Escape для «официального» и «закулисного» взаимодействия приложения с драйвером. Например, драйвер PostScript позволяет вставлять данные в формате PostScript прямо в поток данных при помощи функции DrvDrawEscape. Информация о поддержке этой возможности может быть получена при помощи функции DrvEscape. В процессе обработки задания печати драйвер принтера может вызвать функцию EngCheckAbort, чтобы проверить, не отменил ли пользователь печать. Ресурсы, выделенные для поверхности, должны освобождаться функцией драйвера DrvDisableSurface. При вызове функции DeleteDC приложением или GDI также вызывается функция DrvDisablePDEV, что позволяет драйверу освободить ресурсы, выделенные для физического устройства. Если приложение вызывает ResetDC в процессе печати, GDI вызывает DrvEnablePDEV для создания нового экземпляра. Далее вызывается функция DrvResetPDEV, чтобы драйвер мог скопировать информацию из старого экземпляра в новый, затем вызывается DrvDisableSurface для старого устройства, после чего вся последовательность операций вывода начинается заново для нового устройства. Перед выгрузкой графической DLL принтера вызывается функция DrvDisableDriver. Драйвер принтера для вывода документа HTML Вы когда-нибудь размышляли над вопросом, как преобразовать страницу документа в растровое изображение? Конечно, существует простое решение — со-
Драйверы принтеров 135 хранить копию экрана. А если на экране помещается лишь часть сохраняемой информации? Сохранять несколько копий экрана и «сшивать» их вручную не хочется. А вдруг вам захочется преобразовать документ, состоящий из 100 страниц, в 100 растровых изображений? Существует простое решение — раздобыть драйвер принтера для печати в растровом формате... или написать его самостоятельно. Обычно на печать выводятся многостраничные документы, поэтому работать с драйвером принтера, который генерирует растр всего для одной страницы, неудобно. Если вы можете сгенерировать один растр, значит, вы можете легко сгенерировать документ HTML, связывающий воедино отдельные растры. Ниже описан пример драйвера принтера, генерирующий выходные данные на языке HTML. Конечно, не существует принтера, который бы принимал документы HTML в качестве непосредственного ввода, однако вы можете легко выполнить печать в файл и просмотреть полученный документ в браузере. HTML не является языком векторного вывода, поэтому мы не сможем однозначно преобразовать графические команды DDI в команды HTML. Вместо этого страница выводится в виде растрового изображения, которое затем связывается с документом HTML. Чтобы этот проект мог воплотиться на практике, необходимо поставить разумные цели. По этой причине мы ограничимся поддержкой одного размера бумаги (11,5 х 8 дюймов, формат Letter), одного разрешения (96 dpi) и одного цветового формата (24-битный DIB). В результате поверхность для вывода всей страницы будет занимать 2,46 мегабайта, что позволит обойтись без разбивки на полосы. Самостоятельная реализация команд GDI — дело хлопотное; вместо этого мы создадим поверхность, управляемую GDI, и поручим всю черновую работу графическому механизму. На самом деле писать специальный драйвер для вывода нескольких растров было бы неразумно, но этот пример дает очень хорошее представление об устройстве GDI. По этой причине наш драйвер перехватывает все стандартные вызовы DDI, чтобы вы могли проверить работу всех параметров. Весь фактический вывод перепоручается графическому механизму. Построение страницы HTML со ссылками на растровые изображения не сводится к простой передаче потока данных спулеру — растровые изображения приходится сохранять в отдельных файлах. К счастью, в win32k.sys предусмотрены простые операции с файлами, отображаемыми на память (функции EngMapFile и EngUnmapFile). Наша реализация драйвера принтера HTML состоит из двух частей: класса KDevice, в котором инкапсулируется физический блок данных устройства, созданный при вызове DrvEnablePDEV, а также операции с устройством, и интерфейсной части DDL Ниже приведен заголовочный файл класса KDevice. struct Pair; class KDevice { int nNesting: // документ, страница int nPages; // количество выведенных страниц
136 Глава 2. Архитектура графической системы Windows void Write(const char * pStr); void WriteW(const WCHAR * pwStr); void WriteHex(unsigned val); void WritelnCconst char * pStr = NULL): void Write(DWORD index, const PAIR * pTable); char * CopyBlock(char * pDest. void * pData. int size); void CopySurface(char * pDest. const SURFOBJ * pso); void LogCalKint index, const void * para, int parano); public: int width: int height: HPALETTE hPalette: HSURF hSurface: HDEV hDevice: HANDLE hSpooler; int nlmage; void Create(void) // ширина в дюймах * 10 // высота в дюймах * 10 // При использовании GDI палитра нужна // даже для 24-битных растров // Стандартная поверхность. // управляемая устройством // Манипулятор устройства GDI // Манипулятор спулера nNesting » 0 nPages = 0 nlmage = 0 void DumpSurface(const SURFOBJ * psoBM); BOOL Ca11Engine(int index, const void * para, int parano): BOOL StartDoc(LPCWSTR pszDocName. const void * firstpara. int parano): BOOL EndDoc(const void * firstpara. int parano): BOOL StartPage(const void * firstpara. int parano): BOOL SendPage(const void * firstpara. int parano): Класс KDevice содержит переменные для хранения размеров бумаги, а также манипуляторов палитры, поверхности, устройства и спулера. Другие переменные класса предназначены для внутреннего использования. Переменная nNesting обеспечивает правильную последовательность вызовов DrvStartDoc, DrvEndDoc, DrvStartPage и DrvSendPage; переменная nPages содержит количество печатаемых страниц; переменная nlmage — порядковый номер ссылки HTML на графическое изображение. В классе KDevice нет ни полноценного конструктора, ни деструктора, ни виртуальных функций — мы не хотим включать runtime-поддержку C++ в DLL режима ядра. Частичная инициализация выполняется методом Create. Методы Write сбрасывают данные HTML в поток данных, передаваемых спулеру. Метод DumpSurface записывает содержимое DIB-поверхности в отдельный растровый файл.
Драйверы принтеров 137 Обратите внимание на функцию KDevice: :LogCall, предназначенную для вывода имени точки входа драйвера и списка параметров. При вызове передается индекс точки входа, адрес первого параметра в стеке и количество параметров (при передаче параметров используются соглашения языка Pascal). Функция ограничивается простой выдачей шестнадцатеричного дампа параметров. void KDeviсе::LogCal1(int index, const void * firstpara. int parano) { Write("<li>"); WriteCindex, Pair_DDIFunction); // Имя функции берется из таблицы Writer С): const unsigned * pDWORD = (const unsigned *) firstpara; for (int i=0; i<parano; i++) { WriteHex(pDWORD[i]); if ( i!=(parano-l) ) WriteC, "); } Writeln(")</li>M); } Функция KDevice:: Call Engine проверяет указатель на KDevice; если указатель отличен от NULL, она вызывает функцию LogCal 1. Функция Call Engine вызывается всеми графическими функциями DDI драйвера перед возвращением вызова графическому механизму. Таким образом, реализация Call Engine может избирательно блокировать некоторые функции DDI, чтобы заставить графический механизм разбить их на упрощенные вызовы. Интерфейс DDI драйвера реализуется в файле HTMLDrv.cpp. Файл начинается с таблицы поддерживаемых точек входа: const DRVFN DDI_Funcs[] « { INDEX DrvEnablePDEV, INDEX DrvCompletePDEV, INDEX DrvResetPDEV. INDEXJDrvDisablePDEV, INDEX_DrvEnableSurface, INDEX_DrvDisableSurface. INDEX DrvStartDoc. INDEX_DrvEndDoc. INDEX_DrvStartPage. INDEX_DrvSendPage. INDEX DrvStrokePath, INDEX DrvFillPath. INDEX DrvStrokeAndFillPath INDEX_DrvLineTo. INDEX DrvPaint, INDEX DrvBitBlt. INDEX DrvCopyBits. (PFN) DrvEnablePDEV, (PFN) DrvCompletePDEV. (PFN) DrvResetPDEV. (PFN) DrvDisablePDEV. (PFN) DrvEnableSurface, (PFN) DrvDisableSurface. (PFN) DrvStartDoc. (PFN) DrvEndDoc, (PFN) DrvStartPage. (PFN) DrvSendPage. (PFN) DrvStrokePath. (PFN) DrvFillPath. .(PFN) DrvStrokeAndFillPath. (PFN) DrvLineTo. (PFN) DrvPaint. (PFN) DrvBitBlt, (PFN) DrvCopyBits.
138 Глава 2. Архитектура графической системы Windows INDEX_DrvStretchBlt, (PFN) DrvStretchBlt. INDEX_DrvTextOut. (PFN) DrvTextOut }: Функция DrvEnableDriver после несложной проверки передает таблицу функций графическому механизму: BOOL APIENTRY DrvEnableDriver(UL0NG iEngineVersion. ULONG cj. DRVENABLEDATA *pded) { // Проверить параметры if (iEngineVersion < DDI_DRIVER_VERSION) { EngSetLastError(ERROR_BAD_DRIVER_LEVEL); return FALSE; if (cj < sizeof(DRVENABLEDATA)) { EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; pded->iDriverVersion = DDI_DRIVER_VERSION; pded->c = sizeof(DDI_Hooks) / sizeof(DDI_Hooks[0]); pded->pdrvfn - (DRVFN *) DDIJooks; return TRUE; } Ниже приведена часть функции DrvEnablePDEV, создающей новый экземпляр класса KDevice. Эта функция заносит информацию о возможностях драйвера в структуры GDI INFO и DEVINFO в соответствии со значениями полей полученной структуры DEVMODW. В данном случае программа проверяет ориентацию бумаги. DHPDEV APIENTRY DrvEnablePDEV(DEVMODEW *pdm LPWSTR ULONG HSURF ULONG ULONG ULONG DEVINFO HDEV PWSTR HANDLE pwszLogAddress, cPat. *phsurfPatterns, cjCaps. *pdevcaps, cjDevInfo. *pdi. hdev, pwszDeviceName. hDriver) if ( (cjCaps<sizeof(GDIINFO)) || (cjDevInfo<sizeof(DEVINFO)) ) { EngSetLastError(ERRORJNVALIDJ>ARAMETER); return FALSE: KDevice * pDevice; // Создать объект физического устройства. Маркер - HTMD pDevice - (KDevice *) EngAllocMem (FL_ZER0_MEM0RY. sizeof(KDevice). 'DMTH'):
Драйверы принтеров 139 if (pDevice — NULL) { EngSetLastError(ERROR_OUTOFMEMORY); return NULL; pDevice->Create(); pDevice->hSpooler - hDriver; pDevice->hPalette - EngCreatePalette (PAL_BGR. 0. 0, 0. 0. 0); if (pdm — NULL || pdm->dmOrientation == DMORIENT_PORTRAIT) { pDevice->width - PaperWidth: pDevice->height = PaperHeight; } else { pDevice->width - PaperHeight; pDevice->height - PaperWidth; } // Инициализация GDI INFO пропущена // Инициализация DEVINFO пропущена pdi->hpalDefault = pDevice->hPalette; return (DHPDEV) pDevice; } Функция DrvEnableSurface создает полностраничную 24-битную DIB-поверх- ность, управляемую GDI, и сообщает графическому механизму, что драйвер желает перехватывать некоторые вызовы DDL Обратите внимание: размеры бумаги задаются в десятых долях дюйма, чтобы избежать выполнения операций с плавающей точкой в ядре. Поверхность инициализируется белым цветом не при создании, а при вызове DrvStartPage. HSURF APIENTRY DrvEnableSurfaceCDHPDEV dhpdev) { «Device * pDevice = («Device *) dhpdev; SIZEL sizl - { pDevice->width * Dpi / 10, pDevice->height * Dpi / 10 }; pDevice->hSurface = (HSURF) EngCreateBitmap(sizl, sizl.cy. BMF_24BPP, BMFJOZEROINIT. NULL); if (pDevice->hSurface — NULL) return NULL: EngAssociateSurface(pDevice->hSurface, pDevice->hDevice. HOOKJITBLT | HOOKJTRETCHBLT | HOOKJEXTOUT | H00K_PAINT | H00K_STR0KEPATH | HOOKJILLPATH | HOOK_STROKEANDFILLPATH | H00K_C0PYBITS | H00K_LINET0); return pDevice->hSurface; }
140 Глава 2. Архитектура графической системы Windows Хотя драйвер HTML перехватывает некоторые графические функции, вся реализация сводится к простому выводу информации о параметрах, после чего вызов возвращается графическому механизму. Ниже приведен лишь один типичный пример, который дает представление и об остальных реализациях. В первом параметре всех графических вызовов DDI передается указатель на SURF0BJ — структуру данных, используемую графическим механизмом для представления поверхности вывода. Поле dhpdev содержит манипулятор физического устройства, который предоставляется драйвером и возвращается функцией DrvEnablePDEV. В данном случае манипулятор преобразуется в указатель на KDevice. BOOL АРI ENTRY DrvBitBlt(SURFOBJ *psoTrg. SURFOBJ *psoSrc. SURFOBJ *psoMask. CLIPOBJ *pco, XLATEOBJ *pxlo. RECTL *prclTrg, POINTL *pptlSrc. POINTL *pptlMask. BRUSHOBJ *pbo. POINTL *pptlBrush. R0P4 rop4) { KDevice * pDevice = (KDevice *) psoTrg->dhpdev; if ( pDevice->CallEngine(INDEX_DrvBitBlt. SpsoTrg, 11) ) return EngBitBltCpsoTrg. psoSrc. psoMask. pco, pxlo. prclTrg. pptlSrc, pptlMask, pbo, pptlBrush, rop4): else return FALSE; } Так выглядят самые интересные компоненты драйвера HTML. Ниже приведена сокращенная версия результатов, полученных при печати стандартной тестовой страницы. В web-браузере она выглядит вполне прилично (рис. 2.9). <html> <head> <tit!e>Test Page </title> </head> <body bgcolor=#80B090><font size=l> <ol> <li>DrvStartDoc(e2229558, ele86488. 2)</li> <li>DrvStartPage(e2229558)</li> <1i>DrvFinPath(e2229558. fld2fa68 l)</li> <li>DrvBitBlt(e2229558. 0. 0. fld2f7b0 fOfO)</li> <li>DrvText0ut(e2229558. fld2f824 dOd)</li> <li>DrvText0ut(e2229558. fld2f824 dOd)</li> <li>DrvSendPage(e22295586)</1i> <p><img src="c:\htmd_000.bmp "></p> <li>DrvEndDoc(e2229558, 0)</li> </ol> </body> </html>
Итоги 141 f е^ММсс, е^МЬШ e'AbWicTWd) ' Tjl j 281. DrvTextOut(el390e58, ft>317828, e2727d08,fb31792c, 0,ft>317834, 1 e25154cc, e2515520, e251541c, dOd) j 282. DrvSendPage(el390e58) Sm i 4Wnclorvys2coo О : ■! .- J",.... Windows 2000 Printer Test Page J - j ►n Рис. 2.9. Тестовая страница в браузере Как видите, упрощенный драйвер принтера (а точнее, его графическая DLL) несложен. Его можно было бы еще упростить, отказавшись от вывода параметров. Настоящий драйвер принтера устроен гораздо сложнее. Если вы захотите убедиться в этом, обратитесь к примеру драйвера плоттера из Microsoft Windows 2000 DDK или драйверу PostScript из Windows NT 4.0 DDK. Впрочем, полноценный, качественный, оптимизированный драйвер принтера по своей сложности превосходит примеры, включенные в DDK. Итоги В этой главе мы рассмотрели общую архитектуру графической системы Windows, архитектуру клиентской стороны Win32 GDI, архитектуру DirectX и архитектуру системы печати, познакомились с графическим механизмом, драйверами экрана и принтера. Была создана программа для отслеживания графических системных вызовов как на стороне клиента, так и на стороне сервера. Глава завершается описанием простого драйвера принтера, генерирующего данные в формате HTML. Главное, что читатель должен вынести из этой главы, — это блок-схемы с изображением различных уровней архитектуры графической системы. Вы должны в общих чертах представлять, какие аспекты графической системы Windows NT/2000 обслуживаются тем или иным компонентом системы и как вызовы графических функций Win32 API реализуются различными компонентами в цепочке обработки. Хотя в этой главе рассматривались общие вопросы архитектуры, а материал сопровождался блок-схемами, столь нелюбимыми многими программистами, дальнейшее изложение будет более конкретным. В главе 3 мы изучим недоку-
142 Глава 2. Архитектура графической системы Windows ментированные структуры данных, на которых основана работа GDI и DirectDraw, а потом перейдем к более интересной главе 4 и познакомимся с закулисным устройством графической системы. Примеры программ На прилагаемом компакт-диске находятся полные исходные тексты программ, описанных в этой главе (табл. 2.4). Таблица 2.4. Программы главы 2 Каталог проекта Описание 1 Samples\Chapt_02\SysCall Вывод списка системных функций DLL подсистемы Win32 (ntdll.dll, gdi32.dll и user32.dll) и системных функций ядра ОС (ntoskrnl.exe, win32k.sys) Samples\ChaptJ)2\Timer Сравнительный анализ четырех способов хронометража: GetTickCount, timeGetTime, QueryPerformanceCounter и чтение счетчика тактов процессора Intel Pentium Samples\Chapt_02\HTMLDrv Драйвер принтера (построение страниц HTML, ведение протокола команд DDI и воспроизведение страниц на внедренном растре)
Глава 3 Внутренние структуры данных GDI/ DirectDraw Интерфейс Windows API часто называют «объектно-базированным» (object based) — не путайте с «объектно-ориентированным» (object oriented), это не одно и то же. При использовании Win32 API часто приходится создавать разнообразные объекты, выполнять с ними различные операции при помощи функций и в конечном счете уничтожать их. Операционная система полностью управляет внутренним представлением объекта, а в распоряжении программиста находится только манипулятор (handle). В GDI используются десятки всевозможных объектов — контексты устройств, логические перья, логические кисти, логические шрифты, логические палитры, аппаратно-независимые растры, DIB-секции и т. д. Но для любого объекта вы имеете дело только с манипулятором — таинственным числом, с которым и сделать-то ничего нельзя (кроме передачи при вызове функции GDI). В этой главе во всех подробностях описаны манипуляторы GDI и, что еще важнее, — стоящие за ними структуры данных. Вы узнаете, что означает каждый бит в манипуляторе GDI, как устанавливается соответствие между манипулятором и элементом таблицы объектов GDI, и даже познакомитесь со структурами данных, используемыми во внутреннем представлении всех объектов GDI. Кроме того, в этой главе рассматриваются структуры данных DirectDraw. При помощи здравого смысла, «хакерских» приемов, утилит от Microsoft и нескольких программ, написанных специально для этой главы, мы добьемся главной цели — понимания ключевых структур данных GDI/DirectDraw. Возможно, вы не слишком интересуетесь техническими подробностями структур данных GDI. Тем не менее знание общих принципов внутреннего устройства GDI/DirectDraw повысит вашу квалификацию в программировании для Windows. В этой главе также рассматриваются некоторые полезные приемы — например, просмотр содержимого виртуальной памяти, написание драйвера
144 Глава 3. Внутренние структуры данных GDI/DirectDraw устройства режима ядра (нет, не для принтера!) и установка расширения отладчика WinDbg для исследования ядра NT/2000 на том же компьютере. Манипуляторы и объектно-ориентированное программирование В объектно-ориентированных языках и средах объектом называется совокупность данных и функций, моделирующая некоторую сущность в реальном или воображаемом мире. Объекты делятся на классы в соответствии со своими общими чертами. Как правило, в объектно-ориентированных языках центральное место занимают именно определения классов; объект всего лишь является экземпляром класса, созданным во время работы программы. В Win32 API также определяются различные виды объектов. Самыми распространенными объектами GDI являются контексты устройств, логические перья, логические кисти, логические шрифты, логические палитры и аппаратно- зависимые растры. Таким образом, все объекты контекстов устройств являются экземплярами класса контекста устройства, а все логические палитры являются экземплярами класса логической палитры. Класс и объект Классы в объектно-ориентированных языках содержат как данные (переменные класса), так и программный код (функции класса). Доступ к членам класса (то есть его переменным и функциям) контролируется определением класса. Одни члены класса объявляются закрытыми (private) и защищенными (protected), а другие — открытыми (public). При создании экземпляра класса сначала выделяется память, а затем вызывается конструктор. Применение закрытых и защищенных членов класса позволяет изолировать внутреннюю реализацию класса от программного кода, использующего этот класс. Концепции инкапсуляции и маскировки реализации являются краеугольными камнями объектно-ориентированного программирования. В Win32 API реализация некоторых классов также хорошо инкапсулируется на системном уровне. Как будет показано позже, объекты всегда содержат переменные — обычно оформленные в структуру данных или даже в сложную иерархическую сеть структур. Для каждого класса определяется стандартный набор функций, применяемых к объектам этого класса. Например, контекст устройства является объектом GDI, экземпляром класса контекста устройства. В этом классе определяются такие функции, как GetSetColor и SetTextColor, предназначенные для получения/назначения цвета текста. Инстинкт программиста подсказывает, что цвет текста является переменной класса, ассоциированной с объектом контекста устройства, но мы понятия не имеем, где и в каком внутреннем представлении он хранится. Другими словами, внутренняя реализация контекста устройства полностью скрыта от прикладных программистов.
Манипуляторы и объектно-ориентированное программирование 145 Инкапсуляция и маскировка реализации В обычной практике объектно-ориентированного программирования некоторые члены класса объявляются закрытыми или защищенными и не могут использоваться кодом клиентской стороны. Однако компилятор все равно должен точно знать все члены класса, их типы и имена. По крайней мере, компилятору должен быть известен точный размер экземпляра класса для выделения памяти. Это может вызвать массу проблем при модульной разработке программ. Каждый раз, когда в классе изменяется переменная или функция, всю программу приходится компилировать заново. Программы, откомпилированные для старых версий определения класса, не будут работать с новыми версиями. Для решения этой проблемы создаются абстрактные базовые классы. Абстрактный базовый класс при помощи виртуальных функций определяет интерфейс с клиентскими программами, полностью абстрагируясь от его реализации, что способствует маскировке реализации и улучшению модульности программы. Крайним проявлением маскировки реализации являются СОМ-интерфейсы, которые представляют собой классы без переменных, состоящие из одних чисто виртуальных функций. Класс, содержащий чисто виртуальную функцию, не может использоваться для создания объектов. Программист должен создать на его основе производный класс, реализовать все чисто виртуальные функции и создать экземпляр производного класса. Для маскировки производного класса от клиента создается специальная статическая функция, предназначенная для создания объектов. Например, COM DLL всегда экспортируют функцию DllGetClassObject, которая (при содействии фабрики класса) отвечает за создание новых объектов, поддерживаемых COM DLL. Для маскировки реализации от клиентской стороны класса обычно определяется специальная функция, которая создает экземпляры производного класса и выделяет память для них, и другая функция, которая уничтожает экземпляры с освобождением выделенной памяти. Объекты Win32 API можно рассматривать как реализованные с использованием абстрактного базового класса, не содержащего ни одной переменной. Внутреннее представление данных объекта полностью скрыто от пользовательского приложения. Преимущества такого подхода огромны; программа, откомпилированная для Win32s (подмножество Win32 API, реализованное в Windows 3.1), без всяких проблем работает в Windows 95, а программа, откомпилированная для Windows 95, прекрасно работает в Windows NT и Windows 2000. Двоичный код программ Win32 совместим с разными версиями операционной системы, реализующими Win32 API на одном типе процессора. Хотя возможности использования единого Win32 API все же не безграничны, значительное подмножество Win32 API реализуется на разных платформах с одинаковой семантикой. Что касается GDI, реализация этого интерфейса для Windows 95/98 в значительной степени основана на его 16-разрядной реализации для Windows 3.1; в Windows NT 3.51 GDI функционирует в виде отдельного системного процесса, работающего в пользовательском режиме, а в Windows NT 4.0 и Windows 2000 используется 32-разрядный графический механизм режима ядра. Между этими реализациями существуют заметные различия, однако их безукоризненная маскировка в Win32 API обеспечивает переносимость программ. GDI обычно
146 Глава 3. Внутренние структуры данных GDI/DirectDraw поддерживает несколько функций для создания экземпляра объекта и несколько функций для его уничтожения. Чтобы продемонстрировать аналогию между объектно-ориентированным программированием и Win32 API, попробуем написать на C++ минимальную псев- до-реализацию GDI. Результат приведен в листинге 3.1. Листинг 3.1. Псевдо-реализация GDI на C++ // gdi.h class _GdiObj { public: virtual int GetObjectType(void) = 0; virtual int GetObject(int cbBuffer, void * pBuffer) =0; virtual bool DeleteObject(void) = 0; virtual bool UnrealizeObject(void) * 0; }: class _Pen : public _GdiObj { public: virtual int GetObjectType(void) { return 0BJ_PEN: } virtual int GetObjectCint cbBuffer. void * pBuffer) = 0: virtual bool DeleteObject(void) = 0: virtual bool UnrealizeObject(void) { return true: } }: _Pen * _CreatePen(int fnPenStyle. int nWidth. COLORREF crColor): // gdi.cpp #define STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> #include "gdi.h" class _RealPen : public _Pen { LOGPEN m_LogPen: public: _RealPen(int fnPenStyle. int nWidth. COLORREF crColor) T m_LogPen.lopnStyle = fnPenStyle: m_LogPen.lopnWidth.x = nWidth: m_LogPen.lopnWidth.y - 0:
Манипуляторы и объектно-ориентированное программирование 147 m_LogPen.lopnColor - crColor; } int GetObjectCint cbBuffer, void * pBuffer) { if ( pBuffer==NULL ) return sizeof(LOGPEN); else if ( cbBuffer>=sizeof(m_LogPen) ) { memcpy(pBuffer, & m_LogPen. sizeof(m_l_ogPen)); return sizeof(LOGPEN); } else { SetLastError(ERRORJNVALID_PARAMETER); return 0; } } bool DeleteObject(void) { if ( this ) { delete this; return true; } else return false; } }: _Pen * _CreatePen(int fnPenStyle. int nWidth, COLORREF crColor) { return new _RealPen(fnPenStyle. nWidth. crColor); } void Test(void) { _Pen * pPen = _CreatePen(PS_SOLID, 1. RGB(0. 0. OxFF)); //// pPen->DeleteObject(); } В листинге 3.1 определяется абстрактный базовый класс GdiObj, представляющий обобщенный объект GDI. Он состоит из четырех чисто виртуальных функций и не содержит ни одной переменной. Обобщенный класс пера Реп определяется как производный от GdiObj; он реализует две виртуальные функции и оставляет две другие чисто виртуальными. Функция CreatePen создает экземпляры класса Реп. В файле реализации (GDI.cpp) определяется настоящий класс пера (Real Pen), который хранит информацию о пере в структуре L0GPEN. Класс Real Pen представляет собой полную реализацию абстрактного класса Реп с конструктором и двух оставшихся виртуальных функций. Функция CreatePen
148 Глава 3. Внутренние структуры данных GDI/DirectDraw создает экземпляр класса Real Pen и передает указатель на него вместо указателя на обобщенный класс пера Реп. Клиентская сторона не знает, сколько памяти занимает объект пера, откуда выделяется эта память и как реализуются виртуальные функции. Все эти подробности остаются скрытыми. Клиентская сторона должна знать лишь имена методов интерфейса, их назначение и семантику. Указатели и манипуляторы При создании объекта в объектно-ориентированном языке необходимо выделить блок памяти для хранения переменных объекта. Если класс содержит виртуальные функции, вместе с переменными в памяти создается дополнительный указатель на таблицу всех реализаций виртуальных функций данного класса. В таких языках, как C++, центральное место занимают указатели на объекты. Указатели передаются всем не статическим функциям класса, что позволяет обращаться к переменным объекта и вызывать нужные виртуальные функции. В C++ указатель на текущий объект обозначается ключевым словом this. Рисунок 3.1 на примере класса _RealPen (см. листинг 3.1) показывает, на что именно ссылается указатель на объект. В классе Real Pen 16 байт нужны для хранения единственной переменной класса, mLogPen, и еще 4 байта — для указателя на таблицу виртуальных функций. Следовательно, для каждого объекта необходимо выделить минимум 20 байт. Указатель на таблицу виртуальных функций ссылается на блок из четырех указателей на функции. В приведенном примере две функции реализуются классом _Реп, а две другие — классом JtealPen. Экземпляр Knacca_RealPen Таблица виртуальных функций для класса _RealPen Class Указатель" на объект Указатель на таблицу виртуальных функций LOGPEN lopnStyle lopnWidth lopnColor w & _Pen::GetObjectType & _RealPen::GetObject & _RealPen::DeleteObject & _Pen::UnrealizeObject Рис. З.1. Пример представления объекта в C++ В СОМ указатель на объект обычно называется интерфейсным указателем и ссылается на указатель на таблицу функций. В приведенном примере функция CreatePen создает экземпляр Real Pen, но возвращает указатель на класс Реп. Как и в СОМ, клиентский код ничего не знает о внутреннем представлении данных. Хотя в Win32 API для каждого объекта где-то в памяти выделяется блок данных, разработчики Microsoft не стали возвращать указатель на него пользовательскому приложению. Возможно, это было сделано из тех соображений, что указатель несет слишком много информации для «умных» программистов — он выдает точное местонахождение объекта в памяти. Указатели позволяют выпол-
Манипуляторы и объектно-ориентированное программирование 149 нять операции чтения/записи с внутренним представлением объектов, которое операционная система предпочла бы скрыть от пользователя. Кроме того, указатели затрудняют совместное использование объектов из адресных пространств разных процессов. Чтобы скрыть эту информацию от программистов, функции создания объектов Win32 вместо указателя обычно возвращают манипулятор (handle) объекта. Манипулятор определяется как число, которое однозначно идентифицирует объект и может использоваться для косвенных ссылок на него. Взаимосвязь объектов с манипуляторами не документирована, ее неизменность в будущих версиях Windows не гарантируется, и вообще все подробности известны разве что Microsoft да еще нескольким производителям системных утилит. Можно считать, что отображение на манипуляторы указателей на объекты и наоборот производится двумя функциями Encode и Decode, прототипы которых приведены ниже. HANDLE EncodeCvoid * pObject); // Преобразовать указатель в манипулятор void * Decode(HANDLE hObject): // Преобразовать манипулятор в указатель Тождественное отображение Иногда значение манипулятора может совпадать со значением указателя на объект; в этом случае функции Encode и Decode ограничиваются преобразованием типа, а связь между указателями на объекты и манипуляторами является тождественной. В Win32 API манипулятор экземпляра (HINSTANCE) или манипулятор модуля (HM0DULE) представляет собой обычный указатель на образ РЕ-файла, отображаемого на память. Считается, что функция LockResource фиксирует ресурс в памяти и отображает глобальный манипулятор на указатель, но в действительности их значения совпадают. Манипулятор ресурса, возвращаемый функцией Load- Resource, в действительности представляет собой «замаскированный» указатель на ресурс, отображенный на память. Табличное отображение Наиболее распространенным механизмом установления связи между объектом и его манипулятором является табличное отображение. Операционная система строит таблицу всех используемых объектов. При создании нового объекта в таблице находится пустая строка, которая заполняется данными объекта. При удалении объекта его переменные удаляются из памяти, а соответствующий элемент таблицы освобождается для последующего использования. В табличной схеме управления объектами индексы таблицы являются хорошими кандидатами на роль манипуляторов, а преобразование указателей в манипуляторы и наоборот выполняется тривиально. В Win32 API информация о объектах ядра хранится в таблицах уровня процесса. К категории объектов ядра относятся мьютексы, семафоры, события, ключи реестра, порты, файлы, символические ссылки, каталоги объектов, файлы, отображаемые на память, программные потоки, рабочие столы, таймеры и т. д. Для управления многочисленными объектами ядра каждый объект создает свою
150 Глава 3. Внутренние структуры данных GDI/DirectDraw собственную таблицу объектов ядра. Одним из компонентов исполнительной части ядра NT/2000 является диспетчер объектов (object manager), предназначенный для управления объектами ядра. Одна из функций диспетчера объектов называется ObReferenceObjectByHandle. Согласно документации DDK, эта функция проверяет права доступа для заданного манипулятора объекта и, если доступ разрешен, возвращает указатель на тело объекта. В сущности, эта функция преобразует манипулятор объекта в указатель на объект с некоторой дополнительной проверкой безопасности. Также существует очень хорошая утилита HandleEx (доступна на сайте www.sysinternals.com), предназначенная для составления списка объектов ядра на компьютерах с Windows NT/2000. Когда манипулятора недостаточно Хотя манипуляторы обеспечивают почти идеальную абстракцию, защиту и маскировку информации, они также причиняют немало хлопот программистам. Поскольку Win32 API ориентируется на применение манипуляторов, Microsoft не документирует внутреннее представление объектов и не описывает операции с ними. Никаких эталонных реализаций — в распоряжении программиста только прототипы функций, документация Microsoft и книги, материал которых в большей или меньшей степени основан на документации Microsoft. Первая категория проблем, с которыми сталкиваются программисты, связана с системными ресурсами. Никто не знает, какие ресурсы затрачиваются при создании объекта и получении его манипулятора, поскольку внутреннее представление объекта неизвестно. Как действовать программисту — хранить и заново использовать объект или же удалить его при первой возможности? В GDI поддерживаются три типа растров — какой тип следует выбрать, чтобы сократить затраты системных ресурсов? Главным ресурсом компьютера является процессорное время. Маскировка внутреннего представления от программиста затрудняет оценку сложности выполнения некоторых операций при проектировании сложных алгоритмов. Допустим, вы строите сложный регион средствами GDI; какую сложность имеет ваш алгоритм — линейную, квадратичную, кубическую? Полная маскировка реализации также усложняет отладку. Если после 5 минут работы ваша программа начинает «гнать мусор», вероятно, где-то происходит утечка ресурсов, но где именно и как ее исправить? Если вы — системный администратор и в вашей системе работают сотни приложений, как при хронической нехватке системных ресурсов вычислить источник бед? Похоже, единственным инструментом для борьбы с утечками ресурсов является программа BoundsChecker, которая использует специальные средства наблюдения для поиска несоответствий при создании и удаления объектов. И все же самые серьезные проблемы возникают с совместимостью программ. Почему в Windows 95 программа может передавать объекты GDI от одного процесса к другому, а в Windows NT/2000 — не может? Почему Windows 95 не справляется с обработкой больших аппаратно-независимых растров? Похоже, «идеальная» абстракция API в разных системах обладает разной семантикой. В основной части этой главы мы поближе познакомимся с манипуляторами GDI и исследуем недокументированный мир, скрытый за манипуляторами Windows NT/2000.
Расшифровка манипуляторов объектов GDI 151 Расшифровка манипуляторов объектов GDI При создании объекта GDI вы получаете манипулятор этого объекта. В зависимости от типа создаваемого объекта манипулятор может относиться к типу HPEN, HBRUSH, HFONT, HDC и т. д. Однако самым общим типом манипулятора объекта GDI является тип HGDI0BJ. Тип HGDI0BJ определяется как указатель на void. Определение типа HPEN, используемое при компиляции, изменяется в зависимости от состояния макроса компиляции STRICT. Если макрос определен, то HPEN определяется следующим образом: struct HPEN { int unused; }; typedef struct HPEN * HPEN; Если макрос STRICT не определен, то определение HPEN выглядит так: typedef void * HANDLE; typedef HANDLE HPEN; Проще говоря, если макрос STRICT определен, HPEN определяется как указатель на структуру с одним неиспользуемым полем, а если нет — как указатель на void. Компилятор C/C++ позволяет передать указатель на любой тип вместо указателя на void (но не наоборот!). Два указателя на разные типы, отличные от void, не являются взаимозаменяемыми. При определении STRICT компилятор выдает предупреждения при некорректной подмене типов манипуляторов объектов GDI или других объектов (скажем, HWND, HMENU и т. д.), а без определения STRICT вы можете спокойно смешивать разные типы манипуляторов, не рискуя получить предупреждение на стадии компиляции. Например, при определении STRICT можно передать HPEN функции, получающей HGDI0BJ (скажем, функции DeleteObject), но нельзя без предварительного преобразования передать HGDI0BJ функции, получающей HBRUSH (такой, как функция FillRgn). Столь четкое разделение различных манипуляторов GDI имитирует иерархию классов объектов GDI, хотя на самом деле иерархии классов не существует. Для каждого объекта GDI может быть создан только один манипулятор, поэтому вы не сможете создать другой манипулятор для объекта простым дублированием. Обычно манипуляторы объектов GDI действительны только в рамках конкретного процесса — другими словами, манипулятор может использоваться только тем процессом, который его создал. Манипуляторы, переданные из других процессов, недействительны. Как правило, объекты GDI могут создаваться несколькими разными способами, а уничтожаются одной функцией DeleteObject с параметром HGDI0BJ. Помимо непосредственного создания объекта, можно воспользоваться функцией GetStockObject для получения манипулятора заранее созданного объекта GDI или же при помощи более сложных функций преобразовать ресурс, связанный с модулем, в объект GDI. Функции загрузки ресурсов GDI (такие, как LoadBitmap или Loadlmage) создают необходимые объекты GDI за вас. Впрочем, все сказанное выше можно найти и в электронной документации. Мы же хотим узнать о манипуляторах объектов GDI гораздо больше. Подробности работы манипуляторов GDI Windows NT/2000 никогда не документировались, к тому же не существует никаких готовых программ, способных упростить наши исследования. Поэтому мы напишем свою, довольно сложную программу
152 Глава 3. Внутренние структуры данных GDI/DirectDraw GDIHandles, главное окно которой состоит из трех страниц-вкладок. Строение, реализация и использование этой программы будут рассматриваться постепенно по мере изложения материала. А пока взгляните на первую страницу Decode GDI Handle (Расшифровка манипулятора GDI), изображенную на рис. 3.2. Decode GDI Handle | Legate ODI Handle Tabb | Desode BOI Hm<$& Table | C*6fct,0E; jGet Stock Ob j ect (SYSTEM_FIXED_FONT) Jjxl Copi&s^ 1шШёммш£В!£мм2м1 01900011 01900013 0190001S 0190001S 01b00017 oiboooie 01b00018 018a0028 018a0027 018a00Z9 018&0021 018a002S GetStockObject GetStockObject GetStockObj ect GetStockObject GetStockObject GetStockObj ect GetStockObj ect GetStockObject GetStockObject GetStockObject GetStockObject GetStockObject (BLACKJBP.USH) 0 <DKGRAY_BRUSH) 0 (H0LL0IiT__BRUSH) 0 <NULL_BRUSH) 0 (BLACK_PEN) 0 (HULL_PEN) 0 <WHITE_PEN) 0 <ANSI_FIXED_FONT) 0 <ANSI_VAR_FONT) 0 <DEFAULT_GUI_FONT) ( (SYSTEH_FONT) 0 (SYSTEH FIXED FONT) Щ, 0ВЗМРСШТ OK Рис. З.2. Расшифровка манипуляторов GDI В верхней части страницы расположены два комбинированных списка для выбора способа создания объекта (от разнообразных вызовов GetStockObject до CreateEnhMetafile) и количества экземпляров (от 1 до 65 536). После выбора способа создания и количества экземпляров щелкните на кнопке Create — программа создаст заданное количество объектов. Манипуляторы, полученные в результате создания объектов, выводятся в большом списке в шестнадцатеричной записи, вместе с именем функции-создателя и порядковым номером в группе (нумерация начинается с 0). Цикл создания завершается при неудачном вызове функции-создателя или в случае возвращения того же манипулятора, что и при предыдущем вызове. Давайте проведем несколько экспериментов, понаблюдаем за процессом создания объектов GDI и проанализируем закономерности в значениях манипуляторов.
Расшифровка манипуляторов объектов GDI 153 Манипуляторы стандартных объектов — константы Манипуляторы, возвращаемые функцией GetStockObject, всегда являются константами независимо от порядка их вызова. Например, функция GetStockObject (BLACK_ BRUSH) возвращает стандартный (встроенный) объект черной кисти с манипулятором 0x01900011; GetStockObject (BLACKPEN) возвращает стандартный объект черного пера с манипулятором 0х01Ь00017 и т. д. Даже если запустить два экземпляра этой программы, GetStockObject возвращает одинаковые значения в обоих процессах. Можно предположить, что встроенные объекты создаются при инициализации системы и используются заново всеми процессами. HGDIOBJ не является указателем Хотя в заголовочных файлах Windows манипуляторы GDI определяются как указатели, при ближайшем рассмотрении они совершенно не похожи на указатели. Создайте несколько объектов GDI и посмотрите на полученные манипуляторы; вы увидите, что их значения лежат в интервале от 0x01900011 до 0xba040389. Если бы значение типа HGDIOBJ в действительности было указателем, как утверждает заголовочный файл wingdi.h, то нижняя граница соответствовала бы недействительному указателю на свободную область пользовательского адресного пространства, а верхняя адресовала бы адресное пространство ядра. Можно предположить, что манипуляторы GDI на самом деле не являются указателями. Обращает на себя внимание еще один факт: значения манипуляторов, полученных при вызовах GetStockObject(BLACKPEN) и GetStockObject (NULLPEN), отличаются всего на 1, что явно меньше объема памяти, необходимого для хранения внутренних объектов GDI, если бы манипуляторы действительно были указателями. Поэтому можно уверенно сказать, что HGDIOBJ не является указателем. Максимальное количество манипуляторов GDI на уровне процесса — 12 000 Если вызвать функцию CreatePen 16 раз, будет создано 16 новых логических перьев. Но если попытаться создать 65 536 логических перьев, далеко не все вызовы функции будут успешными. В процессе тестирования успешно создается около 12 000 перьев, а остальные вызовы завершаются неудачей. Обратите внимание: когда это происходит, значения в полях комбинированных списков Creator и Copies отображаются неправильно. Более того, вы даже не сможете сохранить копию экрана клавишей PrintScreen, если главное окно программы GDIHandles будет на переднем плане. Но если активизировать другую программу, клавиша PrintScreen работает нормально. ПРИМЕЧАНИЕ По результатам наших тестов выяснилось, что Windows NT устанавливает процессные квоты на количество манипуляторов GDI, чтобы один процесс не мог нарушить работу всей системы GDI. Однако в первой версии Windows 2000 это ограничение не соблюдается, что можно считать дефектом.
154 Глава 3. Внутренние структуры данных GDI/DirectDraw И еще одно интересное обстоятельство: когда CreatePen перестает создавать новые объекты GDI в процессе, другие процессы в системе работают нормально. Похоже, для каждого процесса устанавливается предельное количество активных манипуляторов GDI, равное примерно 12 000. Максимальное количество манипуляторов GDI на уровне системы — 16 384 Теперь запустите два экземпляра программы GDIHandles в одной системе и попробуйте вызвать CreatePen по 8192 раза в каждом процессе. Первый процесс создаст все запрашиваемые объекты, а второй остановится где-то на 7200. Когда второй процесс перестает создавать объекты, система приходит в замешательство. Даже если переключиться на другой процесс, весь вывод на экран нарушается. Хотя из документации Microsoft возникает впечатление, что объекты GDI пользуются только локальными ресурсами процесса, эксперимент наглядно показывает, что объекты GDI выделяются из общесистемного пула ресурсов. Таким образом, интенсивное использование ресурсов GDI одним процессом влияет на работу других процессов. 8192 + 7200 - 15 392. Учитывая манипуляторы объектов GDI, используемые окном GDIHandles и другими процессами, можно обоснованно предположить, что максимальное количество манипуляторов GDI в системе равно 16 384. Часть HGDIOBJ содержит индекс Создавая многочисленные объекты GDI при помощи программы GDIHandles, обратите особое внимание на младшие слова отображаемых двойных слов; вы увидите, что их значения лежат в интервале от 0x0000 до 0x3FFF. Младшие слова манипуляторов всегда уникальны в границах процесса; более того, их уникальность сохраняется и между процессами, если не считать стандартных объектов. Значения младших слов манипуляторов иногда увеличиваются, иногда уменьшаются, причем закономерность порой сохраняется даже между процессами. Например, при вызове CreatePen в одном процессе младшее слово манипулятора может быть равно 0х03С1, а при следующем вызове CreatePen в другом процессе младшее слово манипулятора оказывается равным 0х03С2. У этих фактов имеется простое объяснение: младшее слово HGDIOBJ представляет собой индекс в таблице системного уровня, содержащей информацию о 16 384 (0x4000) объектах GDI. Часть HGDIOBJ содержит тип объекта GDI В Windows NT/2000 манипулятор объекта GDI всегда возвращается в виде 32- разрядного числа. В программе GDIHandles это число отображается в виде 8 шест- надцатеричных цифр. Как было показано выше, младшие 4 шестнадцатеричные цифры манипулятора GDI содержат индекс объекта, поэтому мы можем заняться старшими 4 шестнадцатеричными цифрами.
Расшифровка манипуляторов объектов GDI 155 Если создавать объекты по типам (например, создать несколько кистей, затем несколько перьев, несколько шрифтов, контекстов устройств и т. д.), нетрудно убедиться в том, что манипуляторы GDI однотипных объектов имеют нечто общее — а именно, третья и четвертая шестнадцатеричные цифры их манипуляторов практически всегда совпадают. У кистей третья и четвертая цифры манипулятора всегда равны 0x90 и 0x10; у перьев — 0x30 и ОхЬО; у шрифтов — 0x8а и 0x0а; у палитр — 0x88 и 0x08; у растров — 0x05, а у контекстов устройств — 0x01. Манипуляторы, у которых старший бит этой группы цифр равен 1, принадлежат стандартным объектам. Таким образом, у нас имеется достаточно оснований, чтобы утверждать: третья и четвертая шестнадцатеричные цифры манипулятора содержат признак типа объекта и признак стандартного объекта GDI. Смысл двух оставшихся шестнадцатеричных цифр 32-разрядного манипулятора GDI пока остается неясным. Давайте подведем итог того, что мы знаем о манипуляторах Windows NT/2000. Манипулятор объекта GDI начинается с 8 старших бит, смысл которых пока неизвестен; далее следуют: 1 бит признака стандартного объекта, 7 бит с информацией о типе объекта и 16-битного индекса, старшие 4 бита которого всегда равны 0. Нам известны значения 7-битного типа объекта для контекста устройства, региона, растра, палитры, шрифта, кисти, расширенного метафайла, пера и расширенного пера. Структура манипулятора GDI изображена на рис. 3.3. 1 бит — признак стандартного объекта 7 бит — тип объекта Рис. 3.3. Структура манипулятора GDI в Windows NT/2000 Ниже приведены некоторые определения типов и функций C++, упрощающих кодирование и расшифровку манипуляторов GDI. typedef enum { gdi_objtypeb_dc - 0x01. gdi_objtypeb_region - 0x04. gdi_objtypeb_bitmap = 0x05. gdi_objtypeb_palette = 0x08. gdi_objtypeb_font = 0x0a. gdi_objtypeb_brush - 0x10. gdi_objtypeb_enhmetafile = 0x21.
156 Глава 3. Внутренние структуры данных GDI/DirectDraw gdi_objtypeb_pen = 0x30. gdi_objtypeb_extpen = 0x50 }: inline HGDIOBJ makeHGDIOBJ(unsigned top. bool stock, unsigned objtype. unsigned index) { return ((top & OxFF) « 24) | ((stock & 1) « 23) | ((objtype & 0x7F) « 23) | (index & 0x3FFF); } inline bool IsStockObj(HGDIOBJ hGDIObj) { return ((unsigned) hGDIObj) 0x00800000; } inline unsigned GetObjType(HGDIOBJ hGDIObj) { return ((unsigned) hGDIObj) » 16) & 0x7F; } inline unsigned GetObjIndex(HGDIOBJ hGDIObj) { return ((unsigned) hGDIObj & 0x3FFF); } При помощи этих функций можно узнать, принадлежит ли манипулятор стандартному объекту GDI, а также получить тип и индекс объекта в таблице. Поиск таблицы объектов GDI В ходе экспериментов раздела «Расшифровка манипуляторов объектов GDI» мы выяснили, что младшее слово манипулятора объекта GDI (HGDIOBJ) содержит индекс в интервале от 0 до 0x3FFF. Возникает предположение, что где-то существует таблица объектов GDI, находящаяся под управлением системы (скорее всего — GDI), и индексы относятся к элементам этой таблицы. Такие таблицы существовали в Win3.1 и Win95, поэтому вполне логично было бы встретить их и в Windows NT и Windows 2000. В этом разделе мы займемся поисками этой недокументированной таблицы. В этом месте программисты Windows обычно спрашивают, нельзя ли получить указатель на эту таблицу при помощи какой-нибудь функции Win32 API, а еще лучше — функции MFC, автоматически генерируемой мастером MSVC. На оба вопроса ответ будет отрицательным. Ни в одном официальном документе не подтверждается даже само существование этой таблицы, не говоря уже о документированных функциях API для работы с ней. Давайте ненадолго выйдем из образа программиста, знающего только API и библиотечные функции, и представим себя на месте Шерлока Холмса.
Поиск таблицы объектов GDI 157 Прежде всего предположим, что в системе действительно существует таблица объектов GDI и мы собираемся найти доказательства — то есть обнаружить эту таблицу в памяти. Если таблица существует, скорее всего, она может читаться из пользовательского адресного пространства. Дело в том, что gdi32.dll находится в пользовательском адресном пространстве, рядом с вашими DLL- и ЕХЕ-файлами. Если бы эта таблица могла читаться только из адресного пространства ядра, то для решения простейших задач вроде вызова GetObjectTypeO GDI32 приходилось бы обращаться за помощью к графическому механизму режима ядра win32k.sys, что сильно замедлило бы работу GDI. Поэтому наше второе предположение заключается в том, что таблица объектов GDI по крайней мере читается из программ пользовательского режима — то есть находится в пределах первых 2 Гбайт адресного пространства Win32. Если таблица объектов GDI существует, то при создании нового объекта GDI в нее заносятся новые данные, что приводит к изменению ее содержимого. Обратите внимание: в данном случае речь идет именно о создании нового объекта GDI, поскольку, как было показано выше, функция GetStockObjectO всегда возвращает один и тот же результат. Вполне возможно, что она возвращает заранее созданный манипулятор, не создавая нового объекта, и содержимое таблицы при этом не изменяется. Если создание нового объекта приводит к модификации таблицы объектов, то для поиска таблицы можно сравнить содержимое памяти до и после создания нового объекта GDI. В соответствии с нашими предположениями, при сравнении можно ограничиться пользовательским адресным пространством, не беспокоясь об адресном пространстве режима ядра. Одна из изменившихся областей памяти должна находиться внутри таблицы объектов GDI. От общих идей переходим к построению алгоритма. В сущности, мы должны сохранить содержимое пользовательского адресного пространства до и после создания простого объекта GDI, а затем сравнить их байт за байтом; любая различающаяся ячейка памяти может принадлежать таблице объектов GDI. Впрочем, подобные простые идеи никогда не работают на практике. При чтении первого байта пользовательского адресного пространства, имеющего нулевое смещение, возникает ошибка защиты; это делается для того, чтобы перехватывать попытки разыменования (dereferencing) NULL-указателей. В 2-гигабайтном пользовательском пространстве существуют pi другие области, недоступные для чтения. Вдобавок запись, чтение и сравнение всех доступных для этого областей памяти потребует огромных расходов дискового пространства и будет происходить очень медленно. Чтобы сканирование пользовательского адресного пространства ограничивалось областями, доступными для чтения, мы воспользуемся функцией Win32 API Virtual Query, которая делит виртуальное адресное пространство на блоки с одинаковыми флагами защиты (например, доступ только для чтения, возможность записи и исполнения). Построение контрольных сумм для областей памяти, доступных для чтения, значительно уменьшает объем памяти, участвующей в сохранении и сравнении. Ниже приведен рабочий алгоритм с функцией, которая сохраняет содержимое блоков памяти и сравнивает их.
158 Глава 3. Внутренние структуры данных GDI/DirectDraw void shot(vector<CRegion> & Regions) { MEMORY_BASIC_INFORMATION info; for (LPBYTE start=NULL; Virtual Query(start. & info, sizeof(info)): ) { if (info.State — MEM_COMMITED) { CRegion * pRgn = Regions.Lookup(start. info.RegionSize); if (pRgn==NULL) pRgn = Regions.Add(start. info.RegionSize); pRgn->CRC[0] = pRgn->CRC[l]; pRgn->CRC[l] - GenerateCRC(start. info.RegionSize); pRgn->usage ++; if ( (pReg->usage >- 2) && (pReg->CRC[0]!=pReg->CRC[l]) ) printfC'Possible Table location Ш1х". start); } start += info.RegionSize: } } void SearchGDIObjectTable(void) { vector<CRegion> UserRAM; shot(UserRAM); CreateSolidBrush(RGB(Oxll. 0x22. 0x33)); shot(UserRAM); } В наши дни такой неудобный интерфейс недопустим, поэтому в окне программы GDIHandles создается новая страница Locate GDI Handle Table (Поиск таблицы манипуляторов GDI). На ней отображается табличный список, в котором для каждого блока выводится контрольная сумма, начальный адрес, размер, состояние, тип и даже имя модуля и сегмента (если их удается определить). Для таких модулей, как gdi32.dll, имя модуля определяется функцией GetModuleFileName. Для секций РЕ-модуля (например, для секции .text, обычно содержащей исполняемый код) программа определяет имя секции анализом внутренней структуры РЕ-файла. Кроме того, программа пытается идентифицировать блоки с кучами (heaps) процессов и стеками программных потоков. На странице имеется кнопка Query Virtual Memory, позволяющая в любой момент получить «снимок» виртуальной памяти. Запустите программу и щелкните на кнопке Query Virtual Memory; функция VirtualQuery делит 2-гигабайтное пространство виртуальных адресов на 100 с лишним блоков. Большинство блоков помечено флагами F (Free, свободная память) и R (Reserved, зарезервированная память). Для нас интерес представляют блоки с флагом С (Commited, актуализированная память).
Поиск таблицы объектов GDI 159 Большинство актуализированных блоков содержит сегменты ЕХЕ-файлов программ и системных DLL — таких, как kernel32.dll, gdi32.dll и даже msldle.dll (трудно сказать, почему msidle.dll отображается в это адресное пространство, но факт остается фактом). Несколько блоков содержат кучи; один блок содержит стек. Для каждого актуализированного блока слева выводится контрольная сумма. Перейдите на страницу Decode GDI Handle, создайте однородную кисть, вернитесь на страницу Locate GDI Handle Table и создайте второй снимок памяти. На этот раз почти для всех актуализированных блоков у нас имеются две контрольные суммы (до и после создания объекта). Некоторые блоки могут иметь только одну контрольную сумму, поскольку у них изменился начальный адрес или размер. На рис. 3.4 показано, как выглядит экран после создания второго снимка виртуальной памяти. ншжя^шнн^^^шнннш №Ь /'4 >'' *•/ •' А/ ', " &/"*4-ъ'.»>$* 4'%*- >"'"* W<*: JiSl *A^i..M.i~*MbA~.i.b~.***..L. J..Jj.J...1^Jfl[r^-^J..-..^.^..J...J...,.^...^.^.^..J.J..^..^^ ^—f.....J.J.J.^J.....JJ—..j.—^.^-or[trri. "4шт/\ЬШ М Ыт '<';ttA4iw<>Y'?'\ 'тюш,У'"<; I мыт*' <"\" 1 '-'<?Ж р£ ecOe af cl zz S9tZ %Z 39b8 ST 9Ь12 ££dd77 %Z d044 =S 9dla ?£ al6a & 7714 69f2 39b8 9Ы2 dd77 d044 9dla f49a 3132 :491b 491b ООЗЬОООО 003b2000 003b8000 00400000 00401000 0043b000 0043f000 00444000 0044S000 0044a000 004S0000 00493000 004a0000 00500000 007a0000 nm»i nnn 00002000 00006000 00048000 00001000 ОООЗаООО 00004000 00005000 00001000 00005000 00006000 00043000 OOOOdOOO 00060000 002a0000 00001000 nnnnfnnn С М er R И er F С I С I С I С I С I С I F С И го F С М er R M er С Р rw ewe ewe ewe ewe ewe ewe Handles.exe .text .rdata .data . rsrc T7 HIWfriyiftHII чНююЩт mim !/^';Ччл'и *<<::& ? * ,< »>*>/, ОС*" Cano$ Рис. 3.4. Поиск таблицы объектов GDI (отмечены изменившиеся блоки) Перед теми блоками, у которых контрольные суммы совпадают, появляется зеленый знак равенства, а перед блоками с разными контрольными суммами — предупреждающий красный знак. Блоки с одной контрольной суммой после двух снимков тоже считаются изменившимися.
160 Глава 3. Внутренние структуры данных GDI/DirectDraw ПРИМЕЧАНИЕ Каждый квалифицированный Windows-программист должен хорошо разбираться в работе механизма виртуальной памяти. Это поможет лучше понять, как устроен интерфейс Win32 API и как работает ваша программа. Например, виртуальная память начинается с 64-килобайтного свободного блока с нулевым адресом. Если вы когда-нибудь пытались разыменовать указатель, старшие 16 бит которого равны нулю, вы тем самым обращались к этой «запретной зоне». Поскольку данный блок виртуальной памяти объявлен свободным, при любых попытках чтения/записи возникают ошибки защиты. Каждый поток вашей программы создает отдельный стек, представленный в виртуальной памяти тремя блоками: большой зарезервированный блок для роста стека, одностра- ничный «сторожевой» блок для обнаружения роста стека и актуализированный блок для реально используемой части стека. Все DLIVEXE вашей программы могут создавать свои собственные кучи, если они не используют DLL-версию runtime-библиотеки C/C++ (вот почему в виртуальной памяти так часто встречаются кучи). Пользовательские модули обычно загружаются в нижнюю часть виртуальной памяти, а системные DLL — в верхнюю часть. Наибольший свободный блок между ними определяет максимальный объем данных, которые могут одновременно обрабатываться программой Win32. Обычно объем этого свободного блока виртуальной памяти составляет около 1,5 гигабайта. В нашем тесте между двумя «снимками» памяти примерно у десяти блоков изменились контрольные суммы или поменялся начальный адрес/размер. При этом произошло несколько событий — вывод результата первого «снимка», переключение на другую страницу, создание однородной кисти и возвращение к предыдущей странице. Поскольку при этом был создан по крайней мере один новый объект GDI, таблица объектов GDI должна находиться в одном из этих блоков. Впрочем, десять блоков — все еще слишком много, чтобы просматривать их один за другим. Однако большинство кандидатов удается сразу отвергнуть, поскольку размер блока должен быть больше некоторого порогового значения. Из предыдущего раздела мы знаем, что максимальное количество манипуляторов объектов GDI равно 12 000 для одного процесса или 16 000 для всей системы. Если предположить, что каждый элемент таблицы объектов GDI кодируется минимум четырьмя байтами, размер таблицы должен превышать 64 Кбайт (в шест- надцатеричной записи — 0x10000). Четыре байта составляют минимальный объем памяти для хранения указателя на более сложную структуру данных. С учетом ограничений на размер остается всего два блока (оба видны на рис. 3.4). Оба блока актуализированы, защищены от записи и не имеют осмысленных имен. Оба имеют довольно крупный размер, 268 Кбайт (0x43000) и 384 Кбайт (0x60000). Один блок имеет атрибут PAGE_READ0NLY, а другой - PAGE_ EXECUTE_READ. Чтобы понять, в каком блоке может находиться искомая таблица, проще всего просмотреть их содержимое. При двойном щелчке на первом столбце таблицы на экране появляется диалоговое окно, в котором отображается ше- стнадцатеричный дамп выбранного блока. Теперь дважды щелкните на первом столбце блока, начинающегося с адреса 0x0045000. Преобразуйте дамп к формату двойных слов (для этого устанавливается переключатель Dword). При просмотре содержимого блока вскоре становится ясно, что перед вами таблица с элементами размером по 16 байт. Чтобы убедиться в том, что таблица по адресу 0x00450000 действительно является искомой таблицей объектов GDI, сохраните содержимое блока в текстовом файле (кнопка Dump), создайте несколько объектов GDI, запомните их манипуляторы, со-
Поиск таблицы объектов GDI 161 храните новый дамп того же блока и сравните два файла при помощи какой- нибудь утилиты (например, WinDiff). Допустим, вы создали 256 однородных кистей и получили манипуляторы со значениями от 0x01300440 до 0х0130052Ь; индексы этих манипуляторов лежат в интервале от 0x440 до 0x52b. Просмотрите дамп по адресам от 0x00454400 до 0х004552Ь0; вы увидите, что после создания объектов содержимое этих адресов изменилось. На рис. 3.5 показан дамп блока памяти, начинающегося с адреса 0x45000. ?ffi%ffi$±\&>£- п>. Г Word & 0G45G1GQ: QCWSOUQ? 00450*20; 0D45O1308 QU45Di40i 00450150s OQ450WO? OD450170\ e13с£3вв e!3837G8 000GGGGQ OQGOQQQB йййййййй 00OOOOO0 QOOOOO0O 01100190 01103*90 01100190 01100*90 01100190 01100190 оиосшк* 0000GGGG dqqockkki 00000800 00000000 00000000 00000000 00000000 00000000 * * t * Л8* .78. Ц. % Ч Ч Ч % л «• # * * 4 * * * * I ■ * * % i ► » » 4» J I- * * * * 4 K< \\ШЛ Dump 1 Search jc I Рис. З.5. Дамп памяти возможной таблицы объектов GDI В этой программе таблица объектов GDI нашлась по адресу 0x450000, но нет никаких оснований полагать, что этот адрес является фиксированным. Если внимательно присмотреться к структуре виртуальной памяти, вы увидите, что блок 0x450000 расположен в памяти после главной программы Handles.exe. Следовательно, для программы меньшего размера таблица объектов могла бы начинаться с адреса 0x420000, а для большой программы — с адреса 0x630000. Нам нужен более простой и надежный способ поиска таблицы в памяти. Поскольку адрес таблицы в памяти не фиксируется, a GDI часто приходится обращаться к ней, можно сделать вывод — ссылка на таблицу должна присутствовать в сегменте данных GDI. Откройте диалоговое окно Memory Dump для секции данных GDI32 (.data), перейдите в режим двойных слов (переключатель Dword), введите адрес 450000 и щелкните на кнопке Search. На экране появляется следующий отчет: Search for 0x00450000 in region start at 77f78000. size 1000 bytes 77f78008 77f780bc Две найденные ссылки означают, что в gdi32.dll хранятся две внутренние переменные для обращения к таблице объектов GDI. Продолжим поиск в секции кода GDI (.text) с адресами этих двух переменных, 0x77f78008 и 0x77f780bc. На вторую переменную находятся четыре ссылки, а на первую — несколько сотен. Сравнение адресов с выходными данными Quick-
162 Глава 3. Внутренние структуры данных GDI/DirectDraw View или Dumpbin для gdi32.dll наглядно показывает, что ссылки встречаются во множестве функций, в том числе в SelectObject и GetObjectType. Однако среди функций, использующих указатели на таблицу объектов GDI, особый интерес вызывает одна недокументированная функция — GdiQueryTable. В высшей степени любопытное имя... Оно подсказывает, что где-то существует какая-то таблица, и при помощи этой функции можно получить информацию о ней. Давайте посмотрим, что же делает эта загадочная функция. // querytab.cpp #define STRICT #iinclude <windows.h> typedef unsigned (CALLBACK * ProcO) (void); void TestGdiQueryTable(void) { ProcO p = (ProcO) GetProcAddress(GetModuleHandle("GDI32.DLL"). "GdiQueryTable"); if (p) { TCHAR temp[32]; wsprintf(temp. "Я81Х". p()); MyMessageBox(NULL. temp. "GdiQueryTableO returns". MB_0K); } return 0; } Функция GdiQueryTable возвращает тот же адрес 0x45000, который был получен экспериментальным путем. После долгих хлопот с поисками в виртуальной памяти мы достигли своей цели — действительно, в Windows NT/2000 существует общесистемная таблица объектов GDI и даже имеется недокументированная функция GdiQueryTable, которая возвращает указатель на эту таблицу. В программах пользовательского режима эта таблица доступна только для чтения. Если на вашем компьютере установлены символические файлы для gdi32.dll, запустите программу Handles.exe в отладочном режиме, переключитесь в режим ассемблерного кода и выберите команду Edit ► Go To — на экране появляется диалоговое окно. Введите в нем адрес 0x77f78008 или 0x77f780bc. Отладчик Visual C++ показывает для первого адреса имя _pGdiSharedHandleTable, а для второго — _pGdiSharedMemory. Итак, первый адрес соответствует указателю на общую таблицу объектов GDI, а второй — указателю на общую память GDI, причем оба блока памяти начинаются с одного и того же адреса. Если вместо адреса ввести имя _GdiQueryTable@0 (суффикс означает, что функция вызывается без параметров), отладчик покажет ассемблерный код недокументированной функции GdiQueryTable. Функция устроена элементарно — она просто возвращает содержимое указателя _pGdiSharedHandleTable. Расшифровка таблицы объектов GDI В разделе «Расшифровка манипуляторов объектов GDI» говорилось, что максимальное количество манипуляторов в таблице равно 16 384. В разделе «Поиск таблицы объектов GDI» мы убедились в том, что таблица объектов GDI сущест-
Расшифровка таблицы объектов GDI 163 вует и что она доступна из адресного пространства пользовательского режима. На рис. 3.5 приведено начальное содержимое таблицы объектов GDI. При внимательном изучении дампа на рис. 3.5 вырисовывается четкая закономерность циклов, повторяющихся через каждые 16 байт: сначала следует большое 32-разрядное значение, затем нулевая 32-разрядная величина, еще одно ненулевое 32-разрядное значение и еще 32 нулевых бита. Размер предполагаемой таблицы объектов GDI равен 268 Кбайт, что при делении на 16 384 дает 16,75. Итак, можно с уверенностью сказать, что размер элемента таблицы объектов GDI равен 16 байтам. Главной задачей этого раздела станет расшифровка структуры этой 16-байтовой записи. Если воспользоваться экспериментальными методами, описанными в двух предыдущих разделах, можно прийти к следующей структуре: typedef struct { void * pKernel; unsigned short nProcess; unsigned short nCount; unsigned short nUpper: unsigned short nType; void * pUser; } GdiTableCell: В первых 4 байтах элемента таблицы GDI содержится указатель, значение которого обычно превышает ОхЕ 1000000. Следовательно, он относится к верхним 2 гигабайтам адресного пространства Windows NT/2000, доступным только для кода режима ядра. Речь идет о том, что для каждого объекта GDI в адресном пространстве режима ядра существует структура данных, на которую ссылается таблица объектов GDI. ПРИМЕЧАНИЕ В Windows NT/2000 область памяти от ОхЕЮООООО до OxECFFFFFF (192 Мбайт) представляет собой выгружаемый (paged) пул ядра, в котором хранятся динамически выделяемые структуры данных компонентов ядра. Его отличие от невыгружаемого пула заключается в том, что первый при нехватке системной памяти может выгружаться на диск, тогда как последний заведомо всегда остается в физической памяти. Как будет показано ниже, структуры данных GDI, относящиеся к режиму ядра (включая аппаратно-зависимые растры, DDB), обычно хранятся в выгружаемом пуле. Следующие два байта (поле nProcess) содержат идентификатор процесса, создавшего объект. Идентификатор текущего процесса возвращается функцией GetCurrentProcessId. Для некоторых объектов (например, стандартных объектов GDI) это поле может быть равно 0. Два байта, следующих за nProcess, обычно равны нулю. Впрочем, при некоторых условиях значение может быть и ненулевым. Похоже, в них хранится счетчик применений манипулятора объекта; по этой причине в определении структуры этому полю присвоено имя nCount. За nCount следует поле nUpper — точная копия верхних двух байтов манипулятора объекта GDI. Из предыдущих разделов мы знаем, что nUpper состоит из неизвестного старшего байта и младшего байта с информацией о типе объекта.
164 Глава 3. Внутренние структуры данных GDI/DirectDraw За полем nUpper следует 2-байтовое поле пТуре, содержащее внутреннюю информацию о типе объекта. Последние 4 байта GdiTableCell (поле pUser) содержат еще один указатель. Как правило, значение pUser равно NULL. Если это поле отлично от NULL, в нем хранится указатель на нижние 2 гигабайта адресного пространства, доступных для программного кода пользовательского режима. Для некоторых типов объектов GDI создает структуру данных, локальную по отношению к текущему процессу. Указатели пользовательского режима доступны из адресного пространства режима ядра, но лишь в том случае, если они относятся к текущему процессу. Итак, мы знаем, как получить адрес таблицы объектов GDI и какую структуру имеет каждый элемент таблицы. Все эти сведения будут объединены в класс C++, упрощающий работу с таблицей объектов GDI в Windows-программах. Класс KGDITable приведен в листинге 3.2. Листинг 3.2. Класс KGDITable для работы с таблицей объектов GDI // GDITable.h #pragma once class KGDITable { GDITableCell * pGDITable; public: KGDITableO; GDITableCell operator[](HGDIOBJ hHandle) const { return pGDITableC (unsigned) hHandle & OxFFFF ]; } GDITableCell operator[](unsigned nlndex) const { return pGDITableC nlndex & OxFFFF ]; } }: // GDITable.cpp #define STRICT #include <windows.h> #include <assert.h> #include "Gditable.h" KGDITable::KGDITable() { typedef unsigned (CALLBACK * ProcO) (void); ProcO pGdiQueryTable = (ProcO) GetProcAddress( GetModuleHandle("GDI32.dll". "GdiQueryTable"); assert(pGdiQueryTable): if ( pGdiQueryTable ) pGDITable = (GDITableCell *) pGdiQueryTableO: else
Расшифровка таблицы объектов GDI 165 pGDITable = NULL; } Работать с классом KGDITable очень просто. Ниже показано, как получить адрес структуры данных режима ядра для стандартного объекта черного пера. const void * BlackPenpKernel(void) { KGDITable gditable; return gditable[GetStockObject(BLACK_PEN)].pKernel; } На рис. 3.6 изображена новая страница свойств, Decode GDI Object Table (Расшифровка таблицы объектов GDI) нашей программы GDIHandles. На этой странице содержимое таблицы объектов GDI выводится в структурированном виде. Decode SOI Heftdte \ Uoate GDI Handle Table Qeceds SW ■W £*»се*Я ОзлХу fie | jQuery GDI Table i£xi 1 Xn&gx 11шшш*&?388к 57 4a9 4d4 4d3 1 4e8 I 4f2 4f4 537 S3b SaS 5a7 N Index: 1 |sK«!ira«l e271ale8 elec5008 elec44c8 elec29e8 e2307328 e26012c8 e272e008 e2789388 e2713008 e2S89008 e27164c8 e21a4aa8 55, Handle: r — } nCmmfc 0 0 0 0 0 0 0 0 0 0 0 0 740S00SS | «tP.5tO« Sc8 Sc8 Sc8 Sc8 5c8 Sc8 Sc8 Sc8 Sc8 Sc8 Sc8 Sc8 , Type: OB ntTp^fftr j 740S 0101 eeOa SeOl 6504 8el0 9101 760a 4605 3f0a leOa 3310 JJBITMAP ttfVpe 0005 0401 000a 0001 0004 0010 0401 000a 0005 000a 000a 0010 j |*TJs«r 0 7aOS70 135e68 7a01d0 7b0018 0 7a03a0 13Se78 0 135e80 13Se88 7b0000 *i ? *-J A l*>''i *' ж Рис. 3.6. Содержимое таблицы объектов GDI При помощи флажка, находящегося в левом верхнем углу страницы, пользователь выбирает между выводом всех объектов таблицы или только тех объектов, которые были созданы текущим процессом. Попробуйте выделить любой
166 Глава 3. Внутренние структуры данных GDI/DirectDraw объект GDI в списке; в нижней части страницы появится дополнительная информация о его индексе, значении HGI0BJ и типе объекта. Располагая таким замечательным инструментом для вывода содержимого таблицы объектов GDI, мы можем провести дополнительные эксперименты с объектами GDI и глубже исследовать принципы управления этими объектами. Указатель pKernel ссылается на выгружаемый пул Для любого действительного объекта GDI указатель pKernel всегда отличен от NULL и имеет уникальное значение. Похоже, для каждого объекта GDI существует некая структура данных, обращения к которой производятся только из кода режима ядра (и даже не из gdi32.dll!). Как видно из значений pKernel, объекты разных процессов не имеют четкого деления на разные области памяти. Адреса объектов, на которые указывает pKernel, всегда начинаются с ОхЕЮООООО. Как сообщается в книге «Inside Windows NT», область памяти, начинающаяся с ОхЕЮООООО, представляет собой выгружаемую системную кучу, которая обычно называется «выгружаемым пулом» (paged pool). Visual C++ не разыменовывает эти указатели, поэтому мы пока не сможем узнать, что за ними скрывается. В сущности, Visual Studio — обычная программа пользовательского режима, не поддерживаемая специальными драйверами ядра. Мы вернемся к указателю pKernel в разделе «WinDbg и расширение отладчика GDI» и исследуем его при помощи драйвера режима ядра, который мы создадим в разделе «Обращение к адресному пространству режима ядра». Поле nCount иногда используется как счетчик выбора объектов В Windows 2000 поле nCount всегда равно нулю, то есть оно не используется. Однако в Windows NT 4.0 это поле требуется для некоторых объектов GDI. Чтобы лучше понять смысл nCount, поэкспериментируйте с выбором и восстановлением объектов в одном или нескольких контекстах устройств и проследите за изменениями nCount. В сущности, вы должны создать объект, выбрать его в двух контекстах устройств, потом восстановить старые объекты и, наконец, удалить созданный объект. Как выясняется из этого маленького эксперимента, при создании объекта его поле nCount равно нулю, и для многих типов объектов это значение остается неизменным. Для аппаратно-зависимых растров (DDB) поле nCount при выборе объекта в DC изменяет значение с 0 на 1. Если попробовать заново выбрать растр в другом DC, попытка завершится неудачей. При исключении растра из первого DC поле nCount возвращается к нулевому состоянию. Несомненно, применительно к DDB поле nCount обеспечивает выполнение требования о том, что растр не может выбираться в нескольких контекстах одновременно. Для шрифтов — другого типа объектов GDI, использующего поле nCount, — в этом поле хранится простой счетчик выбора, не накладывающий никаких ог-
Расшифровка таблицы объектов GDI 167 раничений. Выбор логического шрифта во втором контексте устройства проходит успешно, а значение поля nCount при этом увеличивается. Многие программисты задают один очевидный вопрос — существует ли в GDI какой-то механизм защиты от удаления объектов, выбранных в контексте устройства? Ответ — да, существует... по крайней мере, для палитр. Как видно из табл. 3.1, первый вызов DeleteObject после выбора палитры в двух DC завершается неудачей, но второй вызов DeleteObject после исключения палитры из обоих DC работает нормально. Впрочем, поле nCount в этой защите не используется. Другие объекты GDI (например, шрифты, растры, кисти и перья) могут быть удалены программистом в любой момент времени, при этом манипулятор выбранного объекта становится недействительным. Трудно сказать, почему Windows не поддерживает единые правила использования nCount, которые бы предотвращали удаление всех выбранных объектов. Таблица 3.1. Использование поля nCount Функция API Растр (DDB) Шрифт Палитра Create...() SelectObject(hDCl) Select0bject(hDC2) DeleteObject() (De)Select0bject(hDC2) (De)SelectObject(hDCl) DeleteObjectO Успех, nCount^O Успех, nCount=l Неудача, nCountsl Неудача, nCount^l Успех, nCount=0 Успех Успех, nCount=0 Успех, nCount^l Успех, nCount=2 Успех, nCount^l Успех, nCount=0 Успех Успех, nCount^O Успех, nCounte0 Успех, nCount«0 Неудача Успех, nCount^O Успех, nCount-0 Успех Поле nProcess связывает манипулятор GDI с конкретным процессом Если программа пытается воспользоваться манипулятором объекта GDI, относящегося к другому процессу, вызов функции Win32 API обычно завершается неудачей. За этим «волшебством» стоит поле nProcess структуры GdiTableCell. Для стандартных объектов (например, GetStockObject(BLACK_PEN)) поле nProcess равно нулю. Для других объектов GDI, созданных пользовательскими процессами, поле nProcess содержит идентификатор процесса, создавшего объект. Чтобы получить идентификатор текущего процесса, вызовите функцию GetCurrent- ProcessIdO. GDI проверяет, совпадает ли идентификатор текущего процесса с содержимым поля nProcess объекта GDI; тем самым обеспечивается выполнение требования о том, чтобы манипуляторы объектов не использовались другими процессами. Страница Decode GDI Object Table позволяет выбрать между отображением всех объектов GDI и только тех объектов, которые были созданы текущим процессом. Если щелкнуть в строке таблицы, в нижней части страницы выводится
168 Глава 3. Внутренние структуры данных GDI/DirectDraw подробная информация о выбранном объекте — в том числе и информация, возвращаемая при вызове GetObject. Но если переключиться в режим вывода всех объектов GDI и щелкнуть на объекте, созданным другим процессом, вызов GetObject завершается неудачей, а программа выводит ошибку «Invalid Object». Согласно документации Microsoft, при завершении процесса освобождаются все созданные им объекты GDI. Вас когда-нибудь интересовало, как это делается? GDI просто перебирает все записи в таблице объектов GDI и удаляет все объекты с идентификатором текущего процесса. nUpper: дополнительная проверка Поле nUpper в таблице объектов GDI содержит точную копию двух старших байтов 4-байтового манипулятора — эта относительно малая избыточность обеспечивает дополнительную проверку манипуляторов объектов GDI. Предположим, вы создали шрифт; функция CreateFont возвращает 0x9d0a047f. Новый объект соответствует элементу таблицы с индексом 0x047f, у которого поле nUpper равно 0x9d0a. Теперь какая-нибудь другая часть программы удаляет шрифт, не зная, что он используется, в результате запись 0x047f освобождается; затем программа создает другой шрифт. Допустим, GDI почему-либо решает задействовать для нового объекта GDI элемент с индексом 0x047f и назначает ему манипулятор 0x9e0a047f. Если первая часть программы попытается воспользоваться манипулятором 0x9d0a047f, вызовы функций Win32 GDI завершатся неудачей — GDI обнаруживает, что 0x9d0a не совпадает с новым значением nUpper элемента 0x047f, которое теперь равно ОхЗреОа. Попробуйте изменить старший байт манипулятора GDI, сохранив три остальных байта, в которых хранится информация о типе объекта; вы увидите, что вызовы GetObject и GetObjectType завершаются неудачей. Хранение старшего слова манипулятора в таблице находит и другие применения. Если вам известен только индекс манипулятора в таблице GDI, вы сможете восстановить весь манипулятор, прочитав nUpper из таблицы и объединив эти два значения. Например, эта возможность используется при реализации 16- разрядной поддержки GDI в Windows NT. Вспомните: в 16-разрядном интерфейсе GDI используется 16-разрядный манипулятор HGDI0BJ, фактически являющийся индексом. Чтобы 16-разрядная поддержка GDI работала в Windows NT, вызов необходимо переадресовать 32-разрядному интерфейсу GDI, работающему с полноценными 32-разрядными манипуляторами. При анализе структуры манипуляторов GDI в разделе «Поиск таблицы объектов GDI» нерасшифрованными остались лишь старшие 8 бит. Дополнительные эксперименты показывают, что в них хранится счетчик повторного использования — еще одно простое средство проверки манипуляторов. У каждого элемента таблицы объектов GDI первоначальное значение счетчика равно 0. Когда в элемент таблицы заносится информация о новом объекте GDI, его счетчик повторного использования увеличивается (когда значение достигает 255, счетчик снова сбрасывается в 0). Таким образом, когда элемент задействуется впервые, его счетчик повторного использования равен 0x01; это относится ко всем стандартным объектам GDI, которые создаются один раз и никогда не удаляются. Если
Расшифровка таблицы объектов GDI 169 вы удаляете объект GDI и создаете новый объект в том же элементе таблицы, даже при совпадении типов объектов манипуляторы будут отличаться, поскольку значение счетчика повторного использования увеличилось. Все вызовы функций, в которых присутствует исходный манипулятор, завершатся неудачей. Применение счетчика продемонстрировано в разделе «Структуры данных пользовательского режима» (см. ниже). пТуре: внутренний тип объекта В процессе анализа структуры манипуляторов GDI (см. раздел «Расшифровка манипуляторов объектов GDI») мы выяснили, что в каждом манипуляторе присутствует 7-разрядная информация о типе объекта. Эта информация, расширенная до двух байт, имеется и в таблице объектов GDI. Младший байт пТуре обычно содержит те же 7 бит типа, что и HGDI0BJ, а старший байт обычно равен нулю. В поле пТуре манипулятор расширенного метафайла интерпретируется как манипулятор контекста устройства, а манипулятор расширенного пера — как манипулятор кисти. Для некоторых объектов старший байт пТуре определяет подтип объекта — скажем, подтип «совместимый контекст» (memory context) для типа «контекст устройства». Вот что мы знаем об этом слове внутреннего типа объекта: typedef enum gdi_i nt_objtypew_dc gdi_i nt_objtypew_memdc gdi_int__objtypew_region gdi_int_objtypew_bitmap gdi_int_objtypew_palette gdi_i nt_objtypew_font gdi_i nt_objtypew_brush gdi_int_objtypew_enhmetafi1e gdi_int_objtypew_pen gdi_i nt_objtypew_extpen = 0x0001, = 0x0401. = 0x0004, « 0x0004. = 0x0008. = 0x000a. = 0x0010. = 0x0001. - 0x0030. = 0x0010. // He все совмеси // Как для DC // Как для кисти pUser: указатель на структуру данных пользовательского режима Вероятно, вы обратили внимание на симметричное расположение полей в структуре GdiTableCell: она начинается с указателя, затем следуют четыре 16-разрядных слова, а затем следует другой указатель pUser. Обычно указатель pUser равен NULL — исключение составляют некоторые типы объектов GDI. Если указатель отличен от NULL, он принимает такие значения, как 0х001420с8 или 0x790320. Как нетрудно убедиться, эти значения соответствуют действительным адресами блоков памяти, доступным для чтения и записи. Структуры данных объектов GDI более подробно рассматриваются в следующем разделе.
170 Глава 3. Внутренние структуры данных GDI/DirectDraw Структуры данных пользовательского режима Как было показано в предыдущем разделе, каждому объекту GDI соответствует элемент глобальной таблицы объектов GDI, в котором хранится указатель с именем pUser. Для большинства объектов GDI указатель pUser равен NULL (то есть не используется). Тем не менее для объектов кистей, регионов, шрифтов и контекстов устройств поля pUser в таблице объектов GDI ссылаются на довольно интересные структуры данных в адресном пространстве пользовательского режима. Этой теме и посвящен данный раздел. Структура данных пользовательского режима для кистей: оптимизация создания однородных кистей Для однородных кистей указатель pUser ссылается на блок из 24 байт, в котором первые 12 байт содержат копию структуры L0GBRUSH. Если кисть является однородной, она обладает лишь одним атрибутом — цветом. Для остальных типов кистей pUser содержит NULL. typedef struct { LOGBRUSH logbrush; DWORD dwUnused[3]; } User_Data_SolidBrush; Если вам непонятно, почему в реализации GDI однородные кисти занимают особое место, попробуем поставить вопрос иначе — чем однородные кисти отличаются от остальных кистей? Прежде всего тем, что эти объекты GDI живут недолго и используются в больших количествах. При создании градиентных заливок, теней или эффектов освещения сотни и тысячи однородных кистей создаются, разок-другой используются при выводе фрагмента изображения, а затем немедленно удаляются. Поскольку однородные кисти требуются в больших количествах, приложение не может хранить их до следующего раза; в противном случае вы рискуете превысить максимальный размер таблицы объектов GDI. Таким образом, большинство однородных кистей уничтожается сразу же после использования и создается заново в случае необходимости. Сохраняя копию структуры LOGBRUSH в пользовательском режиме, GDI оптимизирует стандартную последовательность действий «создание — использование — удаление» для большого количества кистей. При удалении первой однородной кисти GDI не производит фактического уничтожения создания структуры данных, а сохраняет ее на будущее. Когда программе потребуется новая однородная кисть, GDI берет готовую кисть и изменяет ее цвет; это позволяет обойтись без обращения к режиму ядра для выделения блока памяти и заполнения его данными новой кисти. Чтобы разобраться в происходящем, проведем несложный эксперимент. Попробуйте в цикле создать, проанализировать и уничтожить восемь однородных кистей. Как видно из табл. 3.2, GDI сохраняет в таблице GDI несколько одно-
Структуры данных пользовательского режима 171 родных кистей для дальнейшего использования. Обратите внимание: кисти 1, 3 и 6 имеют одинаковый индекс ОхЗеН. Их поля pKernel и pUser совпадают, но поля lbColor в структуре L0GBRUSH, на которую ссылается pUser, различаются. Таблица 3.2. Повторное использование манипуляторов однородных кистей Манипулятор ОхабЮЗеП ОхЗсЮЗШ 0ха7103е11 0x941031be 0x3dl031f5 0xa8103ell 0x5fl03f49 0x951031be lbColor 0x000000 0x202020 0x404040 0x606060 0x808080 OxaOaOaO OxcOcOcO OxeOeOeO pKernel 0xel25d710 0xel25d878 0xel25d710 0xel25da70 0xel25d878 0xel25d710 0xel25d908 0xel25da70 pUser 0x870048 0x870060 0x870048 0x870078 0x870060 0x870048 0x870090 0x870078 Таблица 3.2 также иллюстрирует то, что говорилось выше о счетчике повторного использования (старшие 8 бит манипулятора GDI). Манипуляторы 1, 3 и 6 в табл. 3.2 создаются в одном и том же элементе таблицы GDI ОхЗеН; все они соответствуют объекту кисти (0x10), однако их счетчики повторного использования отличаются на 1. Структура данных пользовательского режима для регионов: оптимизация прямоугольных регионов Объекты регионов создаются такими функциями, как CreateRectRgn и ExtCreateRgn. По аналогии с кистями, поле pUser используется для простейшего случая — прямоугольных регионов. Размер блока данных прямоугольного региона, адресуемого указателем pUser, равен 24 байтам. Смысл первых двух двойных слов в этом блоке неизвестен, а остальные 16 байт образуют структуру RECT: typedef struct { DWORD dwUnknownl: // - 17 DWORD dwUnknown2: //-1,2 RECT rcBound: } UserData_RectRgn; Как и следовало предположить, манипуляторы прямоугольных регионов, как и манипуляторы кистей, многократно используются GDI. Попробуйте провести простой эксперимент — создайте прямоугольный регион, сохраните значения указателей pKernel и pUser и затем удалите регион. Повторите 8 раз. Вы увидите, что GDI три раза использует старый индекс без изменения указателей pKernel и pUser, хотя координаты прямоугольника при этом изменяются.
172 Глава 3. Внутренние структуры данных GDI/DirectDraw Как правило, создание и последующее использование объектов GDI осуществляется только средствами GDI. Состояние созданного объекта GDI жестко фиксируется. В объектно-ориентированном программировании подобные объекты называются неизменяемыми (immutable). Например, после создания кисти вы уже не сможете напрямую изменить ее цвет. Объекты регионов являются исключением из этого правила — функция SetRectRgn преобразует существующий регион в прямоугольный регион с заданными координатами. Зная определение структуры данных, указатель на которую хранится в поле pUser, вы легко поймете, как реализуется эта функция — GDI просто убеждается в том, что поле pUser не пусто (то есть в памяти была создана структура UserDataRectRgn), и присваивает координатам заданные значения. Таким образом, регионы как объекты GDI являются изменяемыми (mutable). Структура данных пользовательского режима для шрифтов: таблица значений ширины Для шрифтов в Windows GDI определяется больше структур данных, чем для любого другого объекта: LOGFONT, TEXTMETRIC, PANOSE, ABC, GLYPHSET и т. д. Однако в таблице объектов GDI не обнаруживается ни малейшего следа этих структур. Структура данных пользовательского режима для манипулятора шрифта устроена очень просто: typedef struct { DWORD dwUnknown; // = О void *pCharWidthData: // = 1. 2 } UserData_Font; Первое поле UserDataFont всегда равно нулю. Второе поле обычно равно нулю и изменяется только после вызова таких функций, как GetCharWidth. Функция GetCharWidth заполняет целочисленный массив сведениями о ширине символов, принадлежащих заданному интервалу. После вызова GetCharWidth указатель pChar- WidthData указывает на структуру данных, выделенную из системной кучи, которая в основном содержит кэшируемую таблицу значений ширины. Перед нами еще один пример того, как GDI прикладывает дополнительные усилия для оптимизации быстродействия. Получив значение pCharWidthData (например, 0х1456Ь0), вы можете без особого труда вычислить, где находится этот адрес. На странице Locate GDI Object Table программы GDIHandles отображается список всех блоков памяти в пользовательском адресном пространстве. Из этого списка видно, что адрес 0х1456Ь0 принадлежит первой куче, то есть куче процесса по умолчанию. Если дважды щелкнуть на строке первой кучи, на экране появляется окно дампа памяти (см. рис. 3.5). Щелкните на кнопке Dump — содержимое блока сохраняется в текстовом файле вместе со списком всех блоков, выделенных из кучи. Структура данных пользовательского режима для контекста устройства: атрибуты Перед выполнением любых операций вывода в Windows GDI необходимо получить манипулятор контекста устройства. Вы можете создать собственный мани-
Структуры данных пользовательского режима 173 пулятор или получить его от операционной системы. Контекст устройства обладает двумя десятками атрибутов, значения которых могут читаться и задаваться в программах. Например, к числу распространенных атрибутов контекстов устройства относятся режим отображения, цвет текста, цвет фона, а также выбранные объекты кисти, пера и шрифта. Естественно, GDI хранит информацию об атрибутах контекста устройства в структуре данных. В Windows NT/2000 указатель на эту структуру хранится в поле pUser таблицы объектов GDI для манипулятора контекста устройства. После утомительного процесса изменения атрибутов контекста и наблюдения за модификациями двоичных данных можно получить примерное представление о структуре, на которую ссылается указатель pUser для контекста устройства. Впрочем, полученная информация будет неполной и недостоверной. При использовании расширения отладчика GDI уровня ядра, предоставляемого Microsoft, в сочетании с утилитой WinDbg (отладчик исходных текстов системного уровня от Microsoft) вырисовывается значительно более полная и завершенная картина. Использование расширения отладчика GDI подробно описано в разделе «WinDbg и расширение отладчика GDI». Ниже приведена та информация, которую нам удалось получить об этой структуре данных, занимающей 456 байт в Windows 2000 (400 байт в Windows NT 4.0). // dcattr.h typedef struct { UL0NG ull; UL0NG ul2; } FL0AT0BJ; typedef struct { FL0AT0BJ efMll; FL0AT0BJ efM12; FL0AT0BJ efM21: FL0AT0BJ efM22; FL0AT0BJ efDx; FL0AT0BJ efDy; int fxDx; i nt f xDy; long flAccel; } MATRIX; // Windows NT 4.0: 0x190 байт // Windows 2000 : 0xlC8 байт typedef struct { void * pvLDC; // 000 UL0NG ulDirty; HBRUSH hbrush; HPEN hpen; C0L0RREF crBackgroundClr; // 010 UL0NG ulBackgroundClr;
174 Глава 3. Внутренние структуры данных GDI/DirectDraw COLORREF ULONG crForegroundClr; ulForegroundClr; #if (_WIN32_WINNT >- 0x0500) unsigned #endif int int BYTE BYTE BYTE BYTE POINT POINTFX long long long f20[4]; iCS_CP; iGraphicsMode; JR0P2; jBkMode; jFillMode; jStretchBltMode; ptlCurrent; ptfxCurrent; IBkMode; IFillMode; IStretchBltMode: #1f (_WIN32__WINNT >- 0x0500) long long unsigned flFontMapper; HcmMode; hcmXform; HCOLORSPACE hColorSpace; unsigned unsigned unsigned unsigned #endif long long long long long long HFONT MATRIX MATRIX MATRIX f68; IcmBrushColor; IcmPenColor; f74; flTextAlign; ITextAlign; ITextExtra: IRelAbs; IBreakExtra; cBreak; hlfntNew; mxWorldToDevice; mxDeviceToWorld: mxWorldToPage; // 020 // 030 // 038 // 03C // 044 // 04C // 050 // 058 // 060 // 070 // 078 // 080 // 090 // 094 // 0D0 // 10C unsigned fl48[8]: int iMapMode; #if (_WIN32_WINNT >= 0x0500) DWORD dwLayout; long lWindowOrgx; #endif // 148 // 168 // 16c // 170 POINT ptlWindowOrg; SIZE szlWindowExt; // 174 // 17c
Структуры данных пользовательского режима 175 POINT SIZE long SIZE SIZE POINT unsigned RECT ptlViewportOrg: szlViewportExt; flXform: szlVirtualDevicePixel: szlVirtual DeviceMm; ptlBrushOrigin; flb0[2]: VisRectRegion; // 184 // 18c // 194 // 198 // laO // la8 // IbO // lb8 } DCAttr; Смысл большей части полей структуры DC_ATTR понятен без объяснений. При выборе в контексте устройства объектов GDI (таких, как кисти, перья и шрифты) их манипуляторы сохраняются в соответствующих атрибутах. В структуре не видно и следа присутствия аппаратно-зависимых растров, палитр и регионов. Скалярные атрибуты (цвет текста, цвет фона, графический режим, бинарная растровая операция, режим блиттинга с растяжением, тип выравнивания текста и режим отображения) также хранятся в этой структуре. Некоторые атрибуты (цвет и выравнивание текста) по каким-то неизвестным причинам хранятся в двух экземплярах. В Windows NT/2000 GDI поддерживаются мировые преобразования между логической и физической системами координат. Путем мировых преобразований выполняются трансформации переноса, масштабирования, поворота и сдвига. Мировое преобразование описывается вещественной матрицей XF0RM 2x3, передаваемой при вызове функции SetWorldTransform. Матрица XF0RM не сохраняется в структуре данных DC_ATTR непосредственно в вещественном формате. XF0RM состоит из шести вещественных чисел с одинарной точностью, описывающих линейное преобразование на плоскости. Известно, что стандартное представление вещественных чисел с одинарной точностью в формате IEEE занимает 4 байта, тогда как представление числа с двойной точностью содержит 8 байт. Однако в представлении XF0RM в DC_ATTR не используется ни один из этих двух форматов. XF0RM представляется структурой MATRIX, состоящей из шести пар DWORD и трех 32-разрядных чисел. Ближайшим аналогом этих пар DWORD является структура FL0AT0BJ, определяемая в WinNT DDK. Вещественное число с одинарной точностью в формате IEEE состоит из 32 бит. Оно содержит один знаковый бит, 8-разрядную экспоненту со смещением 127 и 23-разрядную мантиссу с одним скрытым битом, который всегда равен 1. Вещественное число вычисляется по формуле знак * 2А(экспонента-127) * (1«24 + мантисса)2*24. Например, число 1.0 хранится в виде 0x3F800000. Знаковый бит соответствует положительному числу, экспонента равна 127, а все биты мантиссы равны нулю. В соответствии с приведенной выше формулой мы получаем: 1 * 24127-127) * (1 « 24 + 0)/2"24 = 1 Microsoft имитирует вещественные вычисления с помощью целочисленной арифметики с «высоким быстродействием» и точностью. Высокая точность означает длинную мантиссу, а быстродействие достигается конструированием формата, из которого в процессе вычислений легко выделяются компоненты числа.
176 Глава 3. Внутренние структуры данных GDI/DirectDraw Для представления вещественных чисел в GDI Microsoft использует структуру FL0AT0BJ. Эта структура делится на два 32-разрядных числа: старшее двойное слово (и12) определяет экспоненту, а младшее (ull) — мантиссу в сумме со знаковым битом. В отличие от формата IEEE скрытые биты или смещения в FL0AT0BJ не используются. Преобразование структуры FL0AT0BJ в вещественное число выполняется очень просто: double FL0AT0BJ2Double(const FLOATOBJ & f) { return (double) f.ull * pow(2, (double)f.u!2-32)); } Например, при представлении в формате FLOATOBJ числа 1.0 поле и12 равно 2, а поле ull — 0x40000000. Эти два числа легко преобразуются в исходную величину 2А30 * 2А(2 - 32) = 1. Помимо представления XF0RM в формате FLOATOBJ, GDI также хранит в целочисленных полях XF0RM_eDxI и XF0RM_eDyI округленные версии смещений (eDx и eDy). В структуре DCATTR содержится немало полей, смысл которых так и остается для нас загадкой. В то же время многие функции контекстов устройств не приводят к непосредственным изменениям DCATTR; например, вызовы SelectPalette, SetMiterLimit и SetArtDi recti on не изменяют содержимого DC_ATTR. Как будет показано ниже, DC_ATTR всего лишь является частью более сложной структуры данных, поддерживаемой GDI для контекста устройства. В структуре данных контекста устройства, хранящейся в адресном пространстве ядра, содержится зеркальная копия структуры DC_ATTR с большим количеством дополнительной информации о драйвере устройства, поддерживающем контекст. Подведем итог: в этом разделе мы рассмотрели структуры данных, доступные в пользовательском режиме, для объектов однородной кисти, прямоугольного региона, шрифта и контекста устройства. Эти структуры данных достаточно просты и в основном предназначены для оптимизации работы GDI при частых переключениях между привилегиями пользовательского режима и режима ядра. Чтобы лучше разобраться во внутренних структурах данных GDI, необходимо проанализировать остальные структуры данных, доступные в режиме ядра. ПРИМЕЧАНИЕ В системах с процессором Intel не рекомендуется использовать вещественные вычисления и инструкции ММХ в компонентах режима ядра, поскольку состояние этих операций не сохраняется при переключении задач. Это одна из причин, по которой в GDI вещественные числа представляются двумя 32-разрядными целыми. Другая причина — быстродействие на компьютерах с недостаточно быстрой или вовсе отсутствующей поддержкой вещественных вычислений. Графический механизм Windows NT/2000 содержит десятки функций, эмулирующих вещественные операций, — FLOATOBJ_ Add, FLOATOBJ_GreaterThan и т. д. На новых процессорах семейства Intel вещественные операции могут выполняться с такой же скоростью, как и целочисленные, однако преобразование вещественного числа в целое происходит относительно медленно. Windows 2000 содержит пару новых функций, позволяющих драйверу сохранить текущий контекст вычислений и использовать аппаратную поддержку вещественных операций и операций ММХ в режиме ядра.
Обращение к адресному пространству режима ядра 177 Обращение к адресному пространству режима ядра Нашим первым шагом к расшифровке структур данных GDI режима ядра должна стать возможность чтения данных из адресного пространства режима ядра в программе пользовательского режима (вроде GDIHandle). В Windows NT/2000 каждому процессу отводится адресное пространство объемом 4 гигабайта, но только нижние 2 гигабайта доступны из программ пользовательского режима. Верхние два гигабайта недоступны для программ пользовательского режима как для чтения, так и для записи или исполнения. При любых попытках обратиться к верхним 2 гигабайтам адресного пространства непосредственно из программы пользовательского режима генерируется аппаратная ошибка защиты. Даже отладчик Microsoft Visual C++ является программой пользовательского режима. Именно по этой причине он не позволяет, например, получить данные по адресу ОхЕ 1234580 или провести пошаговое выполнение DLL режима ядра (как, например, win32k.sys). Работа более мощных отладчиков — таких, как Numega Softlce/W — обеспечивается драйверами режима ядра. Если вы уже работали с Softlce/W, возможно, вы заметили, что при ручном запуске Softlce/W на короткое время появляется окно DOS с командой net start ntice. Эта команда загружает DLL режима ядра ntice.sys в адресное пространство ядра и создает новое устройство — компонент пользовательского режима, с которым взаимодействует Softlce/W. Драйверы режима ядра представляют собой специальные DLL, построенные по определенным правилам. Например, драйверы режима ядра не могут вызывать функции Win32 API, поскольку их точки входа расположены в пользовательском адресном пространстве. Драйверы режима ядра загружаются в адресное пространство ядра, в котором программы могут работать со всеми 4 гигабайтами адресного пространства. Большинство драйверов устройств Windows NT поддерживает операции ввода-вывода, моделируемые посредством файловых операций. Чтобы обратиться к этим драйверам средствами Win32 API, достаточно вызвать функцию Create- File, ReadFile, WriteFile или менее известную функцию DeviceloControl. Например, при помощи файловых операций можно работать с драйверами последовательного порта, параллельного порта и файловой системы. Кроме того, в Win32 предусмотрен набор функций для загрузки, запуска, остановки и закрытия драйверов устройств через служебный интерфейс API. Изящество подобного решения заключается в том, что драйвер устройства не обязан соответствовать реальному физическому устройству — такому, как параллельный порт или USB (Universal Serial Bus). Вы можете создать воображаемое устройство, написать для него драйвер, установить его функцией Win32 API и затем работать с ним при помощи файловых операций Win32. Архитектура драйверов устройств Windows NT/2000 позволяет решать всевозможные хитроумные задачи, не решаемые одними средствами Win32. Все, что требуется на текущей стадии наших исследований, — это возможность чтения данных из адресного пространства ядра. Если рассматривать 2-гигабайтное адресное пространство как виртуальный диск, можно написать для него
178 Глава 3. Внутренние структуры данных GDI/DirectDraw драйвер, который позволит прочитать любой блок памяти и передать его приложению пользовательского режима. Обычно драйвер устройства ввода-вывода для режима ядра Windows NT/2000 содержит одну точку входа, DriverEntry, вызываемую при загрузке драйвера: NTSTATUS DriverEntrydN PDRIVER_OBJECT Driver. IN PUNICODEJTRING RegistryPath) Функция DriverEntry предназначена для тех же целей, что и _DllMainStart- CRTStartup, точка входа в DLL пользовательского режима. Однако в отличие от DLL пользовательского режима, драйверы режима ядра обычно не экспортируют функций. Вместо этого DriverEntry сообщает системе адреса функций, которые должны экспортироваться системными средствами, с использованием структуры DRIVERJBJECT. Простой драйвер ввода-вывода может ограничиться реализацией минимального подмножества функций. Например, прршеденный ниже фрагмент DriverEntry экспортирует две функции. Функция DrvUnload вызывается при выгрузке драйвера. Функция DrvDispatch вызывается при создании, закрытии и вызове DeviceloControl. Driver->Driverllnload = DrvUnload; Driver->MajorFunction[IRP__MJ_CREATE] - DrvDispatch; Driver->MajorFunction[IRP_MJ_CLOSE] - DrvDispatch; Driver->MajorFunction[IRP_MJ__DEVICE_CONTROL] - DrvDispatch; Для достижения поставленной цели — чтения данных из адресного пространства ядра в пользовательском режиме — нам понадобится простой драйвер устройства. Назовем его Periscope. Главная функция драйвера — обработка запроса DeviceloControl. В параметре DeviceloControl передается начальный адрес и размер читаемого блока данных. Periscope читает данные в режиме ядра и сохраняет их в буфере, доступном из пользовательского режима при выходе из DeviceloControl. Ниже приведен полный исходный текст драйвера Periscope — «перископа», через который мы будем наблюдать за работой ядра. #include "kernelopt.h" #include "periscope.h" const WCHAR DeviceNameC] - L"\\Device\\Periscope"; const WCHAR DeviceLinkC] - L"\\DosDevices\\PERISCOPE"; // Обработка CreateFile. CloseHandle NTSTATUS DrvCreateClosedN PDEVICE_OBJECT DeviceObject. IN PIRP Irp) { Irp->IoStatus.Information = 0; Irp->IoStatus.Status - STATUS_SUCCESS: IoCompleteRequestdrp, I0_N0_INCREMENT); return STATUSJUCCESS: } // Обработка DeviceloControl NTSTATUS DrvDeviceControKIN PDEVICE_OBJECT DeviceObject. IN PIRP Irp)
Обращение к адресному пространству режима ядра 179 NTSTATUS nStatus = STATUSJNVALID_PARAMETER; Irp->IoStatus.Information = 0; // Получить указатель на текущую позицию стека. // в которой находятся коды функций и параметры PI0_STACK_L0CATI0N irpStack - IoGetCurrentIrpStackLocation (Irp); unsigned * ioBuffer = (unsigned *) Irp->AssociatedIrp.SystemBuffer; if ( (irpStack->Parameters.DeviceIoControl.loControlCode — IOCTL_PERISCOPE) && (ioBuffer!=NULL) && (irpStack->Parameters.DeviceIoControl. InputBufferLength >« 8) ) { unsigned leng - ioBuffer[l]; if ( irpStack->Parameters.DeviceIoControl. OutputBufferLength >- leng ) { Irp->IoStatus.Information * leng; nStatus - STATUSJUCCESS: _try { memcpy(ioBuffer. (void *) ioBuffer[0]. leng); } ^.except ( EXCEPTION JXECUTEJANDLER ) { Irp->IoStatus.Information - 0; nStatus - STATUSJNVALID_PARAMETER; } } } Irp->IoStatus.Status - nStatus: IoCompleteRequestCIrp. IOJJOJNCREMENT); return nStatus; // Обработка выгрузки драйвера void DrvUnloadCIN PDRIVER_OBJECT DriverObject) { UNICODE_STRING deviceLinkUnicodeString; RtlInitUnicodeString(&deviceLinkUnicodeString, DeviceLink); IoDeleteSymbolicLinkC&devi ceLi nkUni codeStri ng); IoDeleteDevice(DriverObject->DeviceObject); }
180 Глава 3. Внутренние структуры данных GDI/DirectDraw // Инициализационная точка входа // для устанавливаемых (installable) драйверов NTSTATUS DriverEntrydN PDRIVER_OBJECT Driver. IN PUNICODE_STRING RegistryPath) { UNICODE_STRING deviceNameUnicodeString; RtlInitUnicodeString( SdeviceNameUnicodeString. DeviceName ); // Создать устройство PDEVICE_OBJECT deviceObject = NULL; NTSTATUS ntStatus = IoCreateDevice (Driver. sizeof(KDeviceExtension). & deviceNameUnicodeString. FILE_DEVICE_PERISCOPE. 0. TRUE. & deviceObject); if ( NT_SUCCESS(ntStatus) ) { // Создать символическую ссылку, по которой приложения Win32 // будут получать доступ к драйверу/устройству UNICODEJTRING deviceLinkUnicodeString; RtllnitUnicodeString (SdeviceLinkUnicodeString. DeviceLink); ntStatus = IoCreateSymbolicLink( &devi ceLinkUni codeStri ng. &deviceNameUnicodeString); // Создать диспетчерскую таблицу драйвера if ( NT_SUCCESS(ntStatus) ) { Driver->DriverUnload = DrvUnload; Driver->MajorFunction[IRP_MJ_CREATE] = DrvCreateClose; Dri ver->MajorFuncti on[IRP_MJ_CLOSE] = DrvCreateClose; Driver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DrvDeviceControl; } } if ( !NT_SUCCESS(ntStatus) && deviceObject!=NULL ) IoDeleteDevice(deviceObject): return ntStatus; } Основная часть кода составляет «скелет» базового драйвера режима ядра. Каждому устройству, поддерживаемому драйвером, должно соответствовать некоторое имя; в нашем примере используется имя Periscope. Это имя заносится в каталог Device пространства имен объектов Windows. В DDK входит небольшая утилита objdir, которая, помимо прочего, выводит список драйверов устройств, установленных в вашей системе. Функция DrvCreateClose обрабатывает создание и закрытие экземпляров устройства, инициируемые при вызове функций API CreateFile и CloseHandle. Функция DrvDeviceControl решает главную задачу — чтение блока памяти при вызове DeviceloControl в пользовательском приложении. Весь «интересный» код сосредоточен в нескольких строках функции DrvDeviceControl. Убедившись в том, что при вызове был передан правильный код, буфер
Обращение к адресному пространству режима ядра 181 ввода-вывода не пуст, а длина переданного параметра не менее 8 байт, программа получает начальный адрес и размер читаемого блока, после чего просто копирует запрашиваемые данные в выходной буфер. Обратите внимание: процесс чтения защищен механизмом обработки исключений — на тот случай, если какие-нибудь адреса окажутся недействительными. Функция DrvUnload обрабатывает выгрузку драйвера, а функция DriverEntry является главной точкой входа в драйвер. Чтобы приведенный код правильно откомпилировался в драйвер режима ядра, следует изменить некоторые параметры компилятора и компоновщика, используемые по умолчанию. Например, компилятор должен придерживаться соглашения о вызове stdcall вместо принятого по умолчанию соглашения cdecl. Флаг подсистемы Windows в драйвере должен быть равен «native,4.00» вместо Windows GUI. Эти изменения обеспечиваются включением соответствующих параметров в файл проекта и заголовочный файл kernelopt.h. При правильных настройках драйвер режима ядра будет успешно откомпилирован в Visual C++. Программа Periscope компилируется в крошечную библиотеку DLL режима ядра, Periscope.sys. В дальнейших программах предполагается, что эта DLL скопирована в корневой каталог диска С:. Драйвер написан для систем Windows NT 4.0/Windows 2000 и был в них протестирован. Динамическая загрузка, запуск, остановка и выгрузка драйверов устройств режима ядра хорошо поддерживаются на уровне Win32 API. Для выполнения этих функций мы определили класс KDevice C++. Конструктор KDevice устанавливает соединение с диспетчером управления службами, вызывая функцию OpenSCMManager. Открытая функция KDevice::Load загружает драйвер функцией CreateService, запускает драйвер функцией StartService, после чего получает манипулятор объекта устройства при помощи функции CreateFile. В качестве имени файла при вызове CreateFile указывается строка \\.\Periscope — стандартное обозначение открываемого устройства в Windows. После этого можно вызывать функцию DeviceloControl для полученного манипулятора и общаться с драйвером Periscope, работающем в режиме ядра. KDevice представляет собой обобщенный класс для работы с драйверами устройств Windows NT/2000. С таким же успехом можно воспользоваться им по отношению к другому драйверу. Класс KDevice устроен просто, поэтому мы не будем рассматривать его полный исходный текст и сразу перейдем к небольшой тестовой программе для работы с драйвером Periscope. // TestPeriscope.cpp #define STRICT #inc1ude <windows.h> #inc1ude <winioctl.h> #inc1ude <assert.h> #inc1ude "device.h" #include ". APeriscopeWPeriscope.h" class KPeriscopeClient : public KDevice { public: KPenscopeClient(const TCHAR * DeviceName) : KDevice(DeviceName)
182 Глава 3. Внутренние структуры данных GDI/DirectDraw { } bool Read(void * dst. const void * src, unsigned len); }: bool KPeriscopeClient::Read(void * dst. const void * src. unsigned len) { unsigned cmd[2] - { (unsigned) src. len }; unsigned long dwRead; return IoControl(IOCTL_PERISCOPE. cmd. sizeof(cmd). dst. len. &dwRead) && (dwRead==len): } int WINAPI WinMainCHINSTANCE. HINSTANCE. LPSTR. int) { KPeriscopeClient scope("PeriScope"); if ( scope.Load("c:\\periscope.sys")==ERROR_SUCCESS ) { unsigned char buf[256]; scope.Read(buf. (void*) 0xa000004E. sizeof(buf)); scope.CIose(); MessageBoxCNULL. (char*) buf. "Mem[0xa000004e]". MB_0K); } else MessageBox(NULL, fullname. "Unable to load c:\\periscope.sys". NULL. MB_0K): return 0; } Программа создает класс KPeriscopeClient, производный от KDevice, и включает в него дополнительный метод Read — оболочку для вызова DeviceloControl. Этот метод приказывает Periscope прочитать блок памяти при помощи специального управляющего кода IOCTL_PERISCOPE. Основная программа создает экземпляр KPeriscopeClient в стеке, загружает драйвер режима ядра и читает 256 байт, начиная с адреса 0ха000004е. Адрес принадлежит графическому механизму win32k.sys, обеспечивающему работу gdi32.dll и user32.dll. Базовый адрес win32k.sys равен ОхаООООООО. При чтении по смещению 0х4е от начала модуля Win32 обычно возвращается предупреждение, выводимое в DOS при выполнении Windows-программы: «This program cannot be run in DOS mode $». Если вы впервые работаете с текстом простого драйвера режима ядра Windows NT/2000 и элементарными средствами для работы с драйвером из пользовательского режима, вероятно, у вас возникнет искушение выполнить код в пошаговом режиме и посмотреть, как же все в действительности рзаботает. Вооружитесь отладчиком уровня ядра (таким, как Softlce/W), загрузите необходимые символы или таблицу экспортируемых функций для системных DLL, в по-
WinDbg и расширение отладчика GDI 183 шаговом режиме пройдите от функции WinMain в TestPeriscope.cpp до функции DrvDeviceControl в Periscope.cpp. Вы придете к состоянию стека, приведенному в табл. 3.3. Обратите внимание — между входом в функцию DeviceloControl в kernel32.dll и достижением DrvDeviceControl в Periscope работает системный код Windows, поэтому вам придется пройти через ассемблер. Также следует заметить, что ntdll.dll является библиотекой DLL пользовательского режима, а для переключения процессора в режим адресации ядра вызывается прерывание 2 Eh. Другими словами, прерывание 2 Eh обслуживается кодом режима ядра. Таблица 3.3. Состояние стека при переходе от программы пользовательского режима к драйверу режима ядра Уровень Функция Модуль/файл 1 WinMain TestPeriscope.cpp 2 KPeriscopedient::Read TestPeriscope.cpp 3 KDevice::IoContro1 Device.h 4 DeviceloControl Kernel32.dll 5 NTDeviceloControlFile Ntdll.dll 6 Int 2Eh 7 NTDeviceloControlFile Ntoskrnl.exe 8 Iof Call Driver Ntoskrnl.exe 9 DrvDeviceControl Periscope.cpp WinDbg и расширение отладчика GDI Возможность обращения к данным режима ядра Windows является неплохим базовым средством для начала исследований в области структур данных ядра, однако для этого необходимо знать, где искать информацию и как ее расшифровывать, а для этого потребуется хорошее знание ядра Windows. Самым авторитетным источником информации о ядре Windows остается компания Microsoft. Исходя из этого, мы обратимся к официальному инструментарию Microsoft и попробуем с его помощью разобраться в структурах данных GDI. В поставку Windows Platform SDK и Windows NT/2000 DDK входит мощная графическая утилита для отладки приложений Win32 и драйверов режима ядра Windows NT/2000, дающая помимо всего прочего возможность изучать аварийные дампы и данные «синих экранов». Речь идет о Microsoft Windows System Debugger (WinDbg). Самое приятное то, что эта программа распространяется бесплатно. Существует несколько вариантов применения WinDbg. О Для отладки приложений Win32 на одном компьютере как обычный отладчик пользовательского режима — например, отладчик Microsoft Visual C++.
184 Глава 3. Внутренние структуры данных GDI/DirectDraw В этом режиме вы не сможете войти в код режима ядра и работать с данными режима ядра. О Для удаленной отладки приложений Win32 по аналогии со средствами удаленной отладки в отладчике Visual C++. В этом режиме ведущий и ведомый компьютеры соединяются нуль-кабелем, через модем или по сети. Вы работаете с интерфейсом WinDbg на ведущем компьютере и отлаживаете программу, работающую на ведомом компьютере. При этом для отладчика доступен только пользовательский режим. О Для удаленной отладки кода режима ядра Windows NT/2000 по аналогии со средствами отладки ядра Softlce/W. В этом режиме ведущий и ведомый компьютеры соединяются нуль-кабелем. Ведомый компьютер запускается в специальной конфигурации с включенным режимом отладки ядра. WinDbg запускается на ведущем компьютере и управляет работой программ на ведомом компьютере. В режиме удаленной отладки ядра ведущий компьютер обладает доступом ко всему 4-гигабайтному адресному пространству ведомого компьютера. В области отладки кода режима ядра отладчик Softlce/W намного удобнее, поскольку для него достаточно одного компьютера, а для WinDbg нужен дополнительный ведущий компьютер. Кроме того, Softlce/W позволяет легко переходить из кода пользовательского режима в код режима ядра и обратно. С другой стороны, WinDbg превосходит Softlce/W в некоторых областях просто потому, что это официальная программа, разработанная в Microsoft. Отладчик WinDbg невелик, бесплатно распространяется и поддерживает разные версии Windows NT/2000, тогда как Softlce/W стоит немалых денег и нуждается в частых обновлениях при выходе новых версий Windows NT/2000. Самой замечательной особенностью отладчика WinDbg является его модульная, расширяемая архитектура. Обычный отладчик поддерживает ограниченный набор команд для обращения к данным и программному коду, установки точек прерывания, управления выполнением программы и т. д. WinDbg позволяет включать в отладчик новые команды за счет написания DLL расширения отладчика. Каждая DLL расширения обычно специализируется на конкретной области операционной системы Windows. В поставку WinDbg включены DLL расширения, разработанные компанией Microsoft (табл. 3.4). Таблица 3.4. Расширения отладчика WinDbg от Microsoft Расширение Gdikdx.dll Kdextx86.dll Ntsdexts.dll Rpcexts.dll Userexts.dll Userkdx.dll Vdmexts.dll Функциональность ОС GDI, режим ядра Исполнительная часть/HAL, режим ядра Стандартное расширение пользовательского режима Удаленный вызов процедур (RPC) USER, пользовательский режим USER, режим ядра NT DOS/WOW (Window in Window)
WinDbg и расширение отладчика GDI 185 Интерфейс WinDbg с расширениями отладчика организован очень просто, он полностью определяется в заголовочном файле DDK WDBGEXTS.h. Расширения отладчика должны экспортировать три обязательные функции CheckVersion, ExtensionApi Extension и WinDbgExtensionDllInit, выполняющие проверку версии и инициализацию. Функция CheckVersion убеждается в том, что версия ОС на ведомом компьютере совпадает с версией, для которой написано расширение. Не надейтесь получить правильные результаты при загрузке FREE-версии DLL расширения для отладки в CHECKED-версии ОС. Функция ExtensionApi Version проверяет, используют ли DLL расширения и хост WinDbg одну и ту же версию API. Функция WinDbgExtensionDllInit, самая важная из этих трех функций, передает структуру WINDBGEXTENSIONAPIS от WinDbg к DLL расширения. В настоящее время структура WINDBGEXTENSIONAPIS определяет 11 функций косвенного вызова, которые могут вызываться из DLL расширения. В реализации функций косвенного вызова задействованы DLL imagehlp, отладочные файлы символических имен и ведомая система, подключенная через нуль-кабель. typedef struct _WINDBG_EXTENSION_APIS { ULONG nSize: PWINDBG_OUTPUT_ROUTINE IpOutputRoutine; PWINDBG_GET_EXPRESSION 1pGetExpressi onRoutine; PWINDBG_GET_SYMBOL 1pGetSymbolRouti ne; PWINDBG_DISASM 1pDi sasmRouti ne; PWINDBG_CHECK_CONTROL_C 1pCheckControlCRouti ne; PWINDBG_READ_PROCESS_MEMORY_ROUTINE lpReadProcessMemoryRoutine; PWINDBG_WRITE_PROCESS_MEMORY_ROUTINE lpWriteProcessMemoryRoutine; PWINDBG_GET_THREAD_CONTEXT_ROUTINE lpGetThreadContextRoutine; PWINDBG_SET_THREAD_CONTEXT_ROUTINE IpSetThreadContextRoutine; PWINDBG_IOCTL_ROUTINE IpIoctlRoutine; PWINDBG_STACKTRACE_ROUTINE IpStackTraceRoutine; } WINDBG_EXTENSION_APIS. *PWINDBG_EXTENSION_APIS; Как видно из этого определения, DLL расширения могут обращаться к управляющей программе WinDbg с запросами на вывод строки, вычисление выражения, поиск символического имени, дизассемблирование кода, проверку аварийного завершения, чтение/запись содержимого памяти, чтение/запись контекста потока, вызов функций ввода-вывода и даже трассировку стека. Другими словами, вся информация об устройстве внутренних структур данных операционной системы находится у DLL расширения, a WinDbg обеспечивает интерфейс пользователя с отлаживаемой системой. Помимо трех обязательных функций DLL расширения может экспортировать и другие функции, которые могут использоваться в качестве команд в командной строке WnDbg. Имя экспортируемой функции совпадает с именем команды. Все экспортируемые функции имеют одинаковый прототип, определяемый следующим макросом: fine DECLARE API(s)s CPPMOD VOID с ( HANDLE hCurrentProcess. HANDLE hCurrentThread, ULONG dwCurrentPc. \ \ \ \ \ \
186 Глава 3. Внутренние структуры данных GDI/DirectDraw ULONG PCSTR dwProcessor, args ) Поскольку книга посвящена программированию графики в Windows NT/2000, нас в первую очередь интересует Gdikdx.dll — DLL расширения для отладки GDI в режиме ядра. После настройки WinDbg расширение Gdikdx.dll загружается командой 1 oad в командной строке WinDbg: > load gdikdx.dll Debugger extension library [.. .\system32\gdikdx] loaded Все команды расширений отладчика начинаются с символа !, чтобы их можно было отличить от стандартных команд WinDbg. Команда hel p выводит краткую сводку десятков команд, поддерживаемых расширением отладчика GDI. Как и следовало ожидать от внутреннего отладочного инструмента, для gdikdx.dll эта команда выводит устаревшую информацию. В частности, команды brush, cliserv, gdicall и proxymsg приведены в справке, но в действительности не поддерживаются; команда difi была заменена командой ifi, а новые команды dbli и ddib вообще не упоминаются. К счастью, у каждой команды имеется параметр •?, при помощи которого можно получить обновленную информацию. Обратившись к списку функций, экспортируемых gdikdx.dll, вы найдете имена новых команд, отсутствующие в справке. Команды расширения отладчика для отладки GDI в режиме ядра перечислены в табл. 3.5. Таблица 3.5. Команды расширения отладчика для GDI в режиме ядра Команда Параметры Использование dumphmgr dumpobj dumpdd dumpddobj dh dht ddib dbli ddc dpdev dldev [?] [?] [-P pid] [-1] [-s] object_typ [-P pid] [type] [-?] object handle [-?] object handle [-?] [-1 LPBITMAPINFO] [-w Width] [-h Height] [-f filename] [-b Bits] [-y Byte_Width] [-p palbits palsize] pbits [-?] BLTINFO * [-?adeghrstuvx] hdc [•?abdfghmnprRw] ppdev [■?] C-f] [-F#] ldev Сводка объектов GDI по типам Все объекты GDI заданного типа Объекты диспетчера манипуляторов DirectDraw Объекты DirectDraw заданного типа Запись HMGR для объекта GDI Тип/уникальность/индекс для манипулятора GDI Дамп растра Контекст устройства Объект физического устройства Объект логического устройства
WinDbg и расширение отладчика GDI 187 Команда dgdev dco dpo dppal dpw32 dpbrush dfloat ebrush dpso dblt dr cr dddsurface dddlocal dddglobal dsprite dspritestate rgnlog stats verifier hdc del dca ca mix la ef dteb dpeb Параметры [-?m] dgdevptr [-?] clipobj [-?] pathobj [-?] pal [-?] [process] [-?] pbrush | hbrush [•?] [-1 num] Value [-?] pbrush|hbrush [-?] [-f filename] surfobj [-?] BLTRECORD_PTR [-?] hrgn|prgn [-?] hrgn|prgn [-?haruln]ddsurface C-?ha] [-?ha] C-?ha] C-?ha] [-?] nnn[sl][s2][s3][s4] [-?] [•?hds] [-?gltf] handle [-?] DCLEVEL* [-?] DC_ATTR* [-?]COLORADJUSTMENT* [-?]MATRIX* [-?]LINEATTRS* [•?]address [count] [-?] TEB [-?] [-w] Использование GRAPHICS_DEVICE CLIPOBJ PATHOBJ EPALOBJ HBRUSH или PBRUSH Дамп вещественного числа или массива в формате IEEE HBRUSH или PBRUSH Структура SURFACE из SURFOBJ BLTRECORD REGION Проверка REGION EDDJURFACE EDD_DIRECTDRAW_LOCAL EDD_DIRECTDRAW_GLOBAL SPRITE SPRITE_STATE Последние nnn записей rgnlog Накапливаемая статистика Вывод информации верификатора Вывод структуры данных HDC пользовательского режима Вывод MATRIX из DC_ATTR Вывод команд из очереди ТЕВ Вывод кэшированных объектов РЕВ Продолжение #
188 Глава 3. Внутренние структуры данных GDI/DirectDraw Таблица 3.5. Продолжение Команда Параметры Использование с хо [-?] address [count] [-?] EXFORMOBJ* Шрифтовые расширения tstats gs gdata tm tmwi fo pfe pff pft stro gb gdf gp cache fh hb fv ffv helf ifi pubft pvtft devft dispcache [-?] [1..50] [-?] FDGLYPHSET* [-?] GLYPHDATA *elf [-?] TEXTETRICW* [-?]TMW_INTERNAL* [-?acfhwxy] FONTOBJ* [■?] PFE* [-?] PFF* [-?] PFT* [-?phe] STROBJ* [-?hmg] GLYPHBITS* [-?] GLYPHDEF* [-?] GLYPHPOS* [■?] CACHE* [•?] FONTHASH* [-?] HACHBUCKET* [-?] FILEVIEW* [-?] FONTFILEVIEW* [-?] font handle [-?] IFIMETRICS* [-?] [-?] [-?] [-?] Дамп всех открытых шрифтов Дамп всех закрытых или внедренных шрифтов Дамп всех шрифтов устройств Дамп кэша глифов для вывода структуры PDEV Вероятно, вам не терпится подключить второй компьютер через нуль-модем и опробовать на практике эти потрясающие команды, о существовании которых вы и не подозревали. Автору уже довелось через все это пройти. Даже если вам
WinDbg и расширение отладчика GDI 189 удастся правильно настроить ведущую и ведомую системы, связать их и запустить WinDbg на ведущем компьютере для управления ведомым компьютером, использовать команды расширения GDI непросто. Многие из них работают с манипуляторами объектов GDI или указателями на конкретные структуры данных. Чтобы воспользоваться этими командами, вам предстоит изрядно потрудиться над анализом ведомой системы и получением нужных манипуляторов объектов или указателей. Впрочем, исследования GDI требуют творческого и нетрадиционного подхода. Нельзя ли создать простейшую замену WinDbg, предназначенную не для общей отладки, а для единственной цели — лучшего понимания Windows NT/ 2000 GDI? Для этого нам понадобится простое приложение, управляющее DLL расширения GDI, которое работает на одном компьютере. Попробуйте представить, как команда dumphmgr расширения GDI выводит на ведущем компьютере сводку о таблице объектов GDI для ведомого компьютера. Процесс выглядит примерно так. 1. WinDbg по требованию пользователя загружает gdikdx.dll на ведущем компьютере. 2. Когда пользователь вводит команду ! dumphmgr, WinDbg передает ее функции dumphmgr, экспортируемой gdikdx.dll. 3. Функция dumphmgr библиотеки gdikdx.dll обращается к WinDbg с запросом на получение значения глобальной переменной win32k.sys, содержащей адрес таблицы объектов GDI в адресном пространстве ядра. Задача решается при помощи функций косвенного вызова, переданных Gdikdx.dll от WinDbg. WinDbg средствами IMAGEHLP API получает адрес по символическому имени. Не забывайте: отладочные файлы символических имен для ведомого компьютера должны быть установлены на ведущем компьютере, поэтому WinDbg обладает полным доступом к отладочной информации ведомого компьютера. 4. Gdikdx обращается к WinDbg с запросом на чтение значения переменной, содержащей указатель на таблицу объектов GDI, по адресу переменной в адресном пространстве ведомого компьютера. WinDbg посылает на ведомый ком* пьютер запрос по нуль-модему. Запрос обслуживается ведомым компьютером, работающим в режиме отладки. 5. Gdikdx обращается к WinDbg с запросом на чтение всей таблицы объектов GDI по ее начальному адресу. WinDbg снова передает запрос на ведомый компьютер. 6. Gdikdx обрабатывает полученные данные и обращается к WinDbg с запросом на вывод информации в окне. WinDbg как программа, управляющая работой расширения GDI gdikdx.dll, обеспечивает две основные функции — передачу команд gdikdx.dll и обслуживание функций косвенного вызова. Передача команд gdikdx.dll организуется очень просто; WinDbg передает экспортируемой функции манипуляторы текущего процесса и программного потока, программный счетчик процессора, количество процессов на ведомом процессоре и полную командную строку. Обслуживание функций косвенного вызова на первый взгляд кажется сложной задачей, поскольку существует 11 разных функций косвенного вызова. На самом деле gdikdx.dll
190 Глава 3. Внутренние структуры данных GDI/DirectDraw использует лишь некоторые из них. Больше всего трудностей возникает с функцией для чтения памяти процесса, находящейся в адресном пространстве ядра. К счастью, у вас имеется Periscope — драйвер режима ядра, созданный в предыдущем разделе. Давайте попробуем написать для gdikdx.dll небольшую управляющую программу. Программа Fosterer устроена несложно; это программа с пользовательским интерфейсом, через который разработчик вводит команды. Введенные команды передаются расширению отладчика GDI для выполнения. Когда расширению отладчика требуется декодировать символическое имя или прочитать блок памяти, оно обращается за помощью к Fosterer так, как обратилось бы к WinDbg. В следующем листинге приведено объявление класса KHost, обеспечивающего работу функций косвенного вызова. class KHost { public: KImageModule * pWin32k; KPeriscopeClient * pScope; HWND hwndOutput; HWND hwndLog; HANDLE hProcess: KHostO { pWin32k pScope hwndOutput hwndLog hProcess = NULL - NULL - NULL - NULL = NULL } void WndOutputCHWND hWnd. const char * format, vajist argptr): void Log(const char * format. ...): void ExtOutput(const char * format. ...): unsigned ExtGetExpression(const char * expr); bool ExtCheckControlC(void): bool ExtReadProcessMemory(const void * address, unsigned * buffer, unsigned count, unsigned long * bytesread); }: Класс KHost содержит пять переменных. Указатель pWin32k ссылается на экземпляр класса KImageModule, использующий функции imagehlp.dll для поиска символической информации в отладочных файлах графического механизма Windows win32k.sys. Второй указатель, pScope, ссылается на экземпляр класса KPeri scope, предназначенный для чтения данных из адресного пространства режима ядра. Первый манипулятор окна принадлежит главному текстовому окну, имитирующему окно вывода WinDbg. Второй манипулятор окна предназначен для сохранения дополнительной информации об использовании функций косвенного вызова в gdikdx.dll. Последняя переменная класса, hProcess, содержит манипулятор исследуемого процесса. Первые две функции решают вспомогательные задачи;
WinDbg и расширение отладчика GDI 191 за ними следуют пять функций, соответствующие пяти функциям косвенного вызова, которые мы собираемся реализовать. В следующем листинге приведена реализация функций ExtGetExpression и ExtReadProcessMemory. unsigned KHost::ExtGetExpression(const char * expr) { if ( (expr==NULL) || strlen(expr)==0 ) { assert(false); return 0; if ( (expr[0]>='0') && (expr[0]<='9') ) // Шести, число { DWORD number; sscanf(expr. "%x", & number); return number; } if ( pWin32k ) { const IMAGEHLP_SYMBOL * pis; if ( expr[0]=='&' ) // Пропустить первый & pis = pWin32k->ImageGetSymbol(expr+l); else pis - pWin32k->ImageGetSymbol(expr); if ( pis ) { Log("GetExpressionUs)=U081x\n". expr. pis->Address); return pis->Address; ExtOutput("Unknown GetExpression(""Us"")\n". expr); throw "Unknown Expression"; return 0; bool KHost;;ExtReadProcessMemory(const void * address, unsigned * buffer, unsigned count, unsigned long * bytesread) { if ( pScope ) { ULONG dwRead = 0; if ( (unsigned) address >= 0x80000000 ) dwRead = pScope->Read(buffer, address, count); else ReadProcessMemoryChProcess, address, buffer, count. & dwRead);
192 Глава 3. Внутренние структуры данных GDI/DirectDraw if ( bytesread ) * bytesread = dwRead; if ( (unsigned) address >= 0x80000000 ) Log("ReadKRamU08x. Xd)-\ address, count); else LogCReadURamUx. £08x. *d)=". hProcess. address, count); int len = min(4. count/4); for (int i=0; i<len; i++) Log("U08x ". buffer[i]); Log("\n"); return dwRead == count; } else { assert(false); return false; } } Функция KHost: .-ExtGetExpression получает символическое выражение, представленное символьной строкой, и пытается преобразовать его в числовую величину. Сначала функция пытается по возможности декодировать выражение в шестнадцатеричное число. В WinDbg манипуляторы и адреса обычно задаются именно в шестнадцатеричной системе. Если первая попытка завершается неудачей, функция считает, что ей было передано символическое имя, и вызывает pWin32k->ImageGetSymbol для получения адреса по полученному имени вида win32k!gcMaxHmgr. Указатель pWin32k ссылается на объект KImageModule, в который была предварительно загружена информация о символических именах для файла win32k.sys. Функция KImageModule: :ImageGetSymbol, не приведенная в книге, вызывает функцию SymGetSymFromName для преобразования символического имени в адрес. Интересная подробность: при вызове функция SymGetSymFromName получает указатель на неконстантный указатель на строку, тогда как ExtGetExpression в качестве параметра принимает только константный указатель на строку. Возникает естественное желание — преобразовать константный указатель в неконстантный, обмануть компилятор и добиться своего. Ничего не выйдет; вызов SymGetSymFromName завершится неудачей, и вы получите сообщение об ошибке доступа. Обе стороны настроены серьезно. Функция ExtGetExpression вызывается из библиотеки gdikdx.dll, которая компилируется в Visual C++ с параметром, перемещающим все строки в секцию, доступную только для чтения. Следовательно, строки, передаваемые ExtGetExpression, должны быть доступны только для чтения. Функция SymGetSymFromName ищет символ !, отделяющий имя модуля от имени функции, и заменяет его нуль-символом, чтобы обеспечить правильное завершение имени модуля. В результате для константной строки будет генерироваться ошибка. Проблема решается просто: перед вызовом SymGetSymFromName функция Image- GetSymbol копирует параметр в локальную переменную.
WinDbg и расширение отладчика GDI 193 Функция KHost: :ReadProcessMemory отвечает за чтение блоков памяти. Сначала она убеждается в том, что адрес принадлежит пространству ядра. Если проверка дает положительный результат, функция использует класс KPeriscopeClient (см. предыдущий раздел), который, в свою очередь, использует наш маленький драйвер режима ядра Periscope.sys; в противном случае просто вызывается функция Win32 API ReadProcessMemory с манипулятором процесса. Обратите внимание: при правильно заданном манипуляторе функция ReadProcessMemory позволяет читать содержимое адресного пространства пользовательского режима другого процесса. Однако KHost является классом C++, тогда как API расширения отладчика WinDbg определяется только с использованием средств С. Нам придется немного потрудиться, чтобы состыковать их. Ниже приведена часть оставшегося кода. KHost theHost; void WDBGAPI ExtOutputRoutine(PCSTR format. ...) { vajist ap; va_start(ap. format); theHost.WndOutput(theHost.hwndOutput. format, ap): va_end(ap); } UL0N6 WDBGAPI ExtGetExpression(PCSTR expr) { return theHost.ExtGetExpression(expr); } void WDBGAPI ExtGetSymboKPVOID offset. PUCHAR pchBuffer. PULONG pDisplacement) { throw "GetSymbol not implemented"; } ULONG WDBGAPI ExtReadProcessMemory(ULONG address. PVOID buffer. ULONG count. PULONG bytesread) { return theHost.ExtReadProcessMemory( (const void *)address. (unsigned *) buffer, count, bytesread): } WINDBG_EXTENSION_APIS ExtensionAPI - { sizeof(WINDBG_EXTENSION_APIS). ExtOutputRoutine. ExtGetExpression. ExtGetSymbol.
194 Глава 3. Внутренние структуры данных GDI/DirectDraw ExtDisAsm, ExtCheckControl_C, ExtReadProcessMemory, ExtWri teProcessMemory. ExtGetThreadContext. ExtSetThreadContext. ExtlOCTL, ExtStackTrace }: Для взаимодействия с расширением отладчика необходимо заполнить структуру WINDBG_EXTENSION_APIS информацией об И функциях косвенного вызова. Пять из этих функций отображаются на функции класса KHost через глобальный экземпляр theHost. Остальные функции просто инициируют исключения, которые перехватываются главной программой (если до этого не будут перехвачены в gdikdx.dll). В тексте приведена лишь небольшая часть программы Fosterer, но в целом это вполне стандартная и простая Windows-программа. Главная программа создает несколько дочерних окон; в одном окне вводится манипулятор процесса, в другом — команда. В третьем окне выполняется весь основной вывод. Кроме того, создается дополнительное всплывающее (pop-up) окно для вывода служебной информации. Главная программа отвечает за загрузку драйвера Periscope режима ядра, отладочную информацию win32k.sys, а самое главное — расширение отладчика WinDbg gdikdx.dll. Она инициализирует gdikdx.dll таблицей функций косвенного вызова и проверяет совместимость текущей версии ОС с версией ОС gdikdx.dll. У расширения отладчика GDI имеется очень интересная команда dumphmgr, с которой мы и начнем. Эта команда должна выводить общие сведения о манипуляторах GDI — то есть ту самую таблицу объектов GDI, за которой мы так долго охотились в этой главе. Если все было настроено правильно, введите в окне команды строку dumphmgr, щелкните на кнопке Do, закройте глаза и попытайтесь угадать, что вы сейчас увидите. Ура! Работает! Нам удалось успешно использовать gdikdx.dll без WinDbg, всего на одном компьютере, без запуска ОС в отладочном режиме, без нуль-модема — и мы получили сводку содержимого таблицы объектов GDI из адресного пространства ядра! Причем для работы программы Fosterer нам совершенно ничего не нужно знать о таблице объектов GDI, поскольку всей необходимой информацией владеет расширение отладчика GDI. Окно программы Fosterer изображено на рис. 3.7. В небольшом поле слева выводится идентификатор процесса; наверху справа находится поле ввода команды. Команда передается расширению отладчика GDI при щелчке на кнопке Do. В главном окне отображаются результаты работы самой программы и расширения отладчика GDI. В нескольких начальных строках выводится статус загрузки драйвера режима ядра Periscope, файла отладочной информации для графического механизма и расширения отладчика GDI. Расширения отладчика строятся вместе с ОС Windows, поэтому им присваивается тот же номер сборки. Программа убеждается в том, что номер сборки расширения отладчика совпадает с аналогичным номером ОС, и если номера различаются — выводит предупреждение. Точные совпадения встречаются редко, но вы должны постараться, чтобы эти номера были как можно ближе друг к другу.
Структуры данных режима ядра 195 НПЕЗЗжИ Рйе нф Command j20c I DO Urn Miiiiinl jPeriscope loaded JD:\WINNT50^ (Windows OS V osymbolsxsys^ v5.0, bui Id J"D:\WINMT50\System32\gd |««* Extension DLL(2013 jdumphmgr jMax handles jTotal Hmgr: |ulLoop-113C | TYPE JDEF TYPE !DC TYPE ;RGN TYPE JSURF TYPE iCLIOBJ TYPE PAL TYPE 1ICMLCS TYPE JLFONT TYPE Ipfe type |brush type Jtotals jcUnused obj out so f Reserved ar iumphmgr ,.\win32k. 2031 ikdx Free) 1130 memory dll" dbg loaded. loaded. does not match target ШШШВ№&'*:' ' system(2031 Free) 2097152 Committed 36864 gcMaxHmgr-1130 handl current 132, 102, 37, 492, 3, 34, : i. 95, 102, 132, 998, ects 132 ijcUnknown objects 0 0 0 0 0 0 0 0 0 0 0 0 0 - - - - - - - - - - - es, (objects) maximum allocated 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 - 0, 0 0 0 0 0 0 0 0 0 0 0 LookAside LAB Cur 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 0 - 0 ' M*\\ 1 LAB Max 0 1 о 3 о % 0 $ 0 | о % 0 0 1 о ;| о 3 0 Рис. З.7. Управление расширением отладчика WinDbg в программе Fosterer После статусной информации выводятся результаты выполнения команды dumphmgr. Из них видно, что диспетчер манипуляторов GDI (фрагмент кода, отвечающий за работу с таблицей объектов GDI) зарезервировал в адресном пространстве ядра целых 2 мегабайта, но из них актуализировано только 36 Кбайт. Из максимального количества манипуляторов GDI (16 384) с момента последней перезагрузки компьютера одновременно задействовалось не более ИЗО. В момент обработки команды dumphmgr использовались только 998 объектов GDI, а остальные 132 манипулятора относились к созданным, а затем удаленным объектам. В сводке объектов GDI выводится количество контекстов устройств, растров, палитр, логических шрифтов, кистей и т. д., фактически присутствующих в системе. Впрочем, этот пример дает лишь поверхностное представление о том, что можно сделать при помощи расширения отладчика GDI. Этот инструмент играет важнейшую роль в процессе анализа внутренних структур данных GDI. Структуры данных режима ядра Вооружившись драйвером устройства режима ядра Periscope, расширением отладчика WinDbg gdikdx.dll и простейшей программой для управления расширением отладчика Fosterer, можно, наконец, переходить к исследованию недокументированных структур данных GDI в режиме ядра Windows NT/2000.
196 Глава 3. Внутренние структуры данных GDI/DirectDraw Таблица объектов GDI в механизме GDI Как было показано в предыдущих разделах, каждый процесс Win32 работает с глобальной таблицей объектов GDI. В пользовательских процессах таблица доступна только для чтения и в разных процессах она отображается на разные адреса. В GDI существует недокументированная функция GdiQueryTable, возвращающая адрес таблицы объектов GDI для текущего пользовательского процесса. В действительности таблица объектов GDI находится под управлением графического механизма GDI Win32k.sys. Она доступна для чтения и записи с фиксированного адреса в адресном пространстве ядра. Таблица объектов GDI отображается в пользовательское адресное пространство каждого процесса, работающего со средствами GDI. Программный код, управляющий таблицей объектов GDI, называется диспетчером манипуляторов (Handle ManaGeR); вот почему при исследовании внутреннего строения GDI так часто встречается сокращение hmgr. В соответствии со служебными данными, полученными в программе Fosterer, win32k.sys поддерживает глобальную переменную с именем gpentHmgr, указывающую на начало таблицы объектов GDI в адресном пространстве ядра. Максимальное количество объектов GDI, на которое рассчитана таблица объектов GDI, равно 16 384. Обычно таблица используется лишь частично, поэтому многие элементы таблицы остаются пустыми. В Win32k.sys поддерживается еще одна глобальная переменная gcMaxHmgr, в которой хранится максимальный индекс задействованного элемента таблицы. GdiTableCell * gpentHmgr; unsigned long gcMaxHmgr; Элемент таблицы объектов GDI представляет собой 16-разрядную структуру, которую мы назвали GdiTableCell (см. раздел «Расшифровка таблицы объектов GDI»). Типы объектов GDI в механизме GDI Все мы хорошо знакомы с такими объектами GDI, как перья, кисти, шрифты, регионы, палитры и т. д. Однако в таблице объектов GDI присутствует немало других разновидностей объектов, не встречающихся на уровне Win32 API. В табл. 3.6 перечислены типы объектов GDI, полученные по команде dumphmgr. Таблица 3.6. Типы объектов GDI Тип Идентификатор типа Описание Удаленные объекты GDI Контекст устройства, метафайл Объект DirectDraw (теперь обрабатывается отдельно) Поверхность DirectDraw (теперь обрабатывается отдельно) Регион DEFJYPE DCJYPE DD_DRAW_TYPE DD_SURF_TYPE RGN TYPE 0x00 0x01, 0x21 0x02 0x03 0x04
Структуры данных режима ядра 197 Тип SURFJYPE CLIOBJ_TYPE PATH_TYPE PAL_TYPE ICMCSJYPE LFONTTYPE RFONTJYPE PFEJYPE PFTJYPE ICMCXFJYPE ICMDLL_TYPE BRUSH_TYPE D3D_HANDLE_TYPE DD_VPORT_TYPE SPACE_TYPE DD_MOTION_TYPE METAJYPE EFSTATEJYPE BMFD_TYPE VTFDJYPE TTFDJYPE RC_TYPE TEMPJYPE DRVOBJ_TYPE DCIOBJJYPE SPOOL_TYPE Идентификатор типа 0x05 0x06 0x07 0x08 0x09 0x0a OxOb OxOc OxOd OxOe OxOf 0x10, 0x30 Oxll 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 Oxla Oxlb Oxlc Oxld Oxle Описание Аппаратнозависимый растр Клиентский объект Траектория Палитра Логический шрифт Кисть, перо Из более чем 30 типов объектов, перечисленных в табл. 3.6, программистам Win32 известны лишь некоторые — например, объекты DCJTYPE, BRUSHJTYPE и LFONTTYPE, соответствующие контексту устройства, кисти/перу и логическому шрифту. Интересный факт: кисти и перья относятся к одному типу BRUSHJTYPE, хотя их идентификаторы типов несколько отличаются. Win32 API не содержит
198 Глава 3. Внутренние структуры данных GDI/DirectDraw функций для непосредственного создания объектов траекторий (PATHJTYPE), хотя логика подсказывает, что какой-то объект в памяти все же создается. Построение траектории начинается с вызова функции BeginPath. При помощи расширения отладчика GDI мы исследуем структуры данных ядра, создаваемые для объектов GDI. Контекст устройства в механизме GDI Контекст устройства является одним из основных объектов GDI. Его многочисленные атрибуты определяют различные аспекты взаимодействия Win32 API с графическим устройством, будь то видеоадаптер, принтер, плоттер или фотонаборная машина. GDI хранит данные контекста устройства в двух местах. Структура пользовательского режима DC_ATTR содержит такие атрибуты, как текущее перо, текущая кисть, цвета фона и текста. Определение структуры DC_ATTR приведено в разделе «Отслеживание СОМ-интерфейсов DirectDraw» главы 4. Механизм GDI также поддерживает структуру DC0BJ в адресном пространстве ядра; эта структура содержит полную информацию об объекте контекста устройства, включая копию DC_ATTR. Для манипулятора контекста устройства поле pKernel элемента таблицы объектов GDI ссылается на экземпляр DC0BJ, а поле pUser — на экземпляр DC_ATTR. Расширение отладчика GDI поддерживает несколько команд, предназначенных для расшифровки структуры данных, соответствующих манипуляторам устройств. Команда ddc расшифровывает HDC и выводит в основном содержимое DC0BJ; команда del выводит содержимое структуры DCLEVEL со структурой DC0BJ; команда dca выводит содержимое структуры DC_ATTR, присутствующей как в адресном пространстве ядра, так и в пользовательском адресном пространстве. Ниже показано то, что мы знаем об этих структурах. // // ty г 1 dcobj.h Windows 2000, pedef struct HPALETTE void * void * unsigned unsigned unsigned HGDIOBJ unsigned void * void * void * HGDIOBJ unsigned LINEATTRS void * void * 440(0xlB8) байт hpal; ppal; pColorSpace: HcmMode; ISaveDepth; unklJOOOOOOO; hdcSave; unk2 00000000[2]; pbrFill: pbrLine; unk3_ela28d88; hpath; // flPath; // lapath; // prgnClip; prgnMeta; COLORADJUSTMENT ca; // unsigned flFontState; HPATH PathFlags 0x20 байт 0x18 байт
Структуры данных режима ядра 199 unsigned unsigned unsigned unsigned MATRIX MATRIX FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ void * SIZE } DCLEVEL; // Windows 2000. typedef struct { HGDIOBJ void * ULONG ULONG DHPDEV unsigned unsigned void * void * unsigned unsigned unsigned DCLEVEL DC_ATTR unsigned unsigned RECTL unsigned RECTL RECTL unsigned void * void * void * POINT unsigned void * unsigned void * unsigned void * unsigned void * ufi: unk4 00000000C12]: Л; flbrush; mxWorldToDevice; mxDeviceToWorld; efMllPtoD; efM22PtoD: efDxPtoD; efDyPtoD: efMll TWIPS; efM22 TWIPS; efPrll; efPr22; pSurface; sizl; 1548(0x600 байт hHmgr; pEntry; cExcLock; Tid; dhpdev; dctype; fs; ppdev; hsem: f1 Graphics; flGraphics2; pdcattr; dcLevel: dcAttr; hdcNext; hdcPrev; erclClip; // 000 // 004 // 008 // 00c // 0x010 // Флаги // 0x020 // Указатель на DC ATTR // 0x030 0xlB8(440) байт // 0xlC8(456) байт // ОхЗВО unk4_00000000[2]; erclWindow; erclBounds; unk5_00000000[4]; prgnAPI; prgnVis; prgnRao: FillOrigin; unk6_00000000[10]; peal; // Указатель на DCLEVEL.ca unk7_00000000[20]; pca2; unk8_00000000[20]; рсаЗ; unk9_00000000[20]; pca4;
200 Глава 3. Внутренние структуры данных GDI/DirectDraw unsigned HFONT unsigned void * unsigned unsigned unsigned unsigned unka 00000000C10]: hlfntCur; unkb_00000000[2]: prfnt: unkc 00000000[33]: unkd OOOOffff; unke ffffffff; unkf 00000000C3]; } DCOBJ; В последний раз подробные описания структур вроде DCOBJ встречались в книге Шульмана (Schulman), Макси (Махеу) и Питрека (Pietrek) «Undocumented Windows», опубликованной в 1992 году. Эта книга помогла нам разобраться в некоторых полях, унаследованных от Windows 3.0/3.1, — как расшифрованных, так и не расшифрованных командами расширения отладчика. Для каждого объекта GDI данные режима ядра начинаются с 16-байтовой структуры. В первом поле хранится манипулятор GDI объекта; второе поле содержит неизвестный указатель; третье поле — счетчик блокировок, а последнее поле — идентификатор программного потока, создавшего объект. По манипулятору механизм GDI может обратиться к таблице объектов GDI и определить, какому процессу принадлежит манипулятор, а также получить доступ к структурам пользовательского режима (таким, как DCATTR). Первое поле после заголовка, dhpdev, содержит манипулятор структуры PDEV, находящейся под управлением драйвера графического устройства. Драйвер графического устройства должен обеспечивать управление несколькими физическими устройствами. Для этого драйвер устройства определяет структуру данных, необходимую для управления этими устройствами. В документации Windows DDI эти структуры упоминаются под именем PDEV; они определяются и используются только драйвером устройства. Чтобы драйвер создал и инициализировал структуру PDEV, механизм GDI вызывает функцию драйвера DrvEnablePDEV. Поскольку структура PDEV управляется исключительно драйвером устройства, механизм GDI не интересуют подробности ее строения, поэтому DDI (интерфейс драйвера устройства) позволяет DrvEnablePDEV вернуть манипулятор PDEV вместо указателя на PDEV. Механизм GDI действует честно — он позволяет разработчику драйвера скрыть реализацию за манипулятором по аналогии с тем, как сам механизм GDI скрывает свою реализацию за манипуляторами GDI. Манипулятор, полученный при вызове DrvEnablePDEV, используется механизмом GDI при последующих обращениях к физическому устройству для создания графической поверхности. Чтобы освободить память и ресурсы, занимаемые физическим устройством, механизм GDI вызывает функцию DrvDisablePDEV. В Windows NT/2000 DDK включены примеры исходных текстов нескольких драйверов экрана, причем все они используют разные структуры PDEV. Функция DrvEnablePDEV, как правило, возвращает в качестве манипулятора обычный указатель на PDEV. Как мы знаем из Win32 API, существует несколько разных типов контекстов устройств. В структуре DCOBJ эти различия обозначаются в поле dctype. В настоящее время выделяются три типа контекстов: typedef enum {
Структуры данных режима ядра 201 DCTYPEJ3IRECT =0. // обычный контекст устройства DCTYPEJ1EM0RY =1, // совместимый контекст DCTYPE__INF0 =2 // информационный контекст }: Поле fs структуры DC0BJ содержит флаги, относящиеся к контексту устройства. Ниже перечислены некоторые из флагов, выводимых расширением GDI. typedef enum { DC DISPLAY DC DIRECT DC CANCELED DC PERMANENT DC DIRTY RAO DC ACCUM WMGR DC ACCUM APP DC RESET DC SYNCHRONIZEACCESS DC EPSPRINTINGESCAPE DC TEMPINFODC DC FULLSCREEN DC IN CLONEPDEV DC REDIRECTION - 0x0001. = 0x0002. - 0x0004. = 0x0008. = 0x0010. - 0x0020. - 0x0040. - 0x0080. = 0x0100. - 0x0200. - 0x0400. • 0x0800. - 0x1000. = 0x2000 } DCFLA6S; Следующее поле DCOBJ выводится расширением GDI под именем ppdev. Вполне естественно предположить, что это сокращение означает «Pointer to Physical DEVice», то есть «указатель на физическое устройство». В расширении GDI даже предусмотрена команда dpdev для расшифровки указателя на PDEV. Но согласно DDK, структура данных физического устройства находится под управлением драйвера устройства, и механизму GDI о ней знать ничего не положено. Функция драйвера DrvEnablePDEV возвращает манипулятор физического устройства вместо указателя на него. Одно из возможных объяснений заключается в том, что механизм GDI создает для физического устройства свою собственную структуру данных, которую мы назовем PDEVWIN32K, чтобы избежать путаницы со структурой PDEV драйвера. Структура PDEVWIN32K устроена чрезвычайно сложно. Мы поближе познакомимся с ней в следующем подразделе. Поле hsem ссылается на структуру семафора. Очевидно, семафор предназначен для синхронизации обращений к полям. В полях flGraphics и flGraphics2 хранятся флаги возможностей устройства. Состав этих флагов документируется в DDK; к их числу принадлежат флаги GCAPS_ALTERNATEFILL, GCAPS_WINDINGFILL, GCAPS JX)LORJ)ITHER и т. д. Флаги flGraphics2 и flGraphics2 берутся из структуры DEVINF0, заполняемой функцией DrvEnablePDEV драйвера устройства. Поле pdcattr ссылается на структуру DCATTR данного контекста устройства в адресном пространстве пользовательского режима, содержащую большую часть атрибутов контекста. Структура DC0BJ содержит копию этой структуры в поле dcAttr. Вероятно, разработчики GDI хотели оптимизировать процесс присваивания значений атрибутам DC, сведя к минимуму использование кода режима ядра; для этого структура DCATTR должна размещаться в адресном пространстве пользовательского режима. Однако разработчики также хотели упростить дос-
202 Глава 3. Внутренние структуры данных GDI/DirectDraw туп к атрибутам в режиме ядра, для чего копия DC_ATTR должна находиться и в режиме ядра. Синхронизация двух копий DC_ATTR осуществляется с помощью специальных флагов. В процессе анализа структуры DCATTR выяснилось, что выполнение некоторых функций с манипулятором контекста устройства (например, выбор HBITMAP в совместимом контексте устройства или выбор палитры в DC) никак не влияет на содержимое таблицы объектов. Если вас интересует, эти атрибуты хранятся в структуре DCLEVEL, содержащейся в DC0BJ. Структура DCLEVEL содержит информацию о палитре, цветовой глубине, регулировке цвета, атрибутах линий, области отсечения, преобразованиях, траекториях и т. д. Структура PDEV в механизме GDI Все графические драйверы поддерживают базовую точку входа DrvEnableDriver. При загрузке драйвера механизм GDI вызывает функцию DrvEnableDriver, которая заполняет структуру DRVENABLEDATA. DrvEnableDriver передает механизму GDI таблицу реализованных функций, тем самым сообщая ему, какие функции поддерживаются драйвером. В DirectDraw также создаются некоторые таблицы функций косвенного вызова. Конечно, механизм GDI должен хранить полученную информацию, относящуюся к конкретному драйверу, в некоторой структуре данных. Ниже приведено описание структуры PDEV механизма GDI. // Windows 20000 3304 (0хСЕ8) байт typedef struct { unsigned void * int int void * unsigned unsigned void * void * POINT. unsigned SPRITESTATE HFONT HF0NT HFONT HGDIOBJ unsigned void * void * unsigned unsigned void * void * void * void * header[4]; ppdevNext; cPdevRefs; cPdevOpenRefs; ppdevParent; flags; fl Accelerated; hsemDevLock; hsemPointer; ptlPointer; unk_0038[2]; SpriteState; hlfntDefault; hifntAnsiVariable; hifntAnsiFixed; ahsurf[6]; unk_0240[2]: prfntActive; prfntlnactive; clnactive; unk_0254[27]; pfnDrvSetPoi nterShape pfnDrvMovePointer; pfnMovePointer; pfnSync; // 0010 // 0014 // 0018 // 001c // 0020 // 0024 // 0028 // 002c // 0030 // 0038 // 0040. 476(ldc) байт // 021c // 0220 // 0224 // 0228 // 0240 // 0248 // 024c // 0250 // 0254 // 02c0 // 02c4 // 02c8 // 02cc
Структуры данных режима ядра 203 unsigned void * unsigned void * DHPDEV void * DEVINFO GDI INFO void * void * unsigned unsigned unk 02d0; pfnDrvSetPalette; unk_02d8[2]; pldev; dhpdev; ppalSurf; devinfo; gdiinfo; pSurface; hSpooler; pDesktopId: unk 0054; EDD_DIRECTDRAW_GLOBAL eDirectOrawGlobal; void * POINT DEVMODEW * unsigned void * PDEV WIN32K; pGraphicsDevice; ptlOrigin; pdevmode; unk 0b78[3]; apfn[89]; // // // // // // // // // // // // // // // // // // 02d0 02d4 02d8 02e0 02e4 02e8 02ec-417 0418-547 0548 054c 0550 0554 1552 (Oxi 0b68 0b6c 0b74 0b78 0b84 Структура PDEV_WIN32K имеет довольно большой размер (3304 байта) и содержит большое количество информации, относящейся к драйверу устройства. PDEVWIN32K в первую очередь используется механизмом GDI при обращениях к драйверу графического устройства для выполнения различных запросов пользователя. Структура начинается с неизвестного заголовка, состоящего из 16 байт. Разные структуры PDEVWIN32K, существующие в системе, объединяются в иерархическое дерево. Поле ppdevNext содержит ссылку на следующую структуру, а поле ppdevParent указывает на родительскую структуру. В расширении отладчика GDI поддерживается команда dpdev для расшифровки структуры PDEV_ WIN32K. У этой команды имеется параметр -R, предназначенный для рекурсивного вывода всех структур, на которые ссылается родительская структура. Если воспользоваться параметром -R для структуры PDEV_WIN32K, соответствующей экранному контексту устройства, вы увидите, что поле ppdevNext связывается со структурами PDEVWIN32K нескольких шрифтовых драйверов. В Windows GDI существует несколько типов графических драйверов, каждый из которых обладает специфическими особенностями. Классификация драйверов осуществляется на основании поля флагов flags. В табл. 3.7 перечислены некоторые флаги, поддерживаемые расширением GDI. Таблица 3.7. Флаги структуры PDEV_WIN32K Флаг Интерпретация PDEVDISPLAY Экранный вывод PDE VHARDWAREPOINTER Аппаратная поддержка курсора PDEV_G0TF0NTS Наличие шрифтового драйвера PDEV_DRIVER_PUNTED_CALL Драйвер возвращает запросы механизму GDI PDEVFONTDRIVER Шрифтовой драйвер
204 Глава 3. Внутренние структуры данных GDI/DirectDraw Следующие несколько полей предназначены для управления курсором мыши в драйверах экрана. Поле hsemPointer представляет собой семафор, синхронизирующий операции с курсором мыши. Драйвер устройства обеспечивает несколько функций косвенного вызова для вывода курсора мыши; адреса этих функций хранятся в полях pfnDrvSetPointerShape и pfnDrvMovePointer. В системе автора эти поля ссылаются на mga64!DrvSetPointerShape и mga64!DrvMovePointer. Структуры SPRITESTATE и DIRECTDRAWGLOBAL, внедренные в PDEVWIN32K, относятся к реализации DirectDraw. Мы рассмотрим эти структуры в следующем разделе. В PDEVWIN32K хранятся манипуляторы трех шрифтов. В системе автора поле hlfntDefault ссылается на гарнитуру «System», поле hifntAnsiVariable — на гарнитуру «MS Sans Serif», а поле hi fntAnsi Fixed — на гарнитуру «Courier». Хотя Windows GDI старается соответствовать принципу WYSIWYG, со штриховыми кистями возникают проблемы. В GDI штриховая кисть определяется монохромным растром размером 8 х 8. На экранах с разрешением от 72 dpi до 120 dpi горизонтальные, вертикальные, диагональные или решетчатые узоры выглядят вполне нормально. Однако на принтерах с разрешением от 180 до 2400 dpi и даже выше узор из растров, определяемых матрицами 8x8 пикселов, превращается в сплошную серую рябь. Чтобы штриховые кисти были лучше видны на устройствах высокого разрешения, механизм GDI позволяет драйверу устройства передать свои собственные растры для реализации шести стандартных типов штриховых кистей Windows GDI. Функция EnablePDEV драйвера устройства может передать массив из шести указателей на поверхности (растры), а манипуляторы соответствующих объектов GDI сохраняются в массиве ahsurf. Хотя драйверам экрана рекомендуется передавать эти манипуляторы, в структуру PDEVWIN32K экранного DC включается шесть манипуляторов стандартных растров 8x8, передаваемых GDI по умолчанию. Структуры GDI обычно существуют в виде пар; одна структура относится к логическому описанию, а другая — к физической реализации. Следовательно, для структуры физического устройства PDEVWIN32K следует поискать парную структуру с логическим описанием. Указатель йа такую структуру хранится в поле pldev, а сама структура расшифровывается командой расширения GDI dldev. Структуры LDEVWIN32K образуют двусвязный список. Начиная с экранного контекста устройства, список открывается драйвером экрана (например, «\SystemRoot \System32\mga64.dll»), за которым следует несколько шрифтовых драйверов. Последовательность завершается шрифтовым драйвером ATM («\SystemRoot\ System32\atmfd.dll»). // Windows 2000, 384 (0x180) байт typedef struct { LDEV_WIN32K * nextldev; LDEV_WIN32K * prevldev; UL0NG Tevtype; UL0NG cRefs; UL0NG unkJIO; void * pGdiFriverlnfo; UL0NG ulDriverVersion; PFN apfn[89]: } LDEV_WIN32K;
Структуры данных режима ядра 205 По данным протокола, сгенерированного программой Fosterer, для получения первой логической структуры графического устройства расширение отладчика GDI читает значение глобальной переменной win32k!gpldevDrivers. Структура PDEVWIN32K содержит копию структуры DEVINF0, заполняемой точкой входа DrvEnablePDEV графического драйвера. Структура DEVINF0 документирована в DDK. В основном она описывает возможности драйвера устройства по обработке кривых, шрифтов, графических форматов, работе с цветом и т. д. Еще структура PDEVWIN32K содержит копию структуры GDI INFO, также заполняемой точкой входа DrvEnablePDEV и документированной в DDK. Структура GDI INFO в основном содержит информацию о размере, формате и разрешении графической поверхности. Многие поля структуры GDI INFO можно получить при помощи функции Win32 API GetDeviceCaps. Например, вызов GetDeviceCaps(hDC, TECHNOLOGY) связан с полем ulTechnology структуры GDIINF0, а значение GetDeviceCaps(hDC, RASTER- CAPS) берется из поля flRaster структуры GDIINF0. Поле pSurface ссылается на структуру поверхности SURFACE, где фактически выполняются все графические операции. Структура поверхности рассматривается ниже в этом разделе. Поле pdevmode ссылается на структуру DEVM0DEW, Unicode-версию DEVMODE. Структура DEVMODE обычно инициализируется графическим драйвером и модифицируется пользовательским приложением, что позволяет не только получать информацию от драйвера устройства, но и менять значения параметров, настройка которых допускается драйвером. При выводе на экран структура DEVMODE особой пользы не приносит, однако драйвер принтера получает из нее важную информацию о качестве печати, размере бумаги, типе носителя, разрешении и т. д. Последнее и самое важное поле структуры PDEVWIN32K, apfn, представляет собой таблицу из 89 указателей на функции. В Windows 2000 интерфейс DDI определяет 89 функций, которые могут реализовываться драйвером графического устройства; каждой функции соответствует заранее определенный индекс. Например, индекс INDEX DrvEnablePDEV равен 0, а индекс INDEX_DrvSynchronizeSurface равен 88. Одни из этих 89 индексов не используются, другие зарезервированы, третьи предназначены только для драйверов экрана, а четвертые — только для драйверов принтеров. Лишь небольшая часть этих функций обязательно должна реализовываться драйверами устройств; остальные функции необязательны. При загрузке драйвера устройства система вызывает функцию DrvEnableDriver, которая заполняет структуру DRVENABLEDATA. В сущности, структура DRVENABLEDATA представляет собой сжатую таблицу с 89 указателями на функции. Присваивание значений 89 указателям на функции — утомительная работа, чреватая ошибками и плохо расширяемая. По этой причине механизм GDI позволяет драйверу передать список поддерживаемых им функций вместе с индексами, по которому механизм GDI строит расширенную таблицу. Таблица функций хранится в двух местах. Структура логического устройства LDEVWIN32K содержит исходную таблицу функций, сконструированную по данным DRVENABLEDATA и полученную от драйвера устройства. Структура физического устройства PDEVWIN32K содержит таблицу, фактически используемую механизмом GDI; эта таблица содержит функции из LDEVWIN32K, а также точки входа механизма GDI для реализации функций, не поддерживаемых драйвером устройства. Например, если драйвер устройства не поддерживает DrvBitBlt, он фактически обращается к механизму GDI с
206 Глава 3. Внутренние структуры данных GDI/DirectDraw просьбой предоставить реализацию этой функции. По этой причине таблица функций PDEVJJIN32K вместо указателя NULL содержит указатель на функцию win32k.sys — Win32ISpBitBlt. В табл. 3.8 приведено содержимое таблицы функций DDI на компьютере автора. Таблица 3.8. Пример таблицы функций PDEV_WIN32K Индекс 00 01 02 03 04 05 06 07 10 11 12 13 14 15 17 18 19 20 22 23 24 29 30 31 40 Адрес fdd691f0 fdd69310 fdd69350 fdd693b0 fdd69640 fdd69760 fdd69710 fdd68fa0 fdd4fc00 fdd4fd00 fdd50fb0 fdd50cc0 a00a8fe3 aOOaaafc fdd68890 aOO1834b a001d26d a0064500 fdd68cl0 a001d72e fdd70fcc fdd5del0 fdd5df90 аООаабЗЬ a003c327 Имя функции mga64!DrvEnablePDEV mga64!DrvCompletePDEV mga64!DrvDisablePDEV mga64!DrvEnableSurface mga64!DrvDisablePDEV mga64!DrvAssertMode mga64!Drv0ffset mga64!DrvResetPDEV mga64!DrvCreateDevi ceBi tmap mga64!DrvDeleteDevi ceBi tmap mga64!DrvReali zeBrush mga64!DrvDi therColor win32k!SpStrokePath win32k!SpFil!Path mga64!DrvPaint win32kISpBitBlt win32k!SpCopyBits win32k!SpStretchBlt mga64!DrvSetPalette win32k!SpText0ut mga64!DrvEscape mga64!DevSetPoi nterShape mga64!DrvMovePoi nter win32k!SpLineTo wi n32k!SpSaveScreenBlt
Структуры данных режима ядра 207 Индекс 41 43 59 60 61 67 68 69 70 71 74 Адрес fdd69950 fdd5bf60 fdd6efl0 fdd6fl90 fdda0410 fdd68dc0 a0036184 a0073180 a010dl93 aOlOdOfl a010d049 Имя функции mga64!DrvGetModes mga64!DrvDestroyFont mga64!DrvGetDi rectDrawInfо mga64!DrvEnableDi rectDraw mga64!DrvDi sableDi rectDraw mga64!DrvIcmSetDevi ceGammRamp win32k!SpGradientFill win32k!SpStretchBUR0P win32k!SpPlgBlt win32k!SpALphaBlend wi n32k!SpTransparentBlt Как видно из таблицы, в случае с драйвером экрана механизм GDI выполняет основную работу по выводу кривых, заливок, текста и растров, а драйвер экрана выполняет инициализацию, операции с курсором мыши, реализацию объектов и т. д. Поверхности в механизме GDI На уровне механизма GDI графические функции работают с поверхностями, связанными с драйвером устройства, на котором осуществляется вывод. При работе с поверхностями устройств используется координатная система, напоминающая режим отображения ММТЕХТ GDI API. Пикселы поверхности адресуются парами 28-разрядных целых чисел со знаком; в левом верхнем углу расположено начало координат — точка (0, 0). Поверхность устройства лежит в правом нижнем квадранте этой системы координат, а обе координаты принимают только неотрицательные значения. Хотя координаты в Win32 API хранятся и передаются в виде 32-разрядных целых чисел со знаком, в некоторых графических операциях механизм GDI использует младшие 4 бита 32-разрядного целого для представления дополнительных координат (субпикселов), повышающих точность вычислений. В механизме GDI определены два основных типа поверхностей. Поверхности первого типа, управляемые механизмом GDI, в документации DDK обычно именуются аппаратно-независимыми растрами (Device-Independent Bitmap, DIB). Поверхности, управляемые GDI, состоят из одной цветовой плоскости с упакованными пикселами и выравниванием строк развертки по границам двойных слов. Если драйвер устройства работает с поверхностью, управляемой графическим механизмом, весь вывод на этой поверхности может быть выполнен средствами GDI. Поддержка со стороны механизма GDI в несложных драйверах экрана или драйверах растровых принтеров заметно упрощает сами драйверы
208 Глава 3. Внутренние структуры данных GDI/DirectDraw и их сопровождение. Пример драйвера кадрового буфера, входящий в Windows 2000 DDK, создает поверхность, управляемую графическим механизмом, в качестве основной поверхности и поручает выполнение вывода GDI. Драйвер принтера UniDrv в Windows 2000 также использует поверхности, управляемые графическим механизмом, после деления физической страницы на серию прямоугольных полос. Ко второму типу относятся поверхности, управляемые устройством; то есть драйверам устройств дозволяется организовать самостоятельное управление своими поверхностями. Во внутреннем представлении формат поверхности, управляемой устройством, может совпадать с форматом поверхности, управляемой графическим механизмом, или отличаться от него. Если форматы совпадают, драйвер устройства все равно может выполнять графические операции средствами GDI. Растры в формате устройства (device-format bitmap) составляют особую категорию специализированных форматов поверхностей, управляемых устройствами. Данная возможность поддерживается для того, чтобы некоторые драйверы экрана могли реализовать ускоренное копирование растров на экран. Кроме того, это позволяет драйверам осуществлять вывод в видеопамяти, поделенной на банки, или работать с растрами в нестандартных форматах. Главной структурой данных, предназначенной для представления различных поверхностей GDI, является структура SURF0BJ. Структура SURF0BJ документирована в Windows NT/2000 DDK. Она занимает одно из центральных мест в интерфейсе DDI и используется для представления как растров, так и графических поверхностей. Поскольку структура SURF0BJ очень важна для работы механизма GDI, ниже приведено ее определение, позаимствованное из документации DDK. typedef struct JURFOBJ { DHSURF dhsurf; HSURF hsurf; DHPDEV dhpdev; HDEV hdev; SIZEL sizlBitmap; UL0NG cjBits; PV0ID pvBits: PVOID pvScanO; LONG 1 Delta; UL0NG iUniq; UL0NG iBitmapFormat; USH0RT iType; USH0RT fjBitmap; } SURFOBJ; В первом поле, dhsurf, хранится манипулятор, предназначенный для идентификации поверхностей, управляемых устройством; он может представлять собой указатель, индекс или любое другое значение, с которым сможет работать драйвер устройства. Поле hsurf содержит манипулятор GDI для поверхности — обычно это манипулятор аппаратно-зависимого растра или DIB-секции. Поле dhpdev содержит манипулятор структуры PDEV драйвера устройства, возвращаемый функцией DrvEnablePDEV. В поле hdev хранится логический манипулятор GDI для физического устройства.
Структуры данных режима ядра 209 Размер пиксела поверхности определяется полем sizlBitmap структуры SURFOBJ. Для поверхностей, управляемых механизмом GDI, поле pvBits указывает на графические данные растра поверхности; в поле cjBits задается его размер, а поле pvScanO указывает на первую строку развертки растра. Не забывайте о том, что поверхности DIB могут храниться в памяти как в прямом, так и в перевернутом виде. В последнем случае значение pvBits не совпадает с pvScanO. В поле 1 Delta хранится смещение соседних строк развертки в байтах; при помощи этой величины механизм GDI может быстро перемещаться между строками развертки. Для нормальных растров значение поля 1 Delta положительно, а для перевернутых — отрицательно. Поле illniq предназначено для целей оптимизации. Оно содержит текущее состояние поверхности, управляемой графическим механизмом, и обновляется при каждом изменении поверхности. Это позволяет драйверу устройства организовать кэширование поверхностей. Например, если драйвер принтера PostScript получает два запроса на вывод растра с одним исходным растром и одинаковыми значениями iUniq, драйверу достаточно сохранить исходный растр при обработке первого запроса и воспользоваться им при получении второго запроса. В поле iBitmapFormat структуры SURFOBJ задается стандартный формат поверхности, управляемой графическим механизмом, наиболее близко подходящий к формату данной поверхности. Это может быть изображение с 1, 4, 8, 16, 24 и 32 битами на пиксел, несжатое или сжатое по алгоритму RLE. В Windows 2000 GDI драйвер устройства также может поддерживать сжатые изображения в формате JPEG и PNG, для чего полю iBitmapFormat присваиваются соответственно значения BMFJPEG и BMFPNG. Однако ни Windows GDI, ни графический механизм не поддерживают работу с изображениями в формате JPEG или PNG; эти изображения просто передаются драйверу устройства, если последний заявляет о своей поддержке этих форматов. В поле iType задается тип поверхности. Допустимые значения перечислены в табл. 3.9. Таблица 3.9. Типы поверхностей SURFOBJ.iType STYPE_BITMAP STYPE_DEVICE STYPE_DEVBITMAP Описание Растр, управляемый механизмом GDI Поверхность, управляемая драйвером Растр, управляемый драйвером, в формате устройства В последнем поле f jBitmap хранятся некоторые флаги поверхностей, управляемых графическим механизмом. Эти флаги сообщают, хранится ли растр в прямом или перевернутом виде, инициализируется ли он нулями, является ли он транзитивным или отсутствующим в системной памяти. Если предполагается, что структура SURFOBJ представляет все графические поверхности механизма GDI, где же хранятся сведения о цветах — например, палитра? В механизме GDI управление цветом отделено от SURFOBJ. Для каждого графического вызова, использующего SURFOBJ, передается указатель на струк-
210 Глава 3. Внутренние структуры данных GDI/DirectDraw туру XLATE0BJ, которая при необходимости обеспечивает преобразование цветов между исходной и целевой поверхностью. Например, функции DrvStretchBlt и DrvPlgBlt используют параметр pxlo, содержащий указатель на объект XLATE0BJ. Аппаратно-зависимые растры в механизме GDI Аппаратно-зависимые растры (Device-Dependent Bitmaps, DDB) управляются драйверами графических устройств с поддержкой со стороны Windows GDI. Прежде чем использовать поверхность DDB, необходимо создать для нее объект GDI, при этом возвращается манипулятор типа HBITMAP. Хотя предполагается, что аппаратно-зависимые растры поддерживаются драйвером устройства в собственном формате, все большее количество драйверов устройств Windows NT/ 2000 поручает выполнение большинства графических операций механизму GDI. Для этого формат их растров должен соответствовать формату, поддерживаемому механизмом GDI. Манипуляторы HBITMAP также находятся под управлением диспетчера манипуляторов GDI. Следовательно, с каждым манипулятором в таблице объектов GDI связано 16 байт информации, включая указатель на структуру в адресном пространстве ядра. В расширении отладчика GDI эта структура называется SURFACE. Главной частью структуры SURFACE является структура SURF0BJ. Определение структуры SURFACE выглядит следующим образом: // Windows 2000. 128 (0x80) байт typedef struct HGDI0BJ void * UL0NG UL0NG SURF0BJ XDC0BJ * FL0NG PPALETTE unsigned SIZEL HDC UL0NG HPALETTE unsigned SURFACE: hHmgr; pEntry; cExcLock; Tid; surfobj; pdcoAA; flags; ppal; unk_050[2]; sizlDim[2]; hdc: cRef: hpalHint; unk_06c[5]; // // // // // // // // // // // // // // 000 004 008 00c 010, 044, 048 04c 050 058 060 064 068 06c документируется i выводится gdikdx Структура SURFACE, как и структуры ядра других объектов GDI, начинается с 16-байтового заголовка. После заголовка следует структура SURFOBJ с информацией о формате, размере, графическими данными и т. д. Структура SURFACE должна полностью описывать растр GDI — либо DDB, либо DIB-секцию. Поэтому в структуре SURFACE после структуры SURFOBJ хранится манипулятор палитры и указатель на структуру PALETTE. PALETTE является структурой режима ядра для объекта логической палитры GDI. Мы рассмотрим структуру PALETTE в одном из следующих разделов этой главы.
Структуры данных режима ядра 211 Поле флагов flags в структуре SURFACE содержит флаги APIBITMAP (растр, созданный средствами Win32 API) и DDBSURFACE (аппаратно-зависимый растр Win32 API). Аппаратно-зависимые растры Win32 API и DIB-секции могут выбираться в контексте устройства. В этом случае поле hdc содержит манипулятор контекста устройства, а в поле cRef хранится счетчик выборов объекта в DC. Поле sizlDim обеспечивает поддержку функций Win32 API SetBitmapDimensionEx и GetBitmap- DimensionEx, предоставляя место для хранения физических размеров растра. Терминология Win32 GDI API и Windows NT/2000 DDK нередко приводит к недоразумениям; в обоих случаях используются термины DIB и DDB. В Win32 API существует три типа растров: аппаратно-зависимые растры (DDB), DIB-секции и аппаратно-независимые растры (DIB). Поверхности DDB и DIB-секции находятся под управлением GDI; это означает, что операции их создания, выбора, копирования данных, записи данных и итогового уничтожения должны выполняться средствами GDI API. Однако DIB не находятся в компетенции GDI. Вы можете самостоятельно создать DIB, не прибегая к помощи GDI. Чтение и запись графических данных осуществляются непосредственно по указателю, без использования манипулятора и GDI. В GDI предусмотрено несколько функций для вывода DIB в контекстах устройств GDI. На уровне механизма GDI все растры Win32 — DDB, DIB и DIB-секции — представляют собой поверхности. Очевидно, DIB и DIB-секции относятся к поверхностям, управляемым механизмом GDI (в документации DDK они объединяются термином DIB). Однако DDB могут храниться как в формате DIB, так и в формате устройства (DDB в документации DDK ) — все зависит от драйвера графического устройства. Каждому аппаратно-зависимому растру (DDB) соответствует манипулятор GDI (HBITMAP). Полная информация о растре хранится в структуре SURFACE адресного пространства ядра. У типичных драйверов экрана поле iType структуры SURFOBJ, находящейся внутри SURFACE, обычно равно STYPEBITMAP; поле f jBitmap обычно равно BMFJTOPDOWN; поле flags обычно равно APIBITMAP | DDB_SURFACE, а поле pvBits указывает на адресное пространство ядра. Таким образом, память для графических данных DDB выделяется в общем адресном пространстве ядра из выгружаемого пула. DIB-секции в механизме GDI В терминологии Win32 API DIB-секцией (DIB section) называется растр, который находится под управлением GDI, но доступен для пользовательских программ непосредственно через указатель. DIB-секции создаются функцией CreateDIB- Section с передачей описания в структуре BITMAPINFO. GDI возвращает манипулятор HBITMAP, с которым можно выполнять те же операции, что и с манипуляторами DDB, а также указатель на графические данные, которые можно читать и записывать через указатель, как содержимое обычного блока памяти. В таблице объектов GDI DIB-секция почти эквивалентна DDB. У нее тоже имеется манипулятор и структура SURFACE в адресном пространстве ядра. Главное отличие заключается в том, что память для графических данных DIB-секции выделяется в адресном пространстве пользовательского режима вместо
212 Глава 3. Внутренние структуры данных GDI/DirectDraw адресного пространства режима ядра. Благодаря этому обстоятельству графические данные становятся доступными для пользовательских программ; кроме того, вывод средствами механизма GDI может происходить лишь в том случае, если процесс-владелец является текущим процессом. Поле f jBitmap структуры SURF0BJ для DIB-секций равно BMF_DONTCACHE. Это означает, что графический драйвер не должен кэшировать графические данные на основании содержимого поля iUniq, поскольку графические данные могут быть изменены пользовательской программой без ведома GDI через указатель, полученный при вызове Create- DIBSection. Другое, второстепенное отличие заключается в том, что DIB-секции, как и DIB, обычно хранятся в памяти в перевернутом виде, если только их высота не задается отрицательной величиной. Мы знаем, что аппаратно-независимые растры (DIB) не находятся под управлением GDI. В частности, для них нельзя создать манипуляторы GDI. Однако при передаче DIB драйверу устройства в интерфейсе DDI применяется все та же структура SURF0BJ вместо структуры BITMAPINFO, используемой для представления DIB в Win32 API. Видимо, механизм GDI создает временную структуру SURF0BJ для представления DIB перед обращением к точкам входа механизма GDI или драйвера устройства. Кисти в механизме GDI Кисти задают цвет и узор заполнения некоторой области. Средства Win32 API позволяют создавать однородные (solid) кисти, штриховые (hatched) кисти, узорные (pattern) кисти DDB, а также узорные кисти DIB. Из раздела «Структуры данных пользовательского режима» мы знаем, что для однородных кистей в адресном пространстве пользовательского режима создается небольшая структура для хранения цвета кисти, что повышает эффективность использования однородных кистей. Для всех остальных типов кистей GDI хранит всю информацию в структуре BRUSH ядра. typedef struct { unsigned AttrFlags; COLORREF IbColor; } BRUSHHATTR; // Windows 2000. 112 (0x70) typedef struct { HGDIOBJ void * ULONG ULONG ULONG HBITMAP HANDLE ULONG hHmgr; pentry; cExcLock; Tid; ulStyle; hbmPattern hbmClient; flAttrs: байт (?) // 000, заголовок объектов GDI режима ядра // 004 // 008 // 00с // 010 // 014 // 018 // 01с ULONG ulBrushUnique: // 020 BRUSHATTR * pbrushhttr; // 024
Структуры данных режима ядра 213 BRUSHATTR unsigned unsigned COLORREF COLORREF ULONG ULONG ULONG unsigned ULONG unsigned ULONG DWORD * ULONG unsigned BRUSH; * pbrushattr; unk 030; bCacheGrabbed; crBack; crFore; ulPal Time; ulSurfTime; ulRealization; unk 04c[3]; ulPenWidth; unk 05c; ulPenStyle; pStyle; dwStyleCount; unk 06c; // 028 // 030 // 034 // 038 // 03c // 040 // 044 // 048 II 04c // 058 // 05c // 060 // 064 // 068 Структура BRUSH начинается со стандартного 16-байтового заголовка объектов GDI ядра. За ним следует поле ul Style стиля кисти, значение которого отличается от значения аналогичного поля структуры L0GBRUSH. В расширении отладчика GDI оно кодируется константами HS_CR0SS, HSPAT, HSDITHEREDCLR и т. д. В полях crBack и crFore хранится основной и фоновый цвет контекста устройства, а в поле brushAttr.lbColor — настоящий цвет кисти. В поле flAttrs хранятся дополнительные флаги (табл. 3.10). Таблица 3.10. Атрибуты кисти в структуре BRUSH BRUSH.flAttrs Описание BRJIEED_BK_CLR (0x0002) BR_DITHER_OK (0x0004) BR_IS_SOLID (0x0010) BR_IS_HATCH (0x0020) BR_IS_BITMAP (0x0040) BR_IS_DIB (0x0080) BR_IS_NULL (0x0100) BR_IS_GL0BAL (0x0200) BR_IS_PEN (0x0400) BR_IS_0LDSTYLEPEN (0x0800) BRJSJ1ASKING (0x8000) BR_CACHED_IS_S0LID (0x80000000) Необходим фоновый цвет Разрешить смешивание цветов Однородная кисть Штриховая кисть Узорная кисть DDB Узорная кисть DIB Пустая кисть Стандартные объекты Перо Геометрическое перо Растр узора используется как маска прозрачности При работе с узорными кистями DIB механизм GDI создает объект для растра кисти, манипулятор которого хранится в поле hbmPattern, при этом в поле
214 Глава 3. Внутренние структуры данных GDI/DirectDraw hbmClient остается манипулятор HGL0BAL, передаваемый при вызове CreateDIB- PatternBrush. Для узорных кистей DDB механизм GDI копирует исходную поверхность DDB, сохраняя манипулятор копии в поле hbmPattern, а манипулятор исходной поверхности — в поле hbmClient. Копирование исходного растра позволяет программисту удалить его после создания объекта кисти. Объект узорной кисти никогда не существует в одиночку; для него всегда создается парный объект растра узора. К этому моменту вы должны уже достаточно хорошо понимать, как различные типы кистей представляются в механизме GDI. Перья в механизме GDI Перо определяет цвет и стиль линий, дуг и кривых. Win32 API позволяет создавать косметические и геометрические перья с разными стилями, разной толщиной и атрибутами. Как ни странно, механизм GDI не определяет специальной структуры данных для представления перьев — для них используется та же структура BRUSH, что и для кистей. Впрочем, это выглядит вполне логично, если заметить, что расширенные перья, создаваемые функцией ExtCreatePen, определяются с помощью структуры L0GBRUSH. Механизм GDI различает перья и кисти по флагу BR_IS_PEN в поле flAttrs. Другой флаг, BRISOLDSTYLEPEN, указывает, было ли перо создано при помощи «старомодной» функции CreatePen (или CreatePenlndirect) вместо «новой» функции ExtCreatePen. Поля ulPenWidth, ulPenStyle, pStyle и dwStyleCount имеют тот же смысл, что и аналогичные поля структуры EXTLOGPEN, определяемой в Win32 API. В расширении отладчика GDI существует команда dpbrush для расшифровки структуры BRUSH, однако эта команда работает лишь с полями, относящимися к «настоящим» кистям. Для перьев, созданных функцией ExtCreatePen, эта команда возвращает неполную информацию. Палитры в механизме GDI Палитра представляет собой цветовую таблицу, по которой цветовые индексы преобразуются в значения RGB или, наоборот, значения RGB преобразуются в исходный цветовой индекс. Чтобы работать с палитрой в контексте устройства, необходимо создать логическую палитру функцией CreatePalette или CreateHalf- tonePalette. Эти функции возвращают манипулятор логической палитры (тип HPALETTE). Кроме палитр, обычно описываемых структурой LOGPALETTE, в Win32 используется и другая форма таблиц преобразования цветов — структура BITMAPINFO, являющаяся частью DIB и DIB-секций. Количество индексов в цветовой таблице вычисляется по информации поля bmiHeader структуры BITMAPINFO, а сами данные таблицы хранятся в массиве bmiColors. Структура BITMAPINFO позволяет определять цвет по индексу для растров, содержащих не более 256 цветов. При работе с 16-, 24- и 32-разрядными DIB-растрами также имеется возможность определения масок для выделения красной, зеленой и синей составляющей из 16-, 24- и 32-разрядных цветовых данных.
Структуры данных режима ядра 215 Механизм GDI должен поддерживать единую реализацию для обоих вариантов трансляции цветов. Задача решается при помощи структуры EPALOBJ (имя структуры позаимствовано из gdikdx.dll). typedef unsigned long HDEVPPAL: typedef void * PTRANSLATE; typedef void * PRGB555XL; typedef unsigned PALJJLONG; // Windows 200( typedef struct ; ( HGDIOBJ void * ULONG ULONG FLONG ULONG ULONG HOC HDEVPPAL ULONG ULONG PTRANSLATE PTRANSLATE PTRANSLATE unsigned PFN PFN ULONG PRGB555XL EPALOBJ * PAL ULONG * PAL ULONG } EPALOBJ; ). 84+4n байт _EPAL0BJ hHmgr; pentry; cExcLock; Tid; flPal; cEntries; ulTime; hdcHead; hSelected; cRefhpal; cRefRegular; ptransFore; ptransCurrent ptransOld; unk_038; pGetNearer; pGetMatch; ulRGBTime; pRGBClate; pPalette; papal Col or; apalColor[l]; // // // // // // // // // // // // // // // // // // // // // // 000. 004 008 00c 010 014 018 01c 020 024 028 02c 030 034 038 03c 040 044 048 04c, 050, 054 заголовок объек- this this->apa!Color Структура EPALOBJ представляет объект логической палитры в режиме ядра, поэтому она, как и структуры всех объектов GDI, начинается со стандартного заголовка. Тип таблицы трансляции цветов определяется содержимым поля Л Pal. В табл. 3.11 приведены значения, полученные из выходных данных расширения отладчика GDI и частично — из заголовочного файла winddi.h. Таблица 3.11. Флаги EPALOBJ EPALOBJ.flPal Значение Описание PALJNDEXED PALJITFIELDS PAL_RGB PAL BGR 0x0001 0x0002 0x0004 0x0008 Индексируемая палитра Используются битовые маски Красный, зеленый, синий Синий, зеленый, красный Продолжение &
216 Глава 3. Внутренние структуры данных GDI/DirectDraw Таблица 3.11. Продолжение EPALOBJ.flPal Значение Описание PAL_CMYK PALDC PALFIXED PALFREE PALM0N0CHR0ME PAL_DIBSECTION PALHT PAL_PGB16_555 PAL RGB16 565 0x0010 0x0100 0x0200 0x0400 0x2000 0x8000 0x100000 0x200000 0x400000 Голубой, малиновый, желтый, черный Не может изменяться Только два цвета Используется для DIB-секции Полутоновая палитра 16-битный RGB-цвет в формате 555 16-битный RGB-цвет в формате 565 В поле cEntries хранится количество элементов в цветовой таблице ара!Color. Эти два поля аналогичны полям структуры PAL0BJ. Механизм GDI сохраняет в структуре EPAL0BJ адреса двух функций, pGetNearest и pGetMatch. На компьютере автора поле pGetNearest ссылается на win32k!ulIndexedGetNearestFromPal Entry, а поле pGetMarch — на ulIndexedGetMatchFromPalEntry (хотя в других системах они могут ссылаться на что-нибудь другое). Драйверы устройств не работают со структурой EPAL0BJ напрямую. В файле winddi.h определяется структура PAL0BJ, содержащая единственное поле ulReserved. Чтобы обратиться к цветовой таблице, драйвер устройства должен вызвать функцию графического механизма PAL0BJ_cGetColors. Прослеживается аналогия со структурой XLATEOBJ, для обращения к которой также определяется специальная функция XLATE0BJ_cGetPalette. Регионы в механизме GDI Регион (region) определяется как совокупность точек на поверхности графического устройства. Он может иметь форму прямоугольника, многоугольника, эллипса или произвольной комбинации этих фигур. Для регионов определены операции заливки, инвертирования и обводки; кроме того, они используются при отсечении или проверке принадлежности (hit testing). Вероятно, чаще всего регионы применяются при отсечении. Регионы принадлежат к числу объектов, управляемых GDI. Новые регионы создаются такими функциями, как CreateRectRgn, CreateRoundRgn и CreateEllip- ticRgn. Объединение существующих регионов осуществляется посредством логических операций. Все эти функции возвращают манипулятор объекта GDI, HRGN, который используется при последующем вызове функций GDI. Как было показано в разделе «Структуры данных пользовательского режима», координаты прямоугольных регионов GDI хранит в структурах данных пользовательского режима. Для других регионов информация хранится в адресном пространстве ядра.
Структуры данных режима ядра 217 В расширении отладчика GDI предусмотрена команда dr, предназначенная для расшифровки HRGN или указателя на структуру данных REGION режима ядра. Команда даже перечисляет все прямоугольники, из которых состоит заданный регион. Ниже приведена информация о структуре REGION, полученная при помощи этой команды. // Windows 2000, переменный размер // Не используйте непосредственные ссылки на scnPntCntToo! typedef struct LONG scnPntCnt; LONG scnPntTop; LONG scnPntBottom; LONG scnPntX[2]; LONG scnPntCntToo; } SCAN; // Количество координат х // Верхняя граница (включается) // Нижняя граница (не включается) // Массив переменной длины, содержащий х пар // То же. что и scnPntCnt; // Windows 2000. переменный размер struct REGION / l HGDIOBJ hHmgr: void * pentry; ULONG cExcLock; ULONG Tid: unsigned sizeObj; unsigned unk_014[2] SCAN * pscnTail; unsigned sizeRgn; unsigned cScans; RECTL rcl; SCAN scnHead[l] // 000. заголовок объектов GDI режима ядра // 004 // 008 // 00с // 010 ; // 014 // 01с // 020 // 024 // 028 ; // 038 Структура REGION начинается со стандартного 16-байтового заголовка. Как упоминалось в разделе «Структуры данных пользовательского режима», GDI оптимизирует процедуру создания регионов, состоящих из одного прямоугольника, за счет связывания с манипулятором GDI структуры RECT пользовательского режима. Тем самым GDI привязывает объект региона к процессу-создателю. Для поддержания этой связи механизм GDI сохраняет в заголовке идентификатор программного потока, создавшего объект. Структура REGION имеет переменный размер. Она содержит всю информацию о регионе, объем которой может увеличиваться или уменьшаться в результате применения операций к региону. Например, если регион объединяется с другим регионом операцией RGNJ3R, размер структуры обычно увеличивается, а при использовании операции RGN_AND он обычно уменьшается. Чтобы уменьшить количество операций выделения/освобождения, механизм GDI не выделяет блок памяти именно того размера, который необходим для представления региона; вместо этого он выделяет несколько больший блок, позволяющий увеличивать размеры региона без повторного выделения памяти. Вероятно, размер структуры REGION при выделении памяти хранится в поле sizeObj, а фактически используемый размер — в поле sizeRgn.
218 Глава 3. Внутренние структуры данных GDI/DirectDraw В поле rcl хранятся данные прямоугольника, ограничивающего регион. Важнейшими данными в структуре REGION является массив структур SCAN. В поле cScans хранится количество структур в массиве, а поле pscn ссылается на адрес, следующий после конца последней структуры в массиве. Программисты обычно не хранят указатели подобного рода, поскольку они легко вычисляются по начальному адресу, количеству и размеру элементов. Однако здесь интересно заметить, что структура SCAN имеет переменный размер. Она не документирована в Windows NT/2000 DDK, хотя 16-разрядная версия этой структуры документируется в Windows 95 DDK. Структура SCAN содержит информацию об одной «строке развертки» региона, высота которой в системе координат может быть равна одному пикселу (а может быть и не равна). Выражаясь точнее, в SCAN хранится информация о пересечении региона с областью, ограниченной двумя горизонтальными линиями, при условии, что пересечение контура региона с этой областью состоит только из вертикальных отрезков. Механизм GDI делит регион на последовательность структур SCAN в направлении сверху вниз. Поскольку точки пересечения контура региона с верхней и нижней границами SCAN имеют одинаковые координаты х, механизм GDI хранит лишь одну из них. Итак, в структуре SCAN хранятся значения координат у верхней и нижней границы, пары значений координаты х для пересечений и две копии количества пересечений. Следовательно, для сложных регионов (например, имеющих внутренние отверстия) структура SCAN экономит память, необходимую для представления региона. В первом и последнем поле структуры SCAN хранятся две копии количества пересечений. Поскольку размер структуры SCAN переменный, ее последнее поле не имеет фиксированного смещения от начала структуры. Возможно, у вас возник вопрос — почему структуры REGION и SCAN так странно устроены? На это у механизма GDI есть веские причины. Регионы обычно передаются функциям графических драйверов в виде структур CLIP0BJ. Интерфейс DDI не предоставляет доступа к внутренней структуре данных CLIP0BJ; вместо этого он позволяет графическим драйверам перечислить все прямоугольники, образующие регион, при помощи функции CLIPOBJbEnum. Драйвер может указать порядок перечисления прямоугольников при помощи функции CLIPOBJ_cEnumStart. Механизм GDI позволяет производить перечисление слева направо, сверху вниз; справа налево, сверху вниз; слева направо, снизу вверх и т. д. — в любом порядке, удобном для GDI. Поле pscanTail структуры REGION позволяет механизму GDI быстро перейти к последней структуре SCAN. Поле scnPntCount позволяет быстро переходить слева направо или к следующей структуре SCAN при перечислении сверху вниз. Поле scnPntCountToo обеспечивает быстрый переход справа налево или к следующей структуре SCAN при перечислении снизу вверх. В следующем примере демонстрируется связь структуры REGION с регионами, знакомыми нам по Win32 API. При создании региона функцией CreateEllip- ticRgn(0,0,100,100) вы получаете манипулятор региона. Укажите его при вызове команды dr расширения GDI; в отладчике выводится адрес структуры REGION и список всех прямоугольников, образующих регион. Структура REGION содержит 63 структуры SCAN с ограничивающим прямоугольником [0, 0, 99, 99]. В табл. 3.12 приведен сокращенный список элементов массива структур SCAN.
Структуры данных режима ядра 219 Таблица 3.12. Cnt 0 2 2 2 2 2 2 2 0 Массив структур Тор -maxint - 0 1 39 47 52 97 98 99 SCAN - 1 в структуре REGION (для круга) Bottom 0 1 2 47 52 60 98 99 maxint Х[] 47,52 39,60 1,98 0,99 1,98 39,60 47,52 CntToo 0 2 2 2 2 2 2 2 0 Из приведенного примера видно, что структура REGION содержит аппроксимацию исходной фигуры в виде комбинации прямоугольников, задаваемых целочисленными координатами. Следовательно, если создать для региона манипулятор GDI и потом масштабировать его (при помощи функций GetRegionData и ExtCreateRegion с параметром XF0RM), результат будет отличаться от того, который получится при обратной процедуре (предварительном масштабировании математическими методами и последующем создании манипулятора GDI). Структура REGION описывает регион слева направо, сверху вниз. Верхние и левые координаты включаются в регион, а нижние и правые — не включаются. При создании структур REGION GDI старается действовать как можно точнее, поэтому многие структуры SCAN имеют высоту всего в один пиксел. Например, несколько первых и последних структур SCAN для круга соответствуют прямоугольникам высотой в 1 пиксел. Но там, где это возможно, GDI с целью экономии памяти увеличивает SCAN до максимально возможной высоты. Например, центральная часть круга аппроксимируется прямоугольником высоты 5, соответствующей средней структуре SCAN в массиве (координаты от 47 до 52). Для точного представления регионов, не имеющих ярко выраженного прямоугольного строения, размер структуры REGION обычно прямо пропорционален высоте региона и в меньшей степени зависит от ширины региона. Например, при удвоении высоты эллипса размер REGION может вырасти вдвое, а при удвоении ширины размер REGION может вообще не измениться. Количество структур SCAN и размеры REGION напрямую влияют на работу механизма GDI, на использование памяти драйверами устройств и на общее быстродействие, особенно в режимах печати с высоким разрешением на качественных принтерах. В примере из табл. 13.12 следует обратить внимание на первую и последнюю структуры SCAN. Они не соответствуют фрагментам региона, то есть не содержат
220 Глава 3. Внутренние структуры данных GDI/DirectDraw координат х. В сущности, эти структуры утверждают, что в интервалах у = = [maxint - 1,0] и [99, maxint] в системе координат отсутствуют участки, принадлежащие данному региону. Если эти структуры не описывают видимые части региона, зачем же они хранятся в драгоценном адресном пространстве ядра? Ответ — для упрощения реализации и унификации операций с регионами. Например, при инвертировании региона можно обойтись тем же количеством структур SCAN; достаточно включить в каждую структуру SCAN значения -maxint - 1 и maxint в качестве первой и последней координат х. Пустой регион представляется структурой REGION с ограничивающим прямоугольником {0, 0, 0, 0} и единственной структурой SCAN {0, -maxint - 1, maxint, 0}. Вы когда-нибудь замечали, что при вызове функции GDI для создания круглого региона с координатами 0, 0, 100, 100 вам возвращается регион с ограничивающим прямоугольником 0, 0, 99, 99, в который правая и нижняя граница все равно не включаются? Другими словами, CreateEllipticRgn создает фигуру меньших размеров, чем создала бы функция Ellipse. Да, такова суровая реальность Windows. Этот известный дефект, сохранившийся со времен Windows 3.0 до Windows 2000, документируется в MSDN Win32 SDK (статья Q83807). Структура REGION остается закрытой как для прикладных программистов в Win32 API, так и для программистов драйверов устройств в интерфейсе DDL Единственной низкоуровневой структурой региона, которую можно получить в Win32 API, является структура RGNDATA, используемая функциями GetRegionData и ExtCreateRegion. В RGNDATA вместо массива SCAN присутствует массив прямоугольников. В интерфейсе DDI используется абстрактная структура CLIP0BJ. Для получения прямоугольников, образующих регион, необходимо вызвать функцию CLIP0BJ_bEnum. Траектории в механизме GDI Траектория (path) представляет собой совокупность фигур (или геометрических форм), к которой применяются операции заливки, обводки или заливки с одновременной обводкой. Для создания траектории можно воспользоваться средствами Win32 API, однако вы даже не получите манипулятора созданного объекта. Любой нормальный программист понимает, что для представления траектории в процессе построения и при последующем использовании в GDI требуется какая-то внутренняя структура данных. Графический механизм Windows (win32k.sys) даже экспортирует довольно большую группу функций для выполнения операций с объектами траекторий в драйверах устройств. По данным расширения отладчика GDI, объекты траекторий присутствуют в таблице объектов GDI. Команда dumpobj PATH выводит информацию обо всех объектах траекторий в системе. Расширение отладчика GDI не содержит команд для расшифровки манипулятора объекта траектории или соответствующей структуры данных режима ядра (в этом отношении траектории также отличаются от других типов объектов GDI). Команда dpo расшифровывает только структуру PATH0BJ, передаваемую функциям механизма GDI или функциям драйверов устройств — например, EngStrokePath или DevStrokeAndFi 11 Path. Приведенная ниже информация о структурах данных, представляющих траектории в механизме GDI, была получена с использованием нескольких тесто-
Структуры данных режима ядра 221 вых объектов траекторий, а также документации Win32 API и DDK. Основной структуре было присвоено имя PATH. // Windows 2000. переменный размер typedef struct _PATHDT PATHDT * _PATHDT * unsigned unsigned POINTFIX } PATHDT; pNext; pLast; flags; pointno; point[l]; // 000 // 004 // 008 // 00c // 010 // Windows 2000, переменный размер typedef struct i unsigned void * unsigned PATHDT } PATHDEF; unk_00; pTail; nAllocSize; pathdt[l]; // Windows 2000. ? байт typedef struct i HGDIOBJ void * ULONG ULONG PATHDEF * SEGMENT * PATHDT * PATHDT * RECTFX POINTFX ULONG unsigned } PATH; hHmgr; pentry; cExcLock; Tid; ppachain; pFirst; ppfirst; pplast; rcfxBoundBox; ptfxSubPathStart: nCurves: unk_38[10]; // 000 // 004 // 008 // 010 // 000. заголовок объектов GDI режима // 004 // 008 // 00c // 010 // 014 // 014 // 018 // 01c // 02c // 034 // 038 ядра Структура PATH в отличие от структуры REGION имеет фиксированный размер. Она начинается со стандартного 16-байтового заголовка, за которым следует указатель (ppachain) на структуру PATHDEF с реальным определением траектории. Как говорилось выше, траектория представляет собой совокупность фигур; ее внутреннее представление PATHDEF представляет собой список структур PATHDT, каждая из которых описывает одну часть фигуры, образующей траекторию. В структуре PATH хранятся указатели на первую и последнюю структуры PATHDT в списке (поля pprfirst и pprlast). Кроме того, в структуру PATH включены данные ограничивающего прямоугольника и начальная точка траектории. Как уже упоминалось, координаты устройства хранятся в виде 32-разрядных значений с фиксированной точкой, в отличие от интерфейса Win32 API, использующего 32-разрядные числа со знаком. Примером служит структура PATH. И ограничивающий прямоугольник, и начальная точка представлены 32-разрядными числами в формате с фиксированной точкой. Старшие 28 бит из 32 обра-
222 Глава 3. Внутренние структуры данных GDI/DirectDraw зуют целую часть, а младшие 4 бита — дробную. Например, число 1 в этой записи представляется в виде 0x10, а число 1.125 — в виде 0x12. Microsoft называет этот формат «FIX-координатами» или дробными координатами (fractional coordinates). Система дробных координат позволяет задавать координаты на поверхности устройства с точностью до 1/16 пиксела. FIX-коор- динаты используются при определении линий и кривых Безье, являющихся базовыми компонентами траекторий. В результате точность вычислений повышается без затрат, связанных с применением операций с плавающих точкой. Структура PATHDEF имеет переменный размер и содержит все структуры PATHDT, входящие в траекторию. В поле nAllocSize сохраняется размер текущего блока, а поле pTail ссылается на первый свободный байт. По значениям этих полей можно легко узнать о том, что выделенная для траектории память подходит к концу. После этих полей следует серия структур PATHDT, образующих двусвяз- ный список. Структура PATHDT представляет группу точек на кривой, обладающих некоторыми общими атрибутами. Поле pNext каждой структуры указывает на следующую структуру PATHDT в списке или равно NULL для последней структуры в списке. Поле pStart указывает на предыдущую структуру PATHDT или равно NULL для первой структуры в списке. В поле f 1 ags хранятся общие атрибуты точек. Флаги, используемые в этом поле, документируются в Windows NT/2000 DDK при описании структуры PATHDATA (табл. 3.13). Таблица 3.13. Флаги PATHDT PATHDT.flags Значение Описание PDBEGINSUBPATH 0x0001 Первая точка начинает новую субтраекторию (фигуру) PDENDSUBPATH 0x0002 Последняя точка завершает субтраекторию (фигуру) Сбросить стиль в начале новой субтраектории Добавить линию, соединяющую последнюю точку субтраектории (фигуры) с первой точкой 0x0010 Группы из трех точек описывают кривую Безье, а не сегмент линии PD_RESETSTYLE 0x0004 PD CL0SEFIGURE 0x0008 PD BEZIERS Итак, траектория является объектом GDI, как регион или DDB. Перед использованием объектов GDI при вызове графических функций их необходимо выбрать в контексте устройства. Траектории, в отличие от других объектов GDI, не имеют специальной функции выбора — они неявно выбираются при создании. В момент создания новой траектории старая траектория в контексте устройства уничтожается. Впрочем, механизм GDI все равно должен хранить манипуляторы траекторий для разных контекстов, поэтому манипулятор объекта траектории для заданного контекста устройства хранится в поле hpath структуры DEVLEVEL. За этим полем также следует поле флагов, flPath, и структура LINEATTRS для описания атрибутов линии.
Структуры данных режима ядра 223 Рассмотрим пример — небольшой фрагмент кода Win32, в котором создается траектория: const POINT Points[3] - { {200.50}. {250. 150}. {300. 50} }; BeginPath(hDC); MoveToEx(hDC. 100. 100. NULL); LineTo(hDC. 150. 150); PolyBezierTo(hDC. & POINTSL0]. 3); EndPath(hDC); При помощи расширения отладчика GDI можно провести поиск всех объектов траекторий в системе. Воспользуйтесь командой dumpobj PATH, а затем введите команду dt <MaHunyjmmop_GDI>, чтобы вывести элемент таблицы объектов GDI, соответствующий конкретному манипулятору. Из выходных данных команды берется указатель на структуру PATH, содержащую указатель на структуру PATHDEF. Структура PATHDEF определяется следующим образом: // Пример структуры PATHDEF 0000: unkJO 0x00000000' 0004: pTail & pathdt[2] 0008: nAllocSize Oxfc 000c: pathdt[0] & pathdt[l]. NULL. 5. 2. 100.0. 100.0. 150.0. 150.0 0014: pathdt[l] NULL. & pathdt[0]. 0x12. 3 200.0. 50.0. 250.0. 150.0. 300.0. 50.0 0054: pathdt[2] Механизм GDI выделил для хранения траектории блок из 4032 байт (OxfcO), в котором в настоящий момент занято только 84 (0x54) байта. Для будущего роста этой траектории остается еще достаточно места. В структуре PATHDEF хранятся две структуры PATHDT, объединенные в двусвязный список. Первая структура PATHDT состоит из двух точек с флагами PDBEGINSUBPATH | PDRESETSTYLE. Итак, перед нами две точки, образующие отрезок. Вторая структура PATHDT состоит из трех точек с флагами PDENDSUBPATH | PDBEZIERS. Она описывает одну кривую Безье, которая продолжается из предыдущей точки и завершает субтраекторию. Структура PATHDEF точно воспроизводит все параметры, указанные в коде Win32. Теперь мы знаем, что структура PATH позволяет представить отрезки и кривые Безье, а также их произвольные комбинации. Например, вызовы функций CloseFigure, LineTo, MoveToEx, PolyBezier, PolyBezierTo, Polygon, PolylineTo, PolyPolygon и PolyPolyline легко преобразуются в последовательности отрезков и кривых Безье. С другой стороны, Windows 95/98 позволяет включать в построение траекторий вызовы TextOut и ExtTextOut. Как в структуре PATH представляется текст? Оказывается, при построении траекторий можно использовать только шрифты TrueType, и в траектории записываются только контуры текстовых строк, которые фактически представляют собой кривые Безье. Кроме перечисленных функций Windows NT/2000 позволяет включать в траекторию эллиптические кривые. Например, при построении траектории можно использовать функции AngleArc, Arc, АгсТо, Ellipse, Pie и т. д. Как механизм GDI решает эту задачу? Эллиптические кривые разбиваются на последовательности кривых Безье по аналогии с тем, как непрямоугольные регионы разбиваются на
224 Глава 3. Внутренние структуры данных GDI/DirectDraw группы строк развертки. Предположим, перед вызовом EndPathO в приведенный выше фрагмент включаются две дополнительные команды: CloseFigure(hDC); Ellipse(hDC. -100. -100. 100. 100); Функция CloseFigureO завершает вторую структуру PATHDT (см. выше). Функция EllipseO добавляет в список еще одну структуру PATHDT — группу кривых Безье из 13 точек. Первая точка начинает новую фигуру, а остальные 12 точек образуют 4 кривых Безье. Механизм GDI аппроксимирует эллипс при помощи 4 кривых Безье. Определения 13 контрольных точек выглядят следующим образом: { 99. -0.5 }. { 99. -55.4375 }. { 54.5. -100 }. { -0.5. -100 }. { -55.5. -100 }. { -100. -55.4375 }. { -100. -0.5 }. {-100. 54.4375 }. { -55.5. 99 }. { -0.5. 99 }. { 54.5. 99 }. { 99. 54.4375 }. { 99. -0.5 }. Становится понятно, почему в структуре PATH используются FIX-коорди- наты. Округление координат до целых чисел приведет к искажению формы эллипса. Структура PATH используется не только для хранения траекторий в Win32 GDI. Она также играет очень важную роль в DDI (интерфейсе между механизмом GDI и драйверами графических устройств). В частности, вызовы функций рисования линий (такие, как LineTo и PolyBezier) преобразуются в вызовы функции DrvStrokePath, которой передается указатель на структуру PATH0BJ. Функции с заливкой областей (например, Ellipse и Polygon) преобразуются в вызовы функции DrvStrokeAndFi 11 Path, которой также передается указатель на PATH0BJ. По сравнению с Windows NT 4.0 в Windows 2000 добавилась новая точка входа DrvLineTo, повышающая быстродействие для вызовов LineTo с целочисленными координатами конечных точек. Структура PATH0BJ также относится к числу «замаскированных» структур DDI и содержит только два открытых поля. Вы можете воспользоваться функциями GDI для получения информации о компонентах траектории, построения новых траекторий или расширения траектории посредством включения новых кривых. Например, функция EngCreatePath создает новый объект PATH0BJ; функция PATH0BJ_ bPolyBezier включает в траекторию кривые Безье; функция PATHOBJbEnum перечисляет записи компонентов траектории в структуре PATHDATA, очень похожей на описанную выше структуру PATHDT. Шрифты в механизме GDI То что в Win32 API обычно именуется шрифтами (fonts), правильнее было бы называть «логическими шрифтами». Логические шрифты создаются функциями CreateFont, CreateFontlndirect и CreateFontDirectEx. При вызове функции указываются характеристики, которыми должен обладать шрифт. GDI (а точнее — система подстановки шрифтов, font mapper) находит физический шрифт, в наибольшей степени соответствующий предъявленным требованиям.
Структуры данных режима ядра 225 Для ссылок на логические шрифты, создаваемые GDI, используются манипуляторы типа HF0NT. В расширении отладчика GDI объекты шрифтов обозначаются типом LF0NT. Например, команда dumpobj LF0NT выводит список манипуляторов всех логических шрифтов в системе. Передавая манипулятор логического шрифта команде he! f, вы получите информацию о структуре данных, ассоциированной с этим манипулятором в адресном пространстве ядра. Команда просто выводит дамп соответствующей структуры L0GF0NTW. // Windows 2000. ? байт typedef struct г i HGDIOBJ void * ULONG ULONG unsigned PDEV_WIN32K * unsigned HGDIOBJ unsigned WCHAR unsigned hHmgr; pentry; cExcLock; Tid; unk_010[3]; ppdev; unk 020[8]: hPFE; unk 020[39]: Face[32]; nSize; ENUMLOGFONTEXW enumlogfontex; // // // // // // // // // // // // } LFONT; На самом деле данные, хранимые в пространстве ядра для логических шрифтов, отнюдь не ограничиваются структурой L0GF0NTW, показываемой расширением GDI. Даже функция GetObject возвращает для манипулятора логического шрифта структуру из 356 байт, больше напоминающую структуру ENUMLOGFONTEXW. Ее первым полем действительно является структура L0GF0NTW. Другое поле, заслуживающее внимания, — указатель на структуру физического устройства механизма GDI. Следовательно, в механизме GDI структура LF0NT, поддерживаемая для логического шрифта, фактически представляет собой структуру L0GF0NTW с несколькими дополнительными полями, образующими структуру ENUMLOGFONTEXW, что вполне разумно. Но где же хранится информация о соответствии между логическими и физическими шрифтами? И как информация о шрифтах передается функциям драйверов графических устройств — например, DrvTextOut? Расширение отладчика GDI показывает еще одну недокументированную структуру данных GDI — PFE. Манипулятор структуры PFE хранится в поле hPFE каждой структуры LF0NT. Вы можете получить список всех манипуляторов PFE при помощи команды dumpobj PFE расширения отладчика GDI, а затем воспользоваться командой pfe для получения информации о структуре ядра PFE. Структура ядра PFE выглядит следующим образом: // Windows 2000, 108 (0x6с) байт struct PPF; typedef struct { HGDIOBJ hHmgr; // 000. заголовок объектов GDI режима ядра 000. заголовок объектов GDI 004 008 00с 010 01с 020 040 044 0d0 110 114
226 Глава 3. Внутренние структуры данных GDI/DirectDraw void * ULONG ULONG PFF * ULONG ULONG pentry; cExcLock: Tid; pFFF; i Font; ЛРРЕ; FD_GLYPHSET * pfdg; void * IFIMETRICS * unsigned void * unsigned unsigned unsigned unsigned void * unsigned unsigned unsigned unsigned unsigned unsigned void * unsigned unsigned unsigned PFE: unk__020; pifi: idifi; pkp; idkp; ckp; iOrientation; cjEfdwPFE; pgiset; ulTimeStamp; ufi: unk J)4c; pid; ql: unk_058; pFlEntry; cAlt: cPfdgRef; aiFamilyName; // // // // // // // // // // // // // // // // // // // // // // 004 008 00c 010. pff 014 018 01c. gs 020 f8dd( 024. ifi 028 02c 030 034 038 03c 040 044 048 04c 050 054 058 // 05c // // // 060 064 068 Структура PFE начинается со стандартного заголовка объектов GDI длиной 16 байт. Поле pPFF ссылается на структуру PFF, содержащую информацию о физическом файле шрифта. Структура PFF описывается ниже в этом разделе. В поле pfdg хранится указатель на структуру FDGLYPHSET, документированную в SDK. Структура FD_GLYPHSET определяет отображение символов Unicode на внутренние манипуляторы глифов. Символы Unicode представляются 16-разрядными значениями. Кодировка Unicode поддерживает тысячи разнообразных символов, а шрифты могут ограничиваться небольшим подмножеством этой кодировки. Шрифт представляет собой совокупность глифов, каждому из которых присвоен уникальный манипулятор. При помощи структуры FDGLYPHSET механизм GDI устанавливает соответствие между символами Unicode и манипуляторами глифов. В расширении отладчика GDI предусмотрена команда gs для расшифровки структуры FDGLYPHSET. Например, для шрифта Small Fonts (smallf.fon) эта команда показывает, что шрифт состоит из 224 глифов. Глифу символа «пробел» соответствует манипулятор 0, глифу символа «А» — манипулятор 0x21 и т. д. Структура FDGLYPHSET создается точкой входа шрифтового драйвера DrvQuery- FontTree, Когда параметр iMode равен QFTGLYPHSET. Также обратите внимание на поле pifi, в котором хранится указатель на структуру IFIMETRICS, также документированную в DDK. Структура IFIMETRICS содержит сведения о гарнитуре, используемые GDI. В частности, в ней хранятся имена семейства, стиля и гарнитуры, уникальное имя, возможности эмуляции, идентификатор внедрения и, наконец, 10-байтовый массив panose с описанием визуальных характеристик шрифта. Структура IFIMETRICS заполняется функци-
Структуры данных режима ядра 227 ей DrvQueryFont. В расширении отладчика GDI предусмотрена команда ifi, предназначенная для расшифровки структуры IFIMETRICS. Например, для шрифта Small Fonts команда возвращает информацию о растровом формате 1 бит/пиксел, о возможности масштабирования с целочисленным коэффициентом и поворотах на 90°, а также об эмуляции полужирного, курсивного и полужирного курсивного начертаний. Логический шрифт связывается с конкретным процессом Win32. При уничтожении процесса все его манипуляторы GDI уничтожаются, а элементы таблицы объектов освобождаются для повторного использования. Однако манипуляторы PFE существуют на уровне системы и не ассоциируются с конкретными процессами. С одной структурой PFE может быть связано несколько логических шрифтов. Структура PFF описывает файл физического шрифта. Как нетрудно предположить, в расширении GDI также имеется команда pff для расшифровки этой структуры. Определение структуры PFF выглядит так: struct RF0NT; typedef struct _PFF ULONG PFF * PFF * WCHAR * ULONG ULONG unsigned ULONG ULONG ULONG ULONG RFONT * void * void * unsigned void * void * void * void * ULONG unsigned ULONG void * void * unsigned PFE * WCHAR PFF; sizeofThis; pPFFNext: pPFFPrev; pwazPathName cwc; cFiles; unk_018[2]; flState; cLoaded; cNotEnum; cRFONT; prfntList; hff; hdev; dhpdev; pfhFace; pfhFamily; pfhUFI; pPFT; ulChecksum!; unk_054; cFonts; ppfv; pPvtDataHead; unk 064; pPFE; wszStrings[l]; // // // // // // // // // // // // // // // // // // // // // // // // // // // 000 004. 008. 00c 010 014 018 020 024 028 02c 030. 034 038 03c 040 044 048 04c. 050 054 058 05c 060 064 068. 06c pff pff fo pft pfe } Структуры PFF в механизме GDI объединяются в двусвязные списки. Ссылки на следующий и предыдущий элементы хранятся соответственно в полях pPFFNext и pPFFPrev. Следующее поле содержит указатель на имя файла шрифта на дис-
228 Глава 3. Внутренние структуры данных GDI/DirectDraw ке - например, «\??\C:\WIN2000\FONTS\SMALLF.FON», для которого в поле fl State установлен флаг PFFSTATEPERMANENTFONT. В поле cLoaded хранится признак, который показывает, был ли файл загружен в память; в поле cRFONT хранится количество реализованных шрифтов, созданных на основании физического шрифта, а поле prfntList ссылается на первый элемент списка реализованных шрифтов. Поле pPFT содержит указатель на структуру PFT, которая представляет собой таблицу структур PFF. Структура PFT расшифровывается командой pft и выглядит следующим образом: typedef struct { void * pfhFamily; void * pfhFace; void * pfhUFI; ULONG cBuckets; ULONG cFiles; PFF * apPFF[l]: // 000 // 004 // 008 // 00c // 010 // 014 } PFT: В первых трех полях структуры PFT хранятся указатели на три хэш-таблицы, расшифровываемые командой f h. Хэш-таблицы предназначены для быстрого установления соответствия между логическими и физическими шрифтами. В структуре PFT данные шрифтов сохраняются в хэш-таблице, рассчитанной, как показывают эксперименты, на 100 элементов. В структуре PFT сохраняется указатель на первую структуру PFF в двусвязном списке, создаваемом при помощи двух ссылочных полей структуры PFF. В поле cFiles хранится общее количество шрифтовых файлов, объединенных в структуре PFT. Механизм GDI создает три таблицы структур PFF — для открытых шрифтов, для закрытых шрифтов и для шрифтов устройств. Указатели на эти таблицы хранятся ^ трех глобальных переменных — win32k!gpPFTPublic (открытые шрифты), win32k!gpPFTPrivate (закрытые шрифты) и win32k!gpPFTDevice (шрифты устройств). В расширении отладчика GDI эти переменные используются в работе трех команд, отображающих содержимое трех таблиц: pubft, pvtft и devft. Список шрифтов, выводимый по команде pubft, выглядит примерно так: apPFF[2] "\??\C:\WIN2000\FONTS\TREUCBD.TTF" "\??\C:\WIN2000\FONTS\CGA80W0A.FON" apPFF[3] "\??\C:\WIN2000\FONTS\MICROSS.TTF" apPFF[5] ,,\??\C:\WIN2000\FONTS\PALA.TTF" apPFF[98] "\??\C:\WIN2000\FONTS\TIMES1.TTF" Настало время описать самую важную шрифтовую структуру графического драйвера, FONTOBJ, и ее расширенную версию RF0NT. Выше говорилось о fOM, что структура LF0NT описывает логический шрифт, то есть запрос на получение шрифта с заданным размером и углом поворота, особыми характеристиками (например, насыщенностью) и т. д. С другой стороны, шрифтовой файл, описываемый структурой PFF, представляет собой общий шаблон, который может масштабироваться для разных размеров, поворачиваться на разные углы и дополняться другими специфическими возможностями. В простейшем варианте для каждого символа текстовой строки механизм GDI обращается к шрифтовому драйверу за описа-
Структуры данных режима ядра 229 нием общего контура символа, масштабирует его до нужного размера, поворачивает на нужный угол, преобразует в растр, использует и забывает о его существовании. Однако в общем случае такая схема крайне неэффективна, особенно если учесть, что для однобайтовых шрифтов с небольшим количеством символов легко организуется кэширование, экономящее массу времени по многократному построению растров для каждого шрифта. Для этого в механизме GDI используется структура RF0NT. Структура RFONT описывает конкретную реализацию или, если хотите, — конкретный экземпляр шрифта. Это не логический и не физический шрифт, а набор глифов, созданных в соответствии с требованиями логического шрифта на основании общего описания, взятого из шрифтового файла. Первая часть структуры RF0NT документируется в DDL как структура FONTOBJ, это сделано для ускорения обращений со стороны графических драйверов. К остальным полям структуры RF0NT можно обращаться только посредством специальных методов структуры FONTOBJ - таких, как F0NT0BJ_cGetG1yphHandles и FONTOBJ_cGetGlyphs. В расширении отладчика GDI расшифровка структуры RF0NT выполняется при помощи команды fo. Ниже приведено объемистое определение структуры RF0NT. typedef struct i void * void * void * void * ULONG ULONG ULONG ULONG ULONG ULONG void * void * void * void * void * void * void * ULONG ULONG ULONG } CACHE; pgdNext; pgdThreshold; pjFirstBlockEnd pdblBase; cMetrics; cjbbl; cBlocksMax; cBlocks; cGlyphs; cjTotal; pbblBase; pbblCur; pgbNext; pgbThreshold; pjAuxCacheMem; cjGlyphMax; bSmal1 Metrics; iMax; iFirst; cBits; struct RFONT { FONTOBJ ULONG ULONG ULONG PVOID ULONG PVOID DHPDEV PFE * f Ob j ; iUnique; Л Type; ulContent; hdevProducer; hDeviceFont; hdevConsumer; dhpdev; ppfe: // 000 // 02c // 030 // 034 // 038 // 03c // 040 // 044 // 048
230 Глава 3. Внутренние структуры данных GDI/DirectDraw PFF * FD XF0RM ULONG MATRIX ULONG FLOATOBJ FLOATOBJ ULONG MATRIX ULONG unsigned MATRIX ULONG POINT POINT POINT POINT ULONG FIX FIX FIX pointFX pointFX ULONG LONG LONG ULONG ULONG FD XFORM LONG LONG LONG LONG ULONG FLOATOBJ FLOATOBJ FLOATOBJ LONG FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ ULONG ULONG ULONG void * void * ULONG RFONT * RFONT * RFONT * ppff; fdx; cBitsPerPel; mxWorldToDevice; iGraphicsMode; eptflNtoWScale_x_i; eptflNtoWScale у i; bNtoWIdent: xoForDDI__pmx; xoForDDI_ulMode; unk_000: mxForDDI; flRealizedType: pt1 Underlined ptlStrikeOut; ptIULThickness; ptlSOThickness; ICharlnc; fxMaxAscent; fxMaxDescent; fxMaxExtent: ptfxMaxAscent; ptfxMaxDescent; cxMax; IMaxAscent; IMaxHeight; cyMax; cjGlyphMax; fdxQuantized; // 04c // 050 // 060 // 064 // OaO // 0a4 // Oac // 0b4 // 0b8 // Obc // OcO // 0c4 // 100 // 104 // 10c // 104 // 10c INonLinearExtLeading; INonLinearlntLeading; 1NonLi nearMaxCharWi dth; 1NonLi nea rAvgCha rWi dth; ulOrientation; pteUnitBase_x; pteUnitBasely; efWtoBase; 1 Ascent; pteUnitAscent_x; pteUnitAscent_y; efWtoDAscent; efDtoWAscent; efWtoDEsc: efDtoWEsc; efEscToBase; ef EscToAscent; Л Info; hgBreak; fxBreak; pfdg; wcgp; cSelected; rflPDEV prfntPrev; rflPDEV_prfntNext; rflPFF_prfntPrev;
Структуры данных DirectDraw 231 RFONT * void * CACHE POINT ULONG FLOATOBJ FLOATOBJ TEXTMETRICW LONG LONG LONG ULONG ULONG RFONT * RFONT * RFONT * void * void * ULONG ULONG ULONG ULONG ULONG ULONG rflPFF_prfntNext; hsemCache; cache; ptlSim; bNeededPaths; efDtoWBaseJl: efDtoWAscent_31; * ptmw; IMaxNegA; IMaxNegC; IMinWidthD; blsSystemFont; flEUDCState: prfntSystemTT; prfntSysEUDC; prfntDefEUDC; paprfntFaceName; aprfntQuickBuff[8]: bFilledEudcArray; ulTimeStamp; uiNumLinks; bVertical; pchKernelBase; iKernel Base; }: При виде такой сложной структуры данных можно не сомневаться в том, что \ механизм GDI делает все возможное для оптимизации вывода текста. Другие объекты GDI в механизме GDI Итак, мы рассмотрели структуры данных, представляющие основные объекты GDI в адресном пространстве ядра. В частности, были описаны структуры данных для контекста устройства, аппаратно-независимого растра, DIB-сек- ции, кисти, пера, палитры, региона, траектории, логического шрифта, физического и реализованного шрифтов. В выходных данных команды dumphmgr расширения отладчика GDI упоминаются и другие типы объектов — например, DD_DRAWJYPE, CLIOBJJTYPE и SP00LJYPE. Объекты, относящиеся к DirectDraw, описаны в следующем разделе. Другие объекты в этой главе не рассматриваются, поскольку они либо не играют особой роли для программирования Win32, либо устарели с развитием ОС Windows, либо мы не располагаем средствами для создания их экземпляров. Вскоре вы убедитесь, что знание внутренних структур данных GDI помогает глубже понять программирование для Win32 GDI API. Структуры данных DirectDraw «Дайте мне манипулятор, и я покажу вам структуру данных». Собственно, именно эта задача и решалась в данной главе применительно к объектам GDI. Мы выяснили, что в системе существует глобальная таблица объектов GDI, что GDI
232 Глава 3. Внутренние структуры данных GDI/DirectDraw создает для некоторых объектов структуры данных в адресном пространстве пользовательского режима, и для всех объектов создаются структуры данных, которые механизм GDI хранит в адресном пространстве режима ядра. При помощи расширения отладчика GDI мы постепенно исследуем недокументированные связи между GDI и DDI. А теперь перейдем к DirectDraw — API эпохи COM (Component Object Model). При создании объекта DirectDraw или поверхности DirectDraw вместо манипуляторов (скажем, HDIRECTDRAW или HDIRECTSURFACE) вам предоставляются интерфейсные указатели LPDIRECTDRAW и LPDIRECTDRAWSURFACE. Что с ними делать? С концептуальной точки зрения СОМ-интерфейс представляет собой группу семантически связанных функций, обеспечивающих доступ к объекту СОМ. На уровне реализации СОМ-интерфейс представляется таблицей виртуальных функций, содержащей адреса семантически связанных функций. Интерфейсный указатель СОМ обычно определяется как указатель на СОМ-интерфейс. На самом деле интерфейсный указатель СОМ ссылается на объект (то есть на экземпляр класса) СОМ. Рассмотрим пример создания объекта СОМ для DirectDraw: HRESULT DirectDrawTest (HWND hWnd) { LPDIRECTDRAW Ipdd; HRESULT hr = DirectDrawCreate(NULL. & Ipdd. NULL); if ( hr — DD_0K) { lpdd->SetCooperativeLevel(hWnd. DDSCL_NORMAL); DDSURFACEDESC ddsd; ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps - DDSCAPS_PRIMARYSURFACE; LPDIRECTDRAWSURFACE Ipddsprimary; hr = lpdd->CreateSurface(&ddsd. &lpddsprimary. NULL); if ( hr — DD_0K) { char mess[MAX_PATH]; wsprintf(mess. "DirectDraw object at %x. vtable at JHx\n". "DirectDraw surface object at %x. vtable at %x", Ipdd. * (unsigned *) Ipdd. Ipddsprimary. * (unsigned *) Ipddsprimary); MessageBox(NULL. mess. "DirectDrawTest". MB_0K); lpddsprimary->Release(); } lpdd->Release(); } return hr; }
Структуры данных DirectDraw 233 Приведенный фрагмент создает объект DirectDraw и объект поверхности DirectDraw, а затем выводит их адреса и указатели на таблицы виртуальных функций. Если вставить фрагмент в программу и выполнить его, на экране появляется окно сообщения с текстом следующего вида: DirectDraw object at 7e2al0. vtable at 728405a0 DirectDraw surface object at 7e3b58. vtable at 72840940 Если программа была запущена в отладчике, можно убедиться в том, что объекты создаются из кучи в пользовательском адресном пространстве, а указатели на таблицы виртуальных функций относятся к модулю реализации DirectDraw ddraw.dll. После нескольких минут поисков можно найти адреса функций в виртуальных таблицах и их символические имена. Например, фрагмент таблицы виртуальных функций объекта DirectDraw выглядит так: 7298Е8А4: ddraw.dll!DD_QueryInterface 7298ЕВ48: ddraw.dll!DD_AddRef 7298EC16: ddraw.dll!DD_Release 72980C5A: ddraw.dll!DD_Compact 7297C82B: ddraw.dll!DD_CreateClipper Уловили? Принцип использования адресов и таблиц функций в СОМ очень похож на интерфейс DDI между механизмом GDI и графическими драйверами, хотя он в значительно большей степени формализован. Теперь давайте посмотрим, как DirectDraw отражается в таблице объектов GDI. Для этого мы воспользуемся верным расширением отладчика GDI под управлением нашей собственной программы Fosterer. Дважды выполните команду dumpdd — перед выполнением приведенного выше фрагмента и когда окно сообщения находится на экране (то есть когда объекты DirectDraw еще не освобождены). Результат предугадать нетрудно — мы обнаруживаем два новых типа объектов, DDDIRECTDRAWJTYPE и DD_SURFACE_TYPE. При реализации DirectDraw в GDI все равно используются манипуляторы, хотя и скрытые интерфейсными указателями. Очевидно, DDDIRECTDRAWTYPE соответствует объекту DirectDraw, a DD_SURFACE_ TYPE — объекту поверхности DirectDraw. Начнем с рассмотрения объекта DirectDraw. Список всех объектов DirectDraw выводится командой dumpddobj DDRAW. Структура данных режима ядра расшифровывается командой dddlocal, которая выводит имя структуры — EDDDIRECTDRAWLOCAL. Механизм GDI различает глобальную структуру данных DirectDraw и структуру данных DirectDraw, существующую на уровне процесса. Ниже приведено определение структуры EDDDIRECTDRAWLOCAL. // Windows 2000. 72 байта typedef struct { HGDIOBJ hHmgr; // 000, заголовок GDI void * pentry; // 004 ULONG cExcLock; // 008 ULONG Tid; // 00c EDD_DIRECTDRAW_GLOBAL * peDirectDrawGlobal; // 010 EDDJHRECTDRAWJ3L0BAL * peDirectDrawGlobal2: // 014
234 Глава 3. Внутренние структуры данных GDI/DirectDraw EDD_SURFACE * unsigned EDD DIRECTDRAW LOCAL * FLATPTR FLONG HANDLE PEPROCESS unsigned void * unsigned peSurface Ddlist: unk_01c[2]; peDi rectDrawLocalNext; fpProcess; fl: UniqueProcess; Process; unk_038[2]; unk_040: unk_044; // 018 // 01c // 024 // 028 // 02c // 030 // 034 // 038 // 040 // 044 } EDD_DIRECTDRAW_LOCAL; В поле UniqueProcess структуры EDD_DIRECTDRAW_LOCAL хранится идентификатор процесса. Поле Process содержит указатель на объект ядра, связанный с процессом. Более того, объект DirectDraw связывается с создавшим его потоком через поле Tid (в отличие от большинства объектов GDI, у которых поле Tid обычно равно 0). Механизм GDI также поддерживает один экземпляр глобальной структуры данных EDD_DIRECTDRAW_GLOBAL, управляющей глобальной информацией состояния DirectDraw. В EDDDI RECTDRAWLOCAL указатели на эту структуру встречаются дважды. Как правило, один процесс DirectDraw создает несколько поверхностей DirectDraw. Объекты ядра этих поверхностей объединяются в односвязный список, начинающийся с поля peSurface_DdList. Все объекты DirectDraw, в данный момент существующие в системе, также связываются в список при помощи поля peDi rectDrawLocal Next. Структура EDD_DIRECTDRAW_LOCAL стоит во главе иерархии всех объектов процесса, относящихся к DirectDraw, а также содержит ссылки на другие глобальные объекты из семейства DirectDraw. Сетевая иерархия структур DirectDraw позволяет координировать их работу. Структура EDDD IRECTDRAWGLOBAL расшифровывается командой dddglobal. Ее определение выглядит следующим образом: // Windows 2000. 476 (OxlDC) байт typedef struct { HDEV unsigned SPRITE * SPRITE * SURFOBJ * unsigned FLONG ULONG unsigned SPRITESCAN void * SURFOBJ unsigned REGION * unsigned } SPRITESTATE: hdev; unk_004; pListZ; pListY; psoScreen: unk_014[9]; flOriginalSurfFlags; iOriginalType; unk_040[5]; * pRange: pRangeLimit; psoComposite; unk_060[66]; prgnUnlocked: unk_16c[28]: // 0x000 // 0x004 // 0x008 // 0x00c // 0x010 // 0x014 // 0x038 // 0x03c // 0x040 // 0x054 // 0x058 // 0x05c // 0x060 // 0x168 // 0x16c // Windows 2000. 1552 (0x610) байт
Структуры данных DirectDraw 235 typedef struct { void * DWORD DWORD unsigned LONG unsigned LONGLONG DWORD VIDEOMEMORY * DWORD DWORD * DD_HALINFO unsigned DD CALLBACKS DD SURFACECALLBACKS DD_PALETTECALLBACKS unsigned D3DNTHAL_CALLBACKS unsigned D3DNTHAL_CALLBACKS2 unsigned dhpdev; dwReservedl; dwReserved2; unk_00c[3]; cDriverReferences; unk_01c[3]; 11 AssertModeTimeout; dwNumHeaps; pvmList: dwNumFourCC; pwdFourCC; ddhalinfo; unkje0[44]; ddcallbacks: ddsurfacecallbacks; ddpalettecall backs: unk_314[48]; d3dnthalcall backs; unk_460[7]; d3dnthalcallbacks2; unk 498[18]; DD_MISCELLANEOUSCALLBACKS ddmiseel 1aneouscal1 backs unsigned D3DNTHAL_CALLBACKS3 unsigned EDD DIRECTDRAWLOCAL * EDD SURFACE * FLONG ULONG PKEVENT EDD SURFACE * EDD SURFACE * BOOL unsigned RECTL HDEV unsigned unk_4ec[18]; d3dnthalcallbacks3; unk_54c[23]; peDi rectDrawLocalLi st; peSurface LockList; fl; cSurfaceLocks; pAssertModeEvent; peSurfaceCurrent; peSurfacePrimary; bSuspended; unk_5c8[12]; rcBounds; hdev; unk_60c; // II II II II 0x000 0x004 0x008 0x00c 0x018 II 0x01c II II II II 0x028 0x030 0x034 0x038 II 0x03c II II II II II II II II II II II II II II II II II II II II II II II II II II 0x040 OxleO 0x290 0x2c4 0x304 ? 0x314 0x3d4 0x460 0x47c 0x498 0x4e0 0x4ec 0x534 0x54c 0x5a8 0x5ac 0x5b0 0x5b4 0x5b8 0x5bc 0x5c0 0x5c4 0x5c8 0x5f8 0x608 0x60c } EDD_DIRECTDRAW_GLOBAL; Структура EDDDIRECTDRAWGLOBAL содержит практически всю информацию о поддержке DirectDraw, которую должен знать механизм GDI. В поле dhpdev хранится манипулятор структуры PDEV драйвера устройства, возвращаемый при вызове DrvEnablePDEV. Обычно он представляет собой указатель на закрытую структуру данных физического устройства. В структуру EDD_DIRECTDRAW_GLOBAL включается несколько других структур, полученных механизмом GDI от драйвера экрана. Поле ddhalinfo содержит структуру DD_HALINFO, возвращаемую функцией DrvGetDirectDrawInfo и описывающую возможности оборудования и драйвера. В полях ddcall backs, ddsurfacecall- backs и ddpalettecall backs хранятся структуры DD_CALLBACKS, DD_SURFACECALLBACKS и DD_PALETTECALLBACKS, возвращаемые функцией DrvEnableDirectDraw. Другая группа
236 Глава 3. Внутренние структуры данных GDI/DirectDraw структур относится к функциям трехмерной графики DirectDraw. Они передают механизму GDI информацию о точках входа DirectDraw, поддерживаемых драйвером. Таким образом, механизм GDI знает, какие функции следует вызывать при создании поверхности, назначении цветовых ключей, отображении адресов видеопамяти, переключении поверхностей и т. д. В структуре EDDDIRECTDRAWGLOBAL хранится немало другой интересной информации — например, список объектов DirectDraw, список заблокированных поверхностей, указатель на текущую поверхность и т. д. Функция EDDDIRECTDRAWGLOBAL является частью структуры PDEVWIN32K, описанной в разделе «WinDbg и расширение отладчика GDI». Структура PDEVWIN32K также включает структуру SPRITESTATE. Разобравшись с тем, как в механизме GDI организовано хранение общих данных DirectDraw (как глобальных, так и данных уровня процесса), давайте посмотрим, что же скрывается за поверхностями DirectDraw. С каждым объектом поверхности DirectDraw связывается соответствующая структура данных, скрытая от пользователя. В выходных данных команды dumpddobj эти структуры обозначаются типом DDSURFTYPE. Команда dumpddobj SURF расширения GDI выводит все манипуляторы поверхностей DirectDraw. При вызове команды dddsurface для конкретного манипулятора поверхности выводится структура данных режима ядра EDDSURFACE. typedef struct { HGDIOBJ void * ULONG ULONG DD SURFACE LOCAL DD SURFACE MORE DD SURFACE GLOBAL DD_SURFACE_INT EDD SURFACE * EDD_SURFACE * unsigned EDD DIRECTDRAWGLOBAL * EDD DIRECTDRAWLOCAL * FLONG unsigned ULONG unsigned HANDLE unsigned HBITMAP unsigned ERECTL unsigned } EDD_SURFACE; hHmgr; pentry; cExcLock; Tid; ddsurfacelocal; ddsurfacemore; ddsurfaceglobal; ddsurfaceint; peSurface_DdNext; peSurface_LockNext; unk_0c0: r peDirectDrawGlobal; peDirectDrawLocal; fl; unkJdO: iVisRgnUniqueness; unk_0d8; hSecure; unk_0e0; hbmGdi; unk_0e8; rclLock; unk_0fc[3]; За стандартным заголовком объектов GDI // // // // // // // // // // // // // // 000. заголовок GDI 004 008 00с 010 04с 068 0Ь4 0Ь8 ОсО 0d0 0е4 Оес; Ofc режима ядра в структуре EDD_ FACE следуют четыре структуры, документированные в Windows 2000 DDK
Структуры данных DirectDraw 237 DD_SURFACE_LOCAL, DD_SURFACE_MORE, DD_SURFACE_GLOBAL и DD_SURFACE_INT. Структура DD_ SURFACE_GLOBAL содержит информацию, общую для нескольких поверхностей — шаг (pitch), высота, ширина и координаты х/у. Структура DDSURFACELOCAL содержит данные, относящиеся к конкретному объекту поверхности — первичный и вторичный буферы, цветовые ключи, формат пикселов, присоединенные поверхности и т. д. Структура DDSURFACEMORE содержит дополнительные данные уровня поверхности — такие, как сведения о видеопорте и флаги оверлеев. Последняя структура, DD_SURFACE_INT, содержит указатель на структуру DDSURFACELOCAL. За документированными структурами поверхностей DirectDraw следуют указатели на следующую поверхность в списке, глобальные и локальные данные DirectDraw. В поле hbmGdi иногда хранится манипулятор DDB. Мы знаем, как устроены некоторые структуры данных DirectDraw режима ядра; но как они используются? Обработка графических команд DirectDraw (например, переключения поверхностей) обычно начинается с интерфейсного указателя на поверхность DirectDraw. По интерфейсному указателю на поверхность определяется манипулятор DD_SURF_TYPE объекта GDI и передается механизму GDI. Механизм находит структуру EDD_SURFACE и получает указатель на структуру EDD_DIRECTDRAW_GLOBAL, в которую входит структура DDSURFACECALLBACKS. В структуре DDSURFACECALLBACKS хранится указатель на точку входа драйвера экрана, обрабатывающую переключение поверхностей и вызываемую механизмом DirectDraw. Функции переключения передается структура DDFLIPDATA, которая собирается по данным из исходной и целевой структур EDDSURFACE. За подробностями обращайтесь к описанию DdFlip в DDK. До выхода окончательной версии Windows 2000 (сборка 2195) в DirectX использовалась общая таблица объектов с GDI. Команда dumphmgr расширения отладчика GDI наряду с обычными объектами GDI перечисляет и объекты DirectX. Объектам DirectDraw соответствует внутренний идентификатор типа 0x02, а объектам поверхностей DirectDraw — 0x03. Однако в официальной версии Windows 2000 разработчики Microsoft вывели объекты DirectX «из-под ведома» диспетчера манипуляторов GDI и передали их диспетчеру манипуляторов DirectX. В расширение отладчика GDI были добавлены новые команды dumpdd и dumpdobj. Диспетчер манипуляторов DirectX управляет шестью типами объектов: удаленные объекты, объекты DirectDraw, объекты поверхностей DirectDraw, объекты устройств Direct34D, объекты видеопорта DirectDraw и объект компенсации перемещений (motion compensation) DirectDraw. Согласно данным этих новых команд, диспетчер манипуляторов DirectX поддерживает 16-килобайтную таблицу с 1024 манипуляторами DirectX — сокращенную версию 256-килобайтной таблицы, рассчитанной на 16 384 манипуляторов. Мы пока не знаем, возможно ли увеличение размеров таблицы объектов DirectX. Также в настоящее время неизвестно, отображается ли таблица объектов DirectX на адресное пространство пользовательского режима, по аналогии с таблицей объектов GDI. Несомненно, отделение объектов DirectX от объектов GDI следует считать удачным шагом, который гарантирует, что приложения DirectX не будут конфликтовать с приложениями GDI за ограниченный набор манипуляторов GDI.
238 Глава 3. Внутренние структуры данных GDI/DirectDraw Итоги В этой главе исследуются внутренние структуры данных, лежащие в основе GDI и DirectDraw. В ней досконально разобрана организация внутреннего представления служебных данных GDI и графического механизма Windows. Глава начинается с простой задачи — мы выясняем, что же представляет собой манипулятор объекта GDI. Затем мы находим в памяти таблицу объектов GDI, расшифровываем ее структуру и некоторые структуры данных пользовательского режима, поддерживаемые для конкретных типов объектов GDI. Самые важные структуры данных GDI хранятся в адресном пространстве режима ядра. Чтобы иметь возможность прочитать содержимое этих структур, мы разработали простой драйвер режима ядра, Periscope, и запустили расширение отладчика GDI под управлением нашей собственной программы. Поскольку расширение отладчика располагает информацией о внутреннем устройстве GDI, это позволяет использовать его для расшифровки структур данных GDI режима ядра. Расширение отладчика GDI помогает получить доступ к структурам данных GDI режима ядра, обычно полностью скрытых от посторонних. После прочтения этой главы вы должны гораздо нагляднее представлять, как организовано внутреннее хранение данных в GDI, какие ресурсы при этом задействованы и как выполняется аппроксимация. Кроме того, вы должны получить общее представление о том, как данные преобразуются механизмом GDI и в конечном счете передаются драйверам графических устройств (таких, как драйверы экрана и принтеров). В главе 7 описана простая утилита, разработанная на основе материала этой главы и предназначенная для получения сводной информации об использовании объектов GDI разными процессами. «Дайте мне манипулятор GDI, и я покажу вам структуру данных GDI». «Дайте мне интерфейсный указатель DirectDraw, и я покажу вам структуру данных DirectDraw». Теперь вы можете с полным правом делать подобные заявления. Примеры программ Программы главы 3 (табл. 3.14) не принадлежат к числу обычных примеров графического программирования и даже не являются обычными Windows-программами. Скорее, это системные утилиты, которые помогают анализировать внутренние структуры данных операционной системы Windows. Конечно, вы можете пользоваться ими для своих собственных целей. Таблица 3.14. Программы главы 3 Каталог проекта Описание Samples\Chapt_03\Handles Расшифровка манипуляторов GDI, поиск таблицы объектов GDI и расшифровка таблицы объектов GDI Samples\Chapt_03\QueryTab Пример обращения к таблице объектов GDI из приложения
Итоги 239 Каталог проекта Описание Samples\Chapt_03\Periscope Драйвер устройства режима ядра, позволяющий работать с данными, находящимися в адресном пространстве режима ядра, из пользовательского адресного пространства с применением файловых операций Samples\Chapt_03\TestPeriscope Пример обращения к адресному пространству ядра из приложения Samples\Chapt_03\Fosterer Программа, управляющая работой DLL расширения отладчика GDI режима ядра, — отправная точка для исследования структур данных GDI/DirectDraw режима ядра
Глава 4 Мониторинг графической системы Windows Говорят, лучше один раз увидеть, чем сто раз услышать. Если вы видите происходящее своими глазами, вам гораздо проще разобраться в сути явления. Конечно, для этого желательно выбрать подходящий инструмент. Скажем, микроскоп помогает рассмотреть мельчайших живых существ, в телескоп видны далекие светила, а телевизор сближает людей, живущих в разных частях света. Программистов, работающих в системе Windows, в первую очередь интересует, что же на самом деле происходит между их программами и операционной системой. В главе 2 была описана общая архитектура графической системы Windows, а в главе 3 основное внимание уделялось структурам данных. Но при этом осталась совершенно проигнорированной динамика миллионов вызовов, происходящих в системе. С чего начинается работа программы? Чем она заканчивается? Всегда ли все идет гладко, или в системе случаются аварии, нарушения, пробки и утечки, которые вы попросту не замечаете? В этой главе вы овладеете навыками мониторинга функций API и некоторыми инструментами, необходимыми для понимания динамики вызова функций Win32 API, особенно функций Win32 GDI/DirectDraw, служебных функций графической системы и интерфейса DDL В разделе «Отслеживание вызовов функций Win32 API» разрабатывается общая система мониторинга Win32 API, которая состоит из DLL, внедряемой в целевой процесс, и управляющей программы. В разделе «Отслеживание вызовов Win32 GDI» эта общая система расширяется для мониторинга всех вызовов GDI в процессе. Раздел «Отслеживание СОМ-интерфейсов DirectDraw» посвящен СОМ-интерфейсам, используемым в DirectDraw, а раздел «Отслеживание системных вызовов GDI» иллюстрирует методику перехвата вызовов системных функций GDI. Наконец, в разделе «Отслеживание интерфейса DDI» мы снова «погрузимся» в режим ядра и рассмотрим процесс мониторинга функций интерфейса DDL
Отслеживание вызовов функций Win32 API 241 Отслеживание вызовов функций Win32 API Методика перехвата и отслеживания не так уж редко встречается в Windows- программировании. Существует немало профессиональных и любительских программ, в которых эти приемы используются для наблюдения за мельчайшими подробностями работы системы. Самым известным инструментом, использующим методику перехвата и отслеживания API, является BoundsChecker компании Numega — профессиональный пакет для обнаружения ошибок в среде Windows. BoundsChecker позволяет находить ошибки Windows API, ошибки интерфейсов COM/OLE, ошибки памяти, ошибки указателей, утечки ресурсов и сбои программы. В частности, BoundsChecker обнаруживает неудачные вызовы функций, недопустимые значения параметров, нереализованные функции, выходы за границы блоков памяти, переполнение стека, использование неинициализированной памяти, выход индексов за границы массива, утечки памяти, утечки ресурсов и т. д. Одним из базовых приемов, используемых в работе BoundsChecker, является отслеживание вызовов тысяч функций Windows API. BoundsChecker перехватывает вызовы функций Windows API, чтобы перед вызовом функций проверить параметры и сохранить информацию о содержимом стека, а после вызова — проверить возвращаемую величину, прежде чем передать ее приложению. При запуске программы система BoundsChecker выполняет функции отладчика, что позволяет внедрять DLL этой системы в адресное пространство процесса приложения и передавать им управление. Если BoundsChecker интегрируется с компилятором, обращения к DLL BoundsChecker включаются непосредственно в программный код. Так или иначе, все вызовы функций Win32 API проходят предварительную обработку в BoundsChecker. В «Microsoft System Journal» часто публикуются статьи о применении методики перехвата и отслеживания для обеспечения функционирования колеса мыши, обнаружения операций с памятью в программах СОМ или поиска причин взаимной блокировки (deadlock) в многопоточных программах. Microsoft даже включает в Platform SDK и Windows Resource Kits специальную утилиту для отслеживания API — apimon. Перехват и отслеживание проще всего организуется в коде пользовательского режима, однако такая возможность существует и в коде режима ядра. На web- сайте www.sysinternals.com имеется несколько утилит, работа которых основана на вмешательстве в иерархию файловой системы режима ядра Windows NT или цепочки драйверов устройств для отслеживания операций с файловой системой, реестром и обращений к портам. В Windows 2000 даже компания Microsoft признала пользу перехвата функций драйверов экрана, организовав поддержку зеркальных драйверов (mirroring driver) для драйверов экрана. Вероятно, в Microsoft постоянно поступали жалобы и вопросы, почему пользователь не может легко воспроизвести экран Windows на удаленном компьютере. Теперь при помощи зеркального драйвера можно передать поток данных по сети, не вмешиваясь в работу драйвера экрана. Коммерческие утилиты, инструментарий Microsoft и примеры программ, полученные из других источников, вряд ли удовлетворят все ваши потребности по отслеживанию и перехвату API — во всяком случае, если вас интересует дейст-
242 Глава 4. Мониторинг графической системы Windows вительно удобный, настраиваемый, модульный и достаточно универсальный инструмент. Ниже перечислены лишь некоторые ограничения, с которыми вы столкнетесь. О Настройка типов данных. Готовые инструменты работают с ограниченным набором типов данных, тогда как в Windows-программировании типы данных обновляются очень часто. Желательно, чтобы утилита отслеживания умела преобразовывать коды бинарных растровых операций в имена типа SCRC0PY, сохранять растры в файлах или, скажем, сообщать о том, что манипулятор GDI соответствует объекту логического пера. О Хронометраж. Возможность измерения времени, потраченного на обработку вызова Win32 API, поможет оптимизировать программу и исключить из нее нежелательные вызовы. О Недокументированные функции API, внутримодульные вызовы, вызовы системных функций, вызовы кода режима ядра. Отсутствие поддержки этих возможностей является одной из слабостей готовых программ. Если вы хотите действительно глубоко разобраться в какой-либо области Windows-программирования (например, в графическом программировании), обойтись без хорошей программы мониторинга практически невозможно. Построение программы мониторинга Программа мониторинга обычно состоит из двух частей: управляющей программы и разведчика (DLL или драйвера). Управляющая программа засылает разведчика в нужное место, отдает ему команды и, возможно, получает информацию. Разведчик проникает «в тыл» пользовательского процесса, закрепляется в нужном месте, собирает мельчайшие обрывки информации из интересующей области, действует в соответствии с поставленной задачей или передает информацию управляющей программе. На рис. 4.1 изображена схема работы такой программы. Конечно, у этой общей модели существует немало разновидностей. Если вы найдете надежный способ внедрения разведчика, чтобы он мог действовать самостоятельно, возможно, управляющая программа вам и не понадобится. Например, некоторые среды с двухбайтовой кодировкой символов существуют «поверх» обычной системы Windows. Вместо внедрения DLL во все приложения, обладающие графическим интерфейсом, они просто переименовывают системные DLL и заменяют их собственными реализациями, обеспечивающими поддержку двухбайтовой кодировки в однобайтовой системе. Если вы хотите проследить за операциями, происходящими в адресном пространстве режима ядра, вам наверняка понадобится драйвер устройства (то есть разведывательная DLL) режима ядра. В этом случае управляющая программа устанавливает драйвер и управляет его работой. Например, в программе Fosterer из главы 3 драйвер Periscope режима ядра использовался для чтения данных из адресного пространства ядра и последующего анализа структур данных графической системы, хранящихся в режиме ядра. SoftICE/W, отладчик системного уровня от компании Numega, также использует драйвер режима ядра для обеспечения возможностей отладки общесистемного уровня на одном компьютере.
Отслеживание вызовов функций Win32 API 243 Процесс 2 Программа под наблюдением Наблюдатель DLL/Драйвер Системная DLL Рис. 4.1. Компоненты программы мониторинга При написании программы-разведчика необходимо решить несколько задач: О внедрение разведчика в процесс; О подключение к цепочкам вызовов функций API; О получение параметров, возвращаемых значений и данных хронометража; О сохранение данных в удобном формате; О создание пользовательского интерфейса для выбора программ и модулей, за которыми вы хотите наблюдать, а также перехватываемых функций Win32 API и методов СОМ. В этом разделе мы создадим программу Pogy, предназначенную для общего мониторинга вызовов Win32 API. Программа названа в честь подводной лодки, участвовавшей в подводных научных исследованиях. Мы будем использовать Pogy для исследований глубин операционной системы Windows. Пользовательский интерфейс управляющей программы Роду.ехе оформлен в виде диалогового окна, состоящего из нескольких страниц. Наблюдением занимается DLL Diver.dll. А теперь давайте кратко рассмотрим строение этой программы. Внедрение DLL-разведчика В Win32 API существует возможность установки перехватчиков (hooks) на системном уровне или на уровне программного потока. Перехватчики отслеживают сообщения или изменяют стандартные действия, выполняемые при их обработке. Установка перехватчиков выполняется функцией API SetWindowsHooksEx. В Windows 2000 количество классов перехватчиков даже увеличилось до 15. Скажем, при установке перехватчика класса WMGETMESSAGE отслеживаются сообщения, поставленные в очереди сообщений, а перехватчик класса WH_SHELL получает оповещения о создании и уничтожении окон верхнего уровня.
244 Глава 4. Мониторинг графической системы Windows Функции-перехватчики обычно реализуются в DLL — для перехватчиков системного уровня это является обязательным требованием. Причина заключается в том, что для работы перехватчика в других процессах его код должен загружаться в адресное пространство целевого процесса. Исполняемый файл может загружаться другим процессом только в виде данных, поэтому перехватчик системного уровня должен быть реализован в DLL. После загрузки DLL в адресное пространство процесса перехватчик может вытворять практически все, что захочет. На этом факте основаны некоторые приемы отслеживания вызовов API. Впрочем, вы должны позаботиться о том, чтобы DLL оказалась в нужном месте. Функция SetWindowsHookEx является лишь одним из возможных способов внедрения DLL в исследуемый процесс. Впрочем, этот способ прост и хорошо документирован. Чтобы DLL внедрялась в каждый процесс, ее можно включить в следующий ключ реестра Windows NT/2000: HKEY_LOCAL_MACHINE\Software\Microsoft\ Windows NTXCurrent Version\Windows\AppInit_DLLs Знание нетривиальных способов внедрения DLL во внешние процессы является неплохим показателем квалификации в области Windows-программирования. В классической книге Мэтта Питрека (Matt Pietrek), «Windows 95 System Programming Secrets», продемонстрирован механизм внедрения DLL через API отладчика Win32 и динамическую модификацию кода исследуемого процесса. В книге Джеффри Рихтера Qeffery Richter), «Programming Applications for Microsoft Windows» (5 издание), показано, как сделать то же самое с использованием удаленного программного потока. В нашей программе Pogy функция SetWindowsHookEx устанавливает перехватчик системного уровня, который представляет собой функцию косвенного вызова, определяемую приложением. После регистрации в системе перехватчик системного уровня вызывается при наступлении некоторых событий в системе, тогда как перехватчик уровня программного потока отвечает лишь за один поток. Функция-перехватчик ShellProc реализуется в DLL Diver.dll, как это требуется для перехватчика системного уровня. Модуль Diver экспортирует функцию SetupDiver, вызываемую из управляющей программы Роду.ехе для выполнения установки, удаления и настройки взаимодействия между компонентами. Ниже приведена часть кода перехватчика, работающая на стороне DLL-разведчика. #pragma data_seg("Shared") HWND h_Controller = NULL; HHOOK hJhellHook - NULL; #pragma data_seg() #pragma comment(1 inker. "/section:Shared,rws") LRESULT CALLBACK ShellProc( int nCode, WPARAM wParam. LPARAM 1 Param ) { if ( nCode==HSHELL_WINDOWCREATED ) if (...) StartSpyO; assert(h_ShellHook);
Отслеживание вызовов функций Win32 API 245 if (h_ShellHook) return Cal1NextHookEx(h_ShellHoQfc. nCode. wParam. lParam); else return FALSE; } void _declspec(dllexport) SetupDiver(int nOpt. HWND hWnd) { switch (nOpt) { case Diver_Insta11: assert(h_ShellHook==NULL); hJhellHook = SetWindowsHookEx(WH_SHELL. (H00KPR0C) ShellProc, hlnstance. 0); h_Controller = hWnd; break; case Diver_UnInstall: assert(h_She11Hook!=NULL); UnhookWindowsHookEx(h_ShellHook); h_ShellHook = NULL; break; } } Перехватчик системного уровня регистрируется в системе (диспетчере окон) только один раз. Функция SetWindowsHookEx возвращает манипулятор, который используется функцией-перехватчиком и по которому в итоге перехватчик удаляется вызовом UnhookWindowsHookEx. Возникает проблема: если перехватчик системного уровня может загружаться в адресные пространства разных процессов, обычно изолированные друг от друга, где же тогда хранится манипулятор? Ответ: в секции общих данных той DLL, в которой определена функция перехвата. Обычная секция данных ЕХЕ-файла Win32 является закрытой для процесса, загрузившего DLL; иначе говоря, каждый процесс работает со своей собственной копией этой секции. Однако секция общих данных совместно используется всеми процессами, загрузившими DLL. В приведенном выше фрагменте начало и конец этой секции отмечены двумя директивами dataseg, а директива comment (linker) сообщает компоновщику о том, что эта секция доступна для чтения/записи и является общей («rws»). Мы сохраняем в общей секции манипуляторы перехватчика и окна. Пожалуйста, обратите внимание на необходимость инициализации данных общей секции. Управляющая программа Pogy.exe связана с той же DLL Diver.dll. При загрузке Pogy создает окно для взаимодействия с DLL-разведчиком. Далее Pogy вызывает функцию SetupDiver(Diver_Instan,...), сообщая разведчику манипулятор своего окна и позволяя создать перехватчик. При вызове функции SetWindowsHookEx возвращается манипулятор перехватчика, необходимый для вызова следующего перехватчика в цепочке перехватов. Манипуляторы окна управляющей программы и перехватчика хранятся в DLL и поэтому доступны для всех пользовательских процессов. Таким образом, после присваивания значений h_ShellHook и hController любой процесс может обратиться к этим переменным.
246 Глава 4. Мониторинг графической системы Windows Однако к этому моменту библиотека Diver.dll загружена еще только в процесс управляющей программы. Функция перехвата вызывается лишь при создании или уничтожении окна верхнего уровня. Если это происходит в каком-то процессе, отличном от процесса управляющей программы, операционная система видит, что вызываемый перехватчик отсутствует в текущем процессе, и загружает DLL с перехватчиком. После загрузки DLL вызывается функция Shell Ргос с кодом HSHELLWINDOWCREATED. Функция Shell Ргос связывается с управляющей программой и определяет, следует ли начать отслеживание вызовов API. Главное, что требует операционная система от функции-перехватчика — чтобы она не забыла вызвать следующий перехватчик в цепочке функцией CallNextHookEx. В функции SetupDiver также предусмотрена возможность отключения перехватчика. Подключение к цепочке вызовов функций API Получив от управляющей программы приказ о начале работы, DLL-разведчик инициализируется и создает скрытое окно. Манипулятор этого окна передается управляющей программе. С этого момента управляющая программа и разведчик могут обмениваться сообщениями посредством манипуляторов окон. В операционной системе Windows для обмена простыми сообщениями с двумя 32-разрядными параметрами задействуются коды пользовательских сообщений, начинающиеся с префикса WMUSER. Но если вы захотите передать блок данных за границы процесса, обычный указатель не подойдет — указатель, относящийся к одному адресному пространству, в общем случае не работает в другом адресном пространстве. К счастью, для отправки блоков данных можно воспользоваться функцией WM_COPYDATA. Операционная система Windows специально обеспечивает правильность копирования блоков данных в сообщениях типа WM_ SETTEXT, WM_GETTEXT и WM_COPYDATA за границами процесса. Получив информацию о том, что DLL-разведчик создал коммуникационное окно, управляющая программа отправляет список отслеживаемых функций. Для каждой функции задается имя вызывающего модуля, имя вызываемого модуля, имя функции, количество параметров, типы параметров и тип возвращаемого значения. Например, если пользователь хочет отслеживать вызовы функции GDI SetTextColor из программы CLOCK.EXE, задаются следующие значения: О имя вызывающего модуля — CLOCK.EXE; О имя вызываемого модуля — GDI32.DLL; О имя функции — SetTextCol or; О количество параметров — два; О типы параметров - HDC и C0L0RREF; О тип возвращаемого значения — C0L0RREF. По полученным данным DLL строит внутреннюю таблицу отслеживаемых модулей и функций. В главе 1 кратко рассматривался формат РЕ-файлов, используемых для представления модулей Win32 (находящихся как на диске, так и в памяти). При этом упоминалось, что при статической или динамической компоновке модулей используются каталоги экспорта и импорта, с хранением адреса каждой импор-
Отслеживание вызовов функций Win32 API 247 тируемой функции во внутренней переменной. Следовательно, чтобы подключиться к цепочке вызова функции Win32 API, необходимо лишь найти в каталоге импорта модуля тот адрес, по которому хранится адрес импортируемой функции, и заменить его адресом функции-перехватчика. Конечно, чтобы программа могла нормально работать, перед заменой исходный адрес следует сохранить. При мониторинге сразу нескольких функций вы не сможете просто заменить несколько импортируемых адресов одним адресом функции-перехватчика. Функция-перехватчик по крайней мере должна знать, для какой отслеживаемой функции она вызывается. В нашей реализации для каждого элемента таблицы отслеживаемых функций создается небольшая функция-заглушка, которая заносит индекс функции в стек перед вызовом универсальной функции ProxyProlog. Таким образом, при модификации каталога импорта модуля используются адреса заглушек. Заглушки выглядят следующим образом: push index // 68 хх хх хх хх jmp ProxyProlog // Е9 уу уу уу уу Функции ProxyProlog остается лишь извлечь индекс из стека, а затем воспользоваться им при обращении к таблице функций для получения полной информации. На рис. 4.2 показано, как происходит вызов функции Win32 до и после модификации каталога импорта адресом заглушки. В левой части изображена ситуация до перехвата; значение переменной каталога импорта используется для косвенного вызова функции Win32 API. В правой части показано, что происходит после модификации. Теперь приложение осуществляет косвенный вызов заглушки, передающей управление универсальной функции ProxyProlog библиотеки Diver.dll. Функция ProxyProlog, а также сопутствующие функции и структуры данных Diver.dll отвечают за то, чтобы после обработки была вызвана исходная функция Win32 API, а затем управление было возвращено вызывающей стороне. —► Application call [ imp_SetTextColor] & SetTextColor GDI32.DLL <— 1—► Application call [stub SetTextColor] Diver.DLL push id__SetTextColor jmp ProxyProlog ProxyProlog, etc. GDI32.DLL Рис. 4.2. Перехват вызова функции API с использованием заглушки
248 Глава 4. Мониторинг графической системы Windows ПРИМЕЧАНИЕ Чтобы решение было по возможности универсальным, следует избегать модификации содержимого регистров. Если бы индекс передавался не в стеке, а в регистре, наше решение не работало бы для функций, использующих регистры для передачи параметров. Сбор информации Для тех функций, за которыми мы следим, вызов ProxyProlog предшествует вызову настоящей функции Win32 API. Однако ProxyProlog и связанные с ней функции должны выполнить очень непростую работу — собрать информацию обо всех параметрах, сохранить время входа в функцию, вызвать исходную функцию API, сохранить время возвращения из функции, сохранить возвращаемое значение и, наконец, вернуть управление вызывающей стороне. Программа- разведчик должна восстановить в прежнем виде все, к чему она прикасалась, — все регистры и флаги процессора (кроме счетчика тактов). Из-за своей сложности эта задача разделена между несколькими функциями, написанными на ассемблере, С и даже на C++ с применением виртуальных функций. О Функция ProxyProlog написана на «голом» ассемблере — в том смысле, что компилятор не должен включать в нее стандартный код входа и выхода из функции. Функция сохраняет содержимое регистров, текущее время (время 1), вызывает функцию ProxyEntry, снова сохраняет время (время 2), восстанавливает регистры и, наконец, возвращает управление исходной функции Win32 API, вызываемой приложением. О Функция ProxyEntry написана на языке С. Она создает в программном стеке структуру KRoutinelnfo, сохраняет основную информацию о вызове, вызывает виртуальную функцию C++ KFuncTable: -.FuncEntryCallBack, модифицирует стек процессора, чтобы при выходе из исходной функции Win32 API управление сначала передавалось функции ProxyEpilog, а затем снова модифицирует стек процессора, чтобы функция ProxyProlog передала управление исходной функции Win32 API. О Функция KFuncTable::FuncEntryCallBack реализована как виртуальная функция C++. В минимальной реализации она не делает ничего. Впрочем, эта функция располагает всей информацией о параметрах и времени входа-выхода, поэтому при желании она может выполнить хронометраж, сохранить параметры, проверить и даже изменить их значения. О Функция ProxyEpilog, написанная на «голом» ассемблере, вызывается сразу же после возврата из функции Win32 API. Она сохраняет регистры, сохраняет время (время 3), вызывает функцию ProxyExit, снова сохраняет время (время 4), восстанавливает регистры и, наконец, возвращает управление вызывающей стороне, тем самым завершая мониторинг одного вызова функции API. О Функция ProxyExit написана на языке С. Она извлекает из программного стека структуру KRoutinelnfo, вызывает виртуальную функцию KFuncTable::Func- ExitCallBack и модифицирует стек процессора, чтобы функция ProxyEpilog вернула управление исходной вызывающей стороне.
Отслеживание вызовов функций Win32 API 249 О Функция KFuncTable: iFuncExitCallBack реализована как виртуальная функция C++. В минимальной реализации она не делает ничего. Функция располагает всеми данными о времени входа и выхода, а также о возвращаемом значении функции API. При необходимости она может вернуть эту информацию управляющей программе. Ниже приведен код важнейших входных функций, ProxyProlog и ProxyEntry. typedef struct { unsigned m_flag; unsigned m_edx unsigned m_ecx unsigned m_ebx unsigned m_eax unsigned m_funcid; unsigned m_rtnads; unsigned m_para[32]: } EntryInfo; _declspec(naked) void ProxyProlog(void) { // funcid, rtadr. pi..pn // funcid резервирует в стеке место. // в которое позднее заносится адрес вызывающей стороны // Сохранить общие регистры и флаги asm asm asm asm asm asm asm asm push push push push pushfd _emit _emit shrd eax ebx ecx edx OxOF 0x31 eax. edx. 8 // edx. ecx. ebx. eax // 4 байта EFLAGS // Время 1 // EAX = EDX:EAX » 8 _asm push eax // Время входа _asm sub eax. OverHead _asm push eax // Время входа - затраты _asm lea eax. [esp+8] // Смещение флага в стеке _asm push eax _asm call ProxyEntry // Функция С _asm pop ecx // ecx = время входа _asm _emit OxOF // Время 2 _asm _emit 0x31 _asm shrd eax. edx. 8 // EAX = EDX:EAX » 8 _asm sub eax. ecx // Новые затраты после ProxyEntry asm add OverHead. eax // Восстановить общие регистры и флаги _asm popfd
250 Глава 4. Мониторинг графической системы Windows _asm pop edx _asm pop ecx _asm pop ebx _asm pop eax // Вернуть управление вызывающей стороне asm ret } void _stdcall ProxyEntry(EntryInfo *info. unsigned entertime) { int id = info->m_funcid; assert(pStack!=NULL); KRoutinelnfo * routine - pStack->Push(); if ( routine ) { routine->entertime - entertime; routine->funcid - id: routine->rtnaddr * info->m_rtnads; pFuncTable->FuncEntryCal1 Back(routine, info); // Модифицировать адрес возврата, чтобы перед возвращением // к исходной вызывающей стороне управление было передано // нашей функции ProxyEpilog info->m_rtnads - (unsigned) ProxyEpilog; } // Обеспечить возврат управления исходной функции // при выходе из ProxyProlog info->m_funcid - (unsigned) pFuncTable->m_func[id].f oldaddress; } Измерение времени осуществляется самым точным и эффективным способом, существующим на процессорах Intel благодаря инструкции RDTSC. Эта инструкция возвращает в регистрах EDX:EAX 64-разрядное количество тактов процессора, прошедших с момента последнего запуска. На процессоре Pentium 200 МГц один такт занимает 5 не. Работать с 64-разрядными величинами неудобно, поэтому программа сдвигает пару EDX:EAX на 8 разрядов вправо и использует только младшее 32-разрядное значение. Минимальный интервал времени увеличивается до 5 х 28 - 1280 не, что все равно гораздо лучше миллисекундной точности, обеспечиваемой функцией GetTickCount. При точности в 1,28 мкс 32-разрядная величина способна представить интервал длительностью до 1,58 часа; для обычного тестирования этого вполне достаточно. Для одного вызова API программа читает счетчик тактов 4 раза: перед вызовом ProxyEntry, перед вызовом перехватываемой функции API, перед вызовом ProxyExit и перед возвратом управления вызывающей стороне. Интервал между точками 1 и 2 приближенно определяет затраты на вход в функцию; интервал между точками 2 и 3 определяет истинные затраты на вызов функции Win32 API; наконец, интервал между точками 3 и 4 определяет затраты на выход из
Отслеживание вызовов функций Win32 API 251 функции. Программа поддерживает глобальную переменную OverHead, в которой суммируются все непроизводительные затраты, и вычитает ее значение из данных хронометража. Стек, используемый для передачи параметров и адреса возврата, растет в направлении нижних адресов; при сохранении нового значения указатель стека уменьшается, а при извлечении — увеличивается. После блока параметров следует адрес возврата. При вызове функция-заглушка заносит в стек идентификатор (индекс) функции, после чего вызывает ProxyProlog. Функция ProxyProlog включает в стек несколько стандартных регистров и копию регистра флагов процессора. Все эти значения отображаются на структуру Entry Info уровня С, указатель на которую передается ProxyEntry. Функция ProxyEntry использует указатель на Entry Info для получения идентификатора функции и модификации адресов возврата в стеке. Дальше происходит самое интересное. После вызова ProxyEntry функция Proxy- Prolog восстанавливает общие регистры и регистр флагов, после чего выполняет инструкцию ret. Куда при этом возвращается управление? Когда-то на вершине стека процессора находился индекс функции, занесенный туда заглушкой, но позднее функция ProxyEntry записывает на это место адрес исходной функции Win32 API. Следовательно, последняя инструкция ret в ProxyProlog фактически возвращает управление исходной реализации API. Например, если мы включаемся в цепочку перехвата функции GDI DeleteObject, код заглушки заносит в стек индекс функции (например, 5) и вызывает ProxyProlog. Функция ProxyProlog вызывает функцию ProxyEntry, чтобы та сохранила параметры и записала на место индекса адрес GDI-реализации DeleteObject. Таким образом, последняя инструкция ProxyProlog передает управление функции GDI DeleteObject. Выходная часть представляет собой зеркальное отражение входной части. Функции ProxyEpilog и ProxyExit приведены ниже для полноты картины. typedef struct { unsigned m_rslt; } Exitlnfo; declspec(naked) void ProxyEpilog(void) I _asm push eax // Результат вызова функции API. // Также резервирует место // для адреса возврата __asm push eax // Сохранить общие регистры __asm push ebx __asm push ecx __asm push edx __asm pushfd // 4 байта флагов __asm _emit OxOF // Время 3 _asm _emit 0x31 asm shrd eax, edx, 8 // EAX » EDX:EAX » 8 asm push eax // Время выхода asm sub eax, OverHead
252 Глава 4. Мониторинг графической системы Windows _asm push eax // Время выхода - затраты _asm lea eax. [esp+28] // Адрес зарезервированного участка _asm push eax _asm call ProxyExit _asm pop ecx // ecx = время выхода _asm _emit OxOF // Время 4 _asm _emit 0x31 _asm shrd eax. edx. 8 // EAX - EDXrEAX » 8 _asm sub eax. ecx // Новые затраты после ProxyEpilog _asm add OverHead. eax _asm popfd // Восстановить флаги и регистры _asm pop edx _asm pop ecx _asm pop ebx _asm pop eax _asm ret // Вернуть управление // исходной вызывающей стороне void _stdcall ProxyExit(ExitInfo *info. unsigned leavetime) { int depth; assert(pStack); KRoutinelnfo * routine = pStack->Lookup(depth); if ( routine ) { pFuncTable->FuncExitCa"ll Back (routine, info, leavetime. depth): info->m_rslt = routine->rtnaddr; pStack->Pop(); При выходе из перехватываемой функции Win32 API управление не возвращается непосредственно вызывающей стороне. Вместо этого вызывается наша функция ProxyEpilog. Дело в том, что функция ProxyProlog изменяет адрес возврата в стеке так, чтобы он указывал на ProxyEpilog (посредством простого присваивания info->m_rtnads = (unsigned)ProxyEpilog). Мы предусмотрительно сохранили этот адрес возврата в программном стеке для последующего использования. Теперь особое внимание уделяется регистру ЕАХ; в нем хранится скалярное возвращаемое значение функции (например, манипулятор GDI, возвращаемый функцией CreateSolidPen). Функция ProxyEpilog сохраняет его в стеке и передает информацию ProxyExit в виде указателя на структуру Exitlnfo. Структура Exitlnfo состоит из единственного поля, в котором хранится возвращаемое значение функции. Функция ProxyExit находит структуру KRoutinelnfo в программном стеке, вызывает функцию KFuncTable: :FuncExitCallback, а затем заносит на место возвращаемо-
Отслеживание вызовов функций Win32 API 253 го значения в стеке адрес возврата, который используется функцией ProxyEpilog для передачи управления исходной вызывающей стороне посредством функции ret. На рис. 4.3 изображен процесс перехвата функции API вместе со всеми изменениями, происходящими в стеке процессора. В нижней части показана передача управления от приложения к заглушке, функции ProxyProlog, функции Win32 API, ProxyEpilog и обратно к приложению (функции ProxyProlog и ProxyEpilog являются вспомогательными). В верхней части рисунка показаны изменения в стеке. Стек Параметр ret addr Параметр ret addr funcid Параметр ProxyEpilog & gdi32!func Параметр ProxyEpilog —► Приложение Передача упрг —► шлек Функция- заглушка 1ИЯ —► ProxyProlog ProxyEntry ж —► GDI32.DLL —► ProxyEpilog j А т ProxyExit FuncEntryCall Back FuncExitCall Back Рис. 4.З. Передача управления и изменения в стеке при перехвате функций API И последнее, о чем следует упомянуть, — устройство программного стека. Для каждого вызова функции API программа должна создать структуру KRoutinelnfo с информацией о вызове функции, используемую как входной, так и выходной частью. При вызове функции API в стек заносится одна новая структура, а при завершении обработки вызова API последняя запись выталкивается из стека. Все замечательно... если только процесс не состоит из нескольких программных потоков. Рассмотрим следующую ситуацию: первый поток вызывает функцию API и блокируется в ожидании какого-то ресурса; затем второй поток вызывает функцию API и тоже блокируется. Теперь первый поток «просыпается» и завершает обработку функции API. В этом случае программный стек перестает соответствовать принципу LIFO («последним пришел, первым вышел»). Этот принцип действительно соблюдается только на уровне программного потока. Обратите внимание: стек процессора, используемый при обработке вызовов Win32 API, полностью соответствует принципу LIFO, поскольку каждый программный поток работает с отдельным стеком. В нашей реализации программного стека проблема решается благодаря пометке каждой структуры идентифи-
254 Глава 4. Мониторинг графической системы Windows катором текущего потока, а операции занесения и извлечения из программного стека приходится координировать на уровне потока. Для защиты стека от модификаций применяется критическая секция. Вывод данных Итак, рассмотренные нами функции собирают всевозможную информацию о вызовах функций API. Преобразование «сырых» данных в более осмысленную и удобную форму также является одной из задач DLL-разведчика. Конечно, данные можно сохранять в разных форматах, однако простой текстовый формат проще всего генерируется и читается. Вероятно, обработку больших объемов накопленных данных удобнее проводить в электронных таблицах или базах данных. Такие программы, как Microsoft Excel, Lotus 123 или Microsoft Access, легко преобразуют правильно отформатированные текстовые файлы в свой рабочий формат. Все, что от вас потребуется, — обеспечить последовательное разделение столбцов в текстовых файлах либо по фиксированной ширине, либо при помощи символов табуляции, двоеточий, запятых и других служебных символов. Например, программа SysCall из главы 2 генерирует списки системных функций GDI, вызываемых из GDI32.DLL. Однако список упорядочивается в соответствии с порядком символических имен в отладочных файлах, а не по идентификаторам системных функций или адресам вызывающих функций. Вы можете создать таблицу в Microsoft Excel, импортировать в нее текстовый файл, сгенерированный SysCall, с разделением столбцов по фиксированной ширине, а затем настроить ширину и типы столбцов. В результате вы получаете электронную таблицу Excel с удобными средствами сортировки и анализа данных. Наша разведывательная DLL выводит данные в текстовый файл, разделяя поля запятыми. Файлам присваиваются имена с последовательной нумерацией pogy0000.txt, pogy0001.txt и т. д. Программный код создания файла находит следующий свободный номер в последовательности, чтобы предотвратить стирание старых файлов. В простейшем случае вывод данных организуется просто. Параметры функций Win32 API обычно состоят из 4 байт; такой же размер имеет возвращаемое значение скалярной функции, передаваемое в регистре ЕАХ. Самое «тупое» решение — выводить все значения в виде 8 шестнадцатеричных цифр. Таким образом, TRUE будет выводиться в виде «0x00000001», FALSE — в виде «0x00000000», код растровой операции SRCC0PY — в виде «0х00СС0020», а для текстовой строки будет выводиться только адрес. В общем, для хакера сойдет, но для простых пользователей очень неудобно. В Win32 API определяется очень богатый ассортимент типов (или по крайней мере макросов типов). Мы работаем со знаковыми и беззнаковыми числами разного размера, всевозможными указателями, бесчисленными манипуляторами и типами высокого уровня, например BITMAPINF0, L0GF0NT, DEVM0DE и т. д. Архитектура DLL-разведчика позволяет вам выбрать специальную интерпретацию для каждого из этих типов. Для каждой функции Win32 API типы параметров и возвращаемых значений также могут задаваться по именам. Значения, относящиеся к одному типу, расшифровываются одинаково. Вы можете настроить
Отслеживание вызовов функций Win32 API 255 процесс преобразования «сырых» данных в текстовый формат и добавлять поддержку новых типов данных при помощи подключаемых DLL. Чтобы упростить работу с сотнями имен типов, функций и модулей, мы воспользуемся таблицей атомов и преобразуем имена из текстового формата в целочисленные индексы. Например, вместо имени COLORREF программа передает целочисленный атом COLORREF, значение которого получается на стадии инициализации при включении строки COLORREF в таблицу атомов. Все компоненты системы работают с одной таблицей атомов, поэтому если другой компонент вдруг захочет снова включить COLORREF в таблицу атомов, повторного включения не произойдет; вместо этого будет возвращено исходное целочисленное значение. Происходящее очень похоже на API работы с атомами в Win32. Программа реализует таблицу атомов без использования функций атомов Win32 API по соображениям быстродействия и переносимости. Таблица атомов преобразуется в базовый класс C++ IAtomTable, который сильно напоминает интерфейс СОМ (правда, в данном случае интерфейс IUnknown нам не нужен): struct IAtomTable { virtual ATOM AddAtom(const char * name) = 0: virtual const char * GetAtomName(ATOM atom) = 0; }: Наряду с таблицей атомов также определяется базовый класс C++ IDecoder, преобразующий некоторые типы данных в текстовый формат: struct IDecoder { virtual bool Initialize(IAtomTable * pAtomTable) = 0; virtial int DecodeCATOM typ. const void * pValue. char * szBuffer. int nBufferSize) = 0; }: В этом объявлении ключевое слово struct эквивалентно class, за исключением того, что все определяемые типы и функции являются открытыми (public). Ключевое слово COM interface определяется как struct в файле basetyps.h. Метод IDecoder: .-Initialize включает в таблицу атомов имена типов данных. Метод IDecoder:: Decode расшифровывает блок данных в текстовый буфер и возвращает размер задействованных данных. Такая архитектура позволяет работать с блоками данных вместо отдельных 4-байтовых значений, что бывает очень удобно при расшифровке параметров, которые не поддаются осмысленной расшифровке по отдельности. Например, для функции ExtTextOut в двух последних параметрах передается количество символов и указатель на целочисленный массив. Не зная количества символов, декодер не сможет определить, сколько элементов в массиве он должен расшифровать. Если класс IDecoder определяется так, как показано выше, вы можете определить новый тип массива CountedlntArray и передать методу IDecoder::Decode два 32-разрядных значения для этого массива. Метод IDecoder:-.Decode возвращает количество задействованных байт или О, если данные не были обработаны.
256 Глава 4. Мониторинг графической системы Windows DLL-разведчик содержит базовый декодер (класс KBasicDecoder) для простой расшифровки стандартных типов данных Win32. Ниже приведен небольшой фрагмент этого класса. ATOM atom_char; ATOM atom_BYTE; ATOM atom_C0L0RREF; boo! KBasicDecoder::InitializeCIAtomTable * pAtomTable) { if ( pAtomTable==NULL ) return false: atom_char - pAtomTable->AddAtom("char"); atom_BYTE = pAtomTable->AddAtomCBYTE"); atom_C0L0RREF = pAtomTable->AddAtom("COLORREF"): return true: } int KBasicDecoder::Decode(ATOM typ. const void * pValue. char * szBuffer. int nBufferSize) { unsigned data = * (unsigned *) pValue: if ( typ==atom_char ) wsprintf(szBuffer, "'%c'", data): return 4: f ( typ==atom_BYTE ) wsprintf(szBuffer. "*d\ data & OxFF): return 4: if ( typ==atom_COLORREF ) if ( data==0 ) strcpy(szBuffer. "BLACK"); else if ( data==OxFFFFFF ) strcpy(szBuffer. "WHITE"): else wsprintf(szBuffer. "ЯОбх". data): return 4: } return 0: // Необработанные типы } На стадии инициализации DLL-разведчик создает таблицу атомов, инициализирует экземпляр KBasicDecoder, загружает ini-файл с информацией о специальных настройках IDecoder, загружает и инициализирует каждую из них.
Отслеживание вызовов функций Win32 API 257 Статическая функция MainDecoder управляет всем процессом расшифровки блока данных. Она проходит по цепочке реализаций I Decoder и находит ту, которая позволяет расшифровать определенные типы данных. Реализации KFuncTable:: FuncEntryCallBack и KFuncTable::FuncExitCal 1 Back просто вызывают MainDecoder. Итак, в нашем распоряжении имеется расширяемый декодер для расшифровки типов данных Win32. Как видите, знакомство с архитектурой расширения отладчика WinDbg нас кое-чему научило. Управляющая программа Мы разобрались с процессом внедрения DLL-разведчика, перехватом функций Win32 API, сбором информации и выводом данных... Чего еще не хватает в нашем решении? Очевидно, управляющей программы, при помощи которой выбираются атакуемые программы, отслеживаемые модули и функции, а также точное определение Win32 API. Конфигурация управляющей программы Pogy определяется несколькими стандартными ini-файлами Windows. Эти файлы хранятся в текстовом формате, их структура понятна без лишних объяснений, а в Win32 API предусмотрены средства для их обработки. Управляющая программа является приложением Win32, поэтому ничто не мешает нам использовать все имеющиеся возможности Win32. Главный файл данных, Pogy.ini, состоит из двух секций. В секции Target перечисляются приложения, за которыми вы хотите следить, с указанием конфигурационных файлов для каждого приложения. В секции Option хранятся общие параметры работы программы (например, флаги регистрации вызовов API и отображения информации о вызовах в окне). Здесь же указываются DLL для расшифровки дополнительных типов данных. Пример файла Pogy.ini: [Target] KL0CK.EXE (pclock.ini) 2=N0TEPAD.EXE (pnotepad.ini) [Notepad] LogCall=l DispCall=0 Decoderl=pogygdi.dll!_Create_GDI_Decoder@0 Decoder2=pogygdi.dll!_Create_DDRAW_Decoder В соответствии с этим ini-файлом мы хотим регистрировать вызовы API, но без отображения информации о них. К программе подключаются два декодера для дополнительных типов данных: один предназначен для типов GDI, а другой — для типов, относящихся к DirectDraw. Пользователь может отслеживать работу одной из двух программ, для каждой из которых существует отдельный ini-файл. В ini-файле уровня приложения перечисляются модули прикладного процесса, за которыми вы собираетесь следить. Пользователь должен указать имя вызывающего модуля, имя вызываемого модуля и имя ini-файла для группы функций API. Пример: [Module] CL0CK.EXE. Gdi32.DLL. wingdi CL0CK.EXE. User32.DLL. winuser
258 Глава 4. Мониторинг графической системы Windows Это означает, что нас интересуют обращения к GDI32.DLL и USER32.DLL; им соответствуют отдельные ini-файлы wingdi.ini и winuser.ini. Имейте в виду, что ini-файлы групп функций API играют в нашей программе такую же роль, как заголовочные файлы Windows в компиляторе C/C++; другими словами, они содержат описание API, используемое программой во время мониторинга. Конечно, очень хотелось бы изобрести автоматизированный способ построения этих ini-файлов по содержимому заголовочных файлов Windows, библиотечных файлов или каких-нибудь файлов с символическими именами... Но пока не будем отвлекаться и просто введем вручную всю информацию — имя модуля, имя функции, список типов параметров и тип возвращаемого значения. Ниже приведен небольшой фрагмент файла для GDI API. [wingdi] int SelectClipRgnCHDC. HRGN) int SetR0P2(HDC.int) BOOL SetWindowExtEx(HDC. int. int. LPSIZE) BOOL SetBrushOrgEx(HDC. int. int. LPPOINT) BOOL LPtoDP(HDC. LPPPOINT. int) HBRUSH CreateBrushlndi rect(LPLOGBRUSH) HBRUSH CreateDIBPatternBrushPt(LPVOID. UINT) BOOL DeleteDC(HDC) HBITMAP CreateBitmap(int. int. UINT. UINT. LPVOID) HDC CreateCompatibleDC(HDC) HBRUSH CreateSolidBrush(COLORREF) HRGN CreateRectRgnlndi rect(LPRECT) INT SetBoundsRectCHDC. LPRECT. UINT) BOOL PatBltCHDC. int. int. int. int. DWORD) BOOL SetViewportOrgEx(HDC. int. int. LPPOINT) BOOL SetWindowOrgEx(HDC. int. int. LPPOINT) int SetMapModeCHDC. int) Пользовательский интерфейс управляющей программы Pogy представляет собой диалоговое окно, состоящее из трех страниц-вкладок. На странице Events регистрируются такие события, как создание и уничтожение окон, перехват вызовов функций API DLL-разведчиком, а также выводится подробная информация о вызовах API (если в ini-файле установлен соответствующий флаг). На странице Setup устанавливаются флаги регистрации данных. На странице API выводятся данные, прочитанные программой из ini-файлов. Здесь же выбирается приложение, за которым вы собираетесь наблюдать (из перечисленных в Pogy.ini). В таблице выводится информация о загруженных описаниях функций API. Страница API управляющей программы изображена на рис. 4.4. После запуска программа Pogy устанавливает общесистемный перехватчик, реализованный в Diver.dll. При создании или уничтожении окна верхнего уровня любого приложения DLL-разведчик загружается в его адресное пространство. Diver.dll получает имя главного исполняемого файла приложения и отправляет Pogy сообщение, чтобы узнать, нужно ли следить за данным процессом. Если приказ будет отдан, DLL-разведчик создает скрытое окно для получения информации об отслеживаемых функциях, включается в цепочку перехвата заданных функций и начинает записывать полученную информацию в текстовый файл. Наблюдение прекращается с завершением целевого приложения.
Отслеживание вызовов функций Win32 API 259 ICLODCEXE »', 1 Class-'-'---- lift 6di32.dll ;j£|Gdi32.dll :;|Й 6di32.dll iifiGdi32.dll ijUGdi32.dll iii|Gdi32.dll iiflGdi32.dll Ш Gdi32.dll |:|9 Gdl32.dll |j£| Gdi32.dll jjnterftsce . Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll Gdi32.dll ЛъА* - AddFontResourceA AddFontResourceW AnimatePalette Arc BitBU CancelDC Chord ChoosePixelFormat CloseMetaFile CombineRgn zzg jff •<4>f* m Рис. 4.4. Пользовательский интерфейс программы мониторинга Win32 API После всех потраченных усилий нас ожидает награда — протокол вызовов функций Win32 API, сгенерированный DLL-разведчиком под управлением главной программы. На рис. 4.5 изображен один из таких файлов, импортированный в Microsoft Excel. Depth: Enter i; 183,268 00 : 1: 183,486.00: 1! 192,405.00 : 1: 192,482.00 i 1: 192.552.00i 1; 192,608.00 ! i 11 192,655.00 j 1 i 762,636.00 ; 1! 762,649.00 j 1: 762,663.00 : 1; 762,712.00": 1! 762,754.00 i t 1! 762,758.00 Leave 183,262.00 183,496.00 192,409.00 192,488.00 192,606.00 192,653.00 762,630.00 762,648.00 762,658.00 762,710.00 762,753.00 762,755.00 791,667.00 Return 2 TRUE 2 TRUE 1b0a0132 18a0021 TRUE TRUE 1b0a0132 TRUE 1c0a0132 18a0021 TRUE Caller ; CLOCK EXE+16bd i CLOCK. EXE+166 : CLOCK. EXE+16bd i CLOCK. EXE+1615 : CLOCK. EXE+355b ; CLOCK. EXE+3571 i CLOCK.EXE+3589 ; CLOCK. ЕХЕ+35Ы i CLOCK. EXE+35bd : CLOCK.EXE+3616 : CLOCK EXE+3626 i CLOCK.EXE+3637 : CLOCK.EXE+365a : Gdi32. : Gdi32 ; Gdi32. i Gdi32. ! Gdi32. i Gdi32. i Gdi32. : Gdi32. ! Gdi32. : Gdi32. : Gdi32. : Gdi32 : Gdi32 Calee dlllSetBkMode diiibeleteObject dlllSetBkMode dp'ejeteObject"' dlHCreateFontindirectW dliiSeiectObj'ect dlilGe'tTextExtentPointW .^petTexfExtentPointW [ dlllSeiectbbject dinpeleteObject diiiCreateFontindiVectW difiSe{ectObject dlllGetf extExtentExPointW I Parameter 1 1010056 60i004de 1010058 II "6l'l004de LdGF6lMTW*(135a3c0l 1010058 1 1010058 1010058 1010058 1b0a0132 'LOGFONt^(135a3cO| 1010058 Рис. 4.5. Протокол вызовов API, импортированный в Excel Для каждого вызова функции API, соответствующего одной строке на рис. 4.5, указывается уровень вложенности (пока — только 1), время входа и выхода из
260 Глава 4. Мониторинг графической системы Windows функции, возвращаемое значение, адрес вызывающей стороны и имя вызываемой функции, а также все передаваемые параметры. Одни данные выводятся в десятичном виде, другие — в шестнадцатеричной системе, а третьи — в виде мнемонических обозначений. Пока наша программа-разведчик запрограммирована только на регистрацию вызовов API. Можно добавить в нее код, который бы обеспечивал проверку параметров, проверку результата вызова и даже обнаруживал утечки памяти/ ресурсов. Для проверки параметров необходимо знать область допустимых значений каждого параметра. Например, функция SelectObject выбирает в контексте устройства действительный манипулятор объекта GDI — либо стандартного, либо созданного текущим процессом. Попытка выбрать недействительный манипулятор GDI или манипулятор, принадлежащий другому процессу, является тревожным признаком (особенно в Windows NT/2000). По тому же принципу проверяется и результат функции. Например, если функция SelectObject возвращает действительный манипулятор GDI, это является признаком ошибки при исключении объекта GDI из контекста, возможной причины утечки объектов GDI. Впрочем, обнаружение утечки объектов GDI — задача более сложная. Вам придется регистрировать все вызовы функций, создающих объекты, вместе со значением манипулятора и адресом вызывающей стороны. При удалении объекта GDI (функцией DeleteObject) его манипулятор исключается из списка сохраненных манипуляторов. При завершении программы манипуляторы, оставшиеся в вашем списке, принадлежат «потерянным» объектам GDI; программа должна вывести точную информацию об их создателях. Как обычно, отладочные файлы символических имен помогут преобразовать адреса в более содержательные имена. Отслеживание вызовов Win32 GDI В любой области Windows-программирования действует общее правило: справиться с работой легко, а выполнить ее блестяще — трудно. Конечно, наша программа мониторинга Win32 API приносит реальную пользу, но и она оставляет желать лучшего. Чтобы добиться главной цели — понимания всех аспектов работы GDI, — нам предстоит еще изрядно потрудиться. В действительности мы хотим ориентировать программу на мониторинг функций GDI и DirectDraw, которым, собственно, и посвящена эта книга. Это позволит нам использовать функции Win32 API, реализованные в KERNEL32.DLL и USER32.DLL, не беспокоясь о том, что они сами могут быть предметом мониторинга. Файл определения GDI API Прежде всего нам понадобится полный или почти полный ini-файл, который может читаться DLL-разведчиком и который описывает как можно большее количество функций GDI API.
Отслеживание вызовов Win32 GDI 261 Вся необходимая информация присутствует в заголовочных файлах Windows вместе с информацией, которая нас совершенно не интересует. Конечно, нам хотелось бы иметь автоматизированные средства для выборки из заголовочных файлов основных прототипов функций и приведения их к упрощенному формату. Простой, но специализированный анализатор заголовочных файлов С — неплохая тема курсовой работы для студентов, изучающих построение компиляторов. В результате получилась небольшая консольная Windows-программа, Skimmer, которая ищет в файлах ключевые макросы типа WINGDIAPI, WINUSERAPI, WINAPI и APIENTRY, стандартные признаки прототипов функций Win32 API. Убедившись в том, что найден действительно прототип функции, программа удаляет излишества типа CONST, FAR, IN и OUT, а также имена параметров. Остается лишь лаконичное определение функции Win32 API. Документированные функции, экспортируемые модулем GDI32.DLL, определяются в трех заголовочных файлах Windows 2000 DDK: inc\wingdi.h (стандартные функции GDI), inc\winddi.h (функции DDI пользовательского режима) и sec\ print\genprint\winppi.h (функции GDI для поддержки процессора печати EMF). В поставку Visual C++ включается только файл include\wingdi.h. Обработав эти три заголовочных файла программой Skimmer, мы получаем три ini-файла, практически готовые к использованию программой Pogy. Единственным исключением является функция EngGetFilePath DDI; ее придется слегка подправить вручную. Какой-то умник воспользовался при объявлении второго параметра записью WCHAR(*pDest)[MAX_PATH+l]; нашему простому анализатору это не по силам. Ниже приведен наименьший из трех ini-файлов, содержащий определения GDI API для процессора печати EMF. [winppi] HANDLE Gdi GetSpoolFi1eHandle(LPWSTR.LPDEVMODEW.LPWSTR) BOOL GdiDeleteSpoolFileHandle(HANDLE) DWORD Gdi GetPageCount(HANDLE) HOC GdiGetDC(HANDLE) HANDLE GdiGetPageHandle(HANDLE.DWORD.LPDWORD) BOOL GdiStartDocEMFCHANDLE.DOCINFOW*) BOOL Gdi PIayPageEMF(HANDLE,HANDLE,RECT*,RECT*,RECT*) BOOL Gdi EndPageEMF(HANDLE.DWORD) BOOL GdiEndDocEMF(HANDLE) BOOL Gdi GetDevmodeForPage(HANDLE.DWORD.PDEVMODEW*.PDEVMODEW*) BOOL Gdi ResetDCEMF(HANDLE.PDEVMODEW) [Types] HANDLE LPWSTR LPDEVMODEW BOOL DWORD HDC LPDWORD %v, D0CINF0W* RECT* PDEVMODEW* PDEVMODEW
262 Глава 4. Мониторинг графической системы Windows Как видно из приведенного листинга, файл определения API состоит из двух секций. В первой секции перечисляются упрощенные прототипы функций, а во второй — уникальные типы данных, используемые этими функциями. Для создания условий, в которых происходит большая часть графического вывода, Win32 GDI пользуется услугами модуля управления окнами (USER32.DLL). Модуль USER32 содержит ряд интересных функций API, за которыми тоже было бы полезно проследить, — BeginPaint, EndPaint, GetDC и т. д. Исходя из этого, мы воспользуемся программой Skimmer и сгенерируем файл winuser.ini из файла winuser.h. Декодер данных GDI Программа Skimmer перечисляет все типы данных, задействованные в некоторой группе функций API; эта информация используется DLL-разведчиком. Чтобы сохраненные данные лучше читались, нам понадобится специальный декодер для работы со специфическими типами данных GDI — такими, как HGDIOBJ, L0GF0NTW и даже DEVMODEW, если эта информация кого-нибудь заинтересует. Благодаря знанию структур данных GDI, полученному из главы 3, задача построения DLL декодера данных GDI сводится к обычному кодированию. Ниже приведена базовая структура DLL декодера GDI. class KGDIDecoder : public IDecoder { ATOM atom_HGDIOBJ; public: KGDIDecoderO { pNextDecoder - NULL; } virtual bool InitializedAtomTable * pAtomTable); virtual int Decode(ATOM typ. const void * pValue. char * szBuffer, int nBufferSize); }: bool KGDIDecoder:: Initial izedAtomTable * pAtomTable) { if ( pAtomTable==NULL ) return false: atom_HGDIOBJ = pAtomTable->AddAtom("HGDIOBJ"): return true: } // Поиск типов объектов GDI int KGDIDecoder::Decode(ATOM typ, const void * pValue. char * szBuffer. int nBufferSize) {
Отслеживание вызовов Win32 GDI 263 unsigned data = * (unsigned *) pValue; if ( (typ==atom_HDC) || (typ==atom_HGDIOBJ) || (typ—atomJPEN) || (typ==atom_HBRUSH) jj (typ==atomJPALETTE) jj (typ~=atom_HRGN) jj (typ—atomJFONT) ) { TCHAR temp[32]; unsigned objtyp - (data » 16) & OxFF; if ( ! Lookup( objtyp & 0x7F. Dic_GdiObjectType, temp) ) _tcscpy(temp, "HGDIOBJ"); if ( objtyp & 0x80 ) // Стандартный объект wsprintf(szBuffer, ,,(S*s)*x". temp, data & OxFFFF); else wsprintf(szBuffer. "(*s)*x\ temp, data & OxFFFF); return 4; } if ( typ==atom_PLOGFONTW ) { LOGFONTW * pLogFont - (LOGFONTW *) data; if ( ! IsBadReadPtr(pl_ogFont. sizeof(LOGFONTW)) ) { wsprintf(szBuffer. "& LOGFONTW{Sd.Sd Sws}". pLogFont->1fHeight. pLogFont->1fWidth. pLogFont->lfFaceName); return 4; } } // Необработанные типы return 0; } KGDIDecoder GDIDecoder; extern "C" declspecCdllexport) IDecoder * WINAPI Create_GDI_Decoder(void) { return & GDIDecoder; } Класс KGDIDecoder представляет собой простой декодер для типов данных GDI, созданный на основе базового класса IDecoder (по аналогии с реализацией СОМ- интерфейсов в классах СОМ). Функция Create_GDIJDecoder возвращает указатель на глобальный экземпляр KGDIDecoder (примерно то же самое делает фабрика класса для класса СОМ). Разведчик загружает DLL декодера GDI во время
264 Глава 4. Мониторинг графической системы Windows работы, получает адрес функции-создателя, вызывает ее, а затем инициализирует декодер методом KGDIDecoder->Initialize. Затем новые декодеры подключаются поверх старых и последовательно вызываются для преобразования полученных данных в текстовый формат до тех пор, пока запрос не будет обработан. Как было показано в главе 3, манипулятор объекта GDI в Windows NT/2000 состоит из трех частей: 8-разрядного признака уникальности, 8-разрядного идентификатора типа и 16-разрядного индекса. В нашей программе эта информация используется для выделения из манипулятора имени типа и индекса. Для указателей на структуру L0GF0NTW программа выводит данные логического шрифта: высоту, ширину и название гарнитуры. Перевод DLL-разведчика на новый декодер заметно улучшает качество выходных данных. Каждый манипулятор объекта GDI теперь снабжается пометкой типа. Теперь в выходных данных четко прослеживается последовательность действий: приложение создает объект GDI, выбирает его, использует, затем исключает из контекста и, наконец, удаляет. Реализация новых возможностей декодера позволит внести новые усовершенствования в процесс мониторинга API. Полный мониторинг API До настоящего момента мы подключались к цепочке обработки вызовов API, модифицируя каталог импорта модуля. Таким образом, при модификации каталога импорта модуля CLOCK.EXE для мониторинга функции GDI SelectObject перехватываются все вызовы, исходящие из этого модуля (если программа не додумается до косвенного вызова SelectObject с использованием GetProcAddress). Однако многие компоненты окон (например, заголовок, название, меню и значки) не прорисовываются непосредственно вашей программой — подобные графические задачи решаются стандартной функцией окна заранее определенным способом. В программах MFC, использующих DLL-версию библиотеки, многие графические функции GDI вызываются из MFC DLL (например, MFC42.DLL или MFC42D.DLL для MFC версии 4.2). Если вы хотите отслеживать графические вызовы из всех этих модулей посредством модификации каталога импорта, вам придется перечислить их в ini- файле отслеживаемой программы. В этом случае DLL-разведчику придется перебирать все модули и править их каталоги импорта. Но даже перечисление всех модулей процесса еще не гарантирует полного успеха. Во время работы программы могут загружаться новые модули (например, COM DLL), о которых вы не знали заранее. А если этого недостаточно, учтите, что при вызове из GDI32 экспортируемых функций GDI32 каталог импорта вообще не используется. В этом случае происходит прямой внутримодульный (in- tramodule) вызов; конструкции типа call [ impSelectObj] в нем не участвуют. Для полного мониторинга вызовов API на уровне процесса (то есть перехвата всех обращений изнутри и извне модуля, в котором находится реализация; из модулей уже загруженных и тех, которые будут загружены потом) приходится модифицировать саму реализацию API. Например, если найти адрес функции SelectObject в GDI32.DLL и модифицировать саму функцию, все вызовы Select- Object будут проходить через ваш код.
Отслеживание вызовов Win32 GDI 265 Модифицировать программу нетрудно. Значительно труднее сделать так, чтобы после модификации приложение работало так же, как раньше. Как было показано в разделе «Отслеживание вызовов функций Win32 API», мы хотим вставить в функцию несколько строк ассемблерного кода, чтобы вызов функции API приводил к автоматической передаче управления нашему входному обработчику. После выхода из обработчика исходная функция API должна выполняться в точности с теми же значениями регистров. После завершения функции API перед возвращением управления вызывающей стороне должен быть вызван наш выходной обработчик. Главная проблема заключается в том, что из-за модификации точки входа в функцию API при завершении входного обработчика управление нельзя передать модифицированному входу функции. У этой проблемы существует два основных решения. В первом случае входной обработчик восстанавливает прежнее состояние точки входа, чтобы при возвращении из него управление передавалось исходной реализации API. Такое решение идеально подходит для одноразовой регистрации, но где же вносить исправления для последующих вызовов? Наиболее естественно было бы делать это в выходном обработчике, но это означает, что на время выполнения функции API мы не сможем обрабатывать рекурсивные вызовы, а также вызовы из других программных потоков. Таким образом, мы приходим ко второму решению — не восстанавливать модифицированный участок, а переместить точку входа в функцию. При модификации изначального входа в функцию API как минимум 5 байт приходится выделить под инструкцию безусловного перехода в функцию-заглушку, что приводит fc порче нескольких инструкций. Поврежденные инструкции можно скопировать в буфер, находящийся внутри DLL-разведчика, и поставить после них команду перехода к первой инструкции после поврежденного участка. Когда все это будет сделано, остается лишь разрешить входному обработчику DLL-разведчика передать управление на перемещенные инструкции. Следующий пример поможет лучше разобраться в происходящем. Рассмотрим несколько начальных инструкций функции Sel ectObject на ассемблере и в машинных кодах. _Select0bject@8: 55 push ebp 8В ЕС mov ebp. esp 51 push ecx 83 65 FC 00 and dword ptr [ebp-4]. 0 _Select0bject@8+8: Пять байт, начинающихся с адреса _Select0bject@8, понадобятся для нашей маленькой хирургической операции; это приведет к порче 4 инструкций общим объемом 8 байт. Мы сохраняем первые 8 байт Sel ectOb ject@8 и записываем по этому адресу инструкцию безусловного перехода в заглушку. _Select0bject@8: е9 хх хх хх хх jmp Stub_Select0bject@8 90 nop 90 nop 90 nop _Select0bject@8+8:
266 Глава 4. Мониторинг графической системы Windows Обратите внимание: на самом деле мы используем лишь 5 байт, но для того, чтобы программа нормально работала, в нее включаются три пустые инструкции пор. Код заглушки выглядит следующим образом: Stub_SelectObject@8: push index_selectobject jmp ProxyProlog New_Select0bject@8: push ebp mov ebp. esp push ecx and dword ptr [ebp-4], 0 jmp _Select0bject@8+8 Обратите внимание на пару любопытных подробностей. Во-первых, по адресу Stub_Select0bject@8 находится точно такой же код, как и в нашем предыдущем решении с модификацией каталога импорта. Во-вторых, функция New_ Sel ectObject@8 воссоздает начало функции _Select0bject@8 перед модификацией. Эти совпадения позволяют заново использовать весь код, задействованный в решении с модификацией каталога импорта, за одним исключением: мы должны присвоить pFuncTable->m_func[index_se1ectobject].f_oldaddress значение New_Select- 0bject@8, чтобы при возвращении из ProxyProlog выполнение проходило по тому же пути, что и при использовании исходной функции API. Впрочем, мы еще не рассмотрели самую сложную сторону решения с перемещением кода — внешне простую задачу вычисления количества перемещаемых байт. Мы уже выяснили, что минимальное количество равно 5 байтам, но определить точную величину нелегко, поскольку копироваться должны только целые инструкции. Для процессоров Intel не существует простых правил вычисления длины инструкции по нескольким первым байтам. В результате постоянного добавления новых инструкций в исходный набор 8086 ситуация невероятно усложнилась. Программа вычисления количества байт, работающая для целых инструкций размером не менее 5 байт, фактически представляет собой «скелет» дизассемблера. Во времена Win 16 задача решалась гораздо проще, поскольку все экспортируемые функции имели одинаковый пролог. С появлением 32-разрядного кода и компиляторов с улучшенной оптимизацией (а особенно для процессоров с несколькими конвейерами обработки инструкций) предсказать, с какой инструкции начинается функция, стало невозможно. Попутно возникает другая проблема — не все инструкции можно переместить простым копированием. Команды передачи управления (например, jmp) часто используют относительные адреса, зависящие от текущего местонахождения команды в памяти. Чтобы переместить такую команду, вам придется обновить относительное смещение. В нашей текущей реализации прологи функций с переходами по относительному смещению не поддерживаются. В статической библиотеке Patcher.lib, подключаемой к Diver.dll, реализована модификация с перемещением. Чтобы сообщить, что вы хотите использовать перехват на уровне процесса, задайте одинаковые имена вызывающего и вызываемого модуля. Например, следующий ini-файл обеспечивает перехват на уровне процесса для функций GDI32, перечисленных в wingdi.ini, и функций USER32, перечисленных в winuser.ini:
Отслеживание вызовов Win32 GDI 267 [Module] Gdi32.cn 1. Gdi32.DLL. wingdi User32.dll. User32.DLL, winuser При перехвате на уровне процесса всплывают многочисленные функции GDI API, вызываемые из других системных DLL — таких, как USER32.DLL, COMDLG32.DLL, COMCTL32.DLL, OLE32.DLL и даже из самой GDI32.DLL Вы увидите, что USER32.DLL обращается к GDI32.DLL для выполнения графических операций, что GDI32.DLL объединяет вызовы функций API в вызовы обобщенных функций или наоборот, разбивает сложные функции API на более простые. Таким образом, вы сможете наблюдать за представлением «из-за кулис». Небольшой пример приведен на рис. 4.6. ШШШШ1Ш ^Щ De^h Щт п ЯВ 2! fill 2! ИИ 2 SB 2i ЯВ 2] lijf 2l ^Щ 2! ■j з? |iii 3! ЯШ 3! ■И з1' ЯИ зг ШЩ 3 SB ^ ■l 2! ШЩ 2: |Щ 2| Enter 6271966 "6272098" ..__ 6272458! 6272490! "627252Б? '6272529: 6272534? 6272545! 6272594! 6272616? 6272629г 62729035 6272916! 6272926' "6272967" '62729681 "62729691 :р:ШшШ1р«>Яу012 jfiPjjjj Leaver Return "6272992!" HBITMAP(520504c8) "Й721"7бГ'(Н_с17 "Й72457ТнВ1ТМАР(5205Ь4с8) 6272486И 6272526; (SHBITMAP)f "6272528"! WHITE "6272532! BLACK 6272966 ;96 6272593:1 6272607! (НВГГМАР)4с8 6272626! (SHPALEtfE)b ■'6272902!96 * 6272915! (SHPALEtfEjii) 6272925!" (HBITMAP)4c8 6272966!TRUE " 6272968 Г BLACK "6272969:' WHITE " 6272983]. (HBITMAP)4c8 ^^ШШг^^'^'^'Л''^^^^^^^^ Caller "CARDS. dii+17e9' USER'32.d'li+c60i" USER32.'dli+c650 ' USER32.dll+c663 USER32.dll+c9c2 USER32.dii+c9el' "USER32.dil+c9fi ■'uSER32.dii+ca1e Gbl32.DLL+6baa • GDI32.DLL+6bbb ■■'GDi32'.DLL-»6bd7" "G'DI32.''D'LL46'c6a' "GDi32.DLL+6cic !"GDJ32'.DLL-»6c25 : GDI32.DLL46c32 ruSER32.'dii+ca3f'" ru'SER32!dii+ca4a"' ;'USER32!'dii+ca62 ' ШШ^ДЯИ Caiee ]^u8ei32.dH!LoadBjtmapA' zn'^^^^^z~zzziz j_Gdi32.d]j!Crea^ 1 user32dll!ReleaseDC ? Gdi32.dli!SelectObject ; Gdi'32.dii!SetBkColdr j'Gdi32dlllSetfextCoior' ' ! Gdi32. dlHSetbiBits j Gdi32.dll!SaveDC j Gdi32.dll!SelectObject ZJ^!^:M^^!f^^^.JlZ^ ..... ... rGdi32!dliis^lBiY8Tobewce 7 Gdi32! diiiSejectPaiette j Gdi32.dliiSelect6bject "j Gdi32.dil!RestoreDC rGdi32"diiisette'xtCoior I GdQZdVliSetBkCol'or 1 Gdi3idll!Se'l'ectObjiBct $ЩА ||| 111 III 111 ill 111 111 111 ||| III 111 111 ill Рис. 4.6. Полный мониторинг вызовов API дает представление о реализации LoadBitmap Вызовы API, показанные на рисунке, отсортированы по порядку их обработки. В первом столбце указывается уровень вложенности; во втором — возвращаемое значение; в третьем — адрес вызывающей стороны и в четвертом — имя вызываемой функции. Значения параметров не показаны для экономии места. Обратите внимание: непосредственно сгенерированные файлы сортируются не по времени входа, а по времени выхода. Для упрощения анализа данных использованы средства сортировки Excel. Если внимательно присмотреться к рисунку, вы поймете, что перед вами «секретная» реализация функции LoadBitmap. Функция LoadBitmap поддерживается диспетчером окон (USER32.DLL) и предназначается для загрузки растра в ап- паратно-зависимом формате GDI. Однако из документации мы не знаем, как реализована эта функция. Из рисунка видно, что LoadBitmapA (ANSI-версия Load- Bitmap) вызывает несколько функций GDI для преобразования аппаратно-неза- висимого растра в аппаратно-зависимый растр. Функция CreateCompatibleBitmap создает новый DDB-растр, а функция SetDIBits выполняет преобразование. Из рисунка также видно, как функция SetDIBits реализована в GDI — она сводится к вызову SetDIBitsToDevice.
268 Глава 4. Мониторинг графической системы Windows Отслеживание СОМ-интерфейсов DirectDraw DirectDraw API, как и остальные интерфейсы DirectX API, создан на основе технологии Microsoft COM (Component Object Model). Функциональные возможности DirectDraw предоставляются пользователю в виде нескольких СОМ-интерфейсов — например, IDirectDraw и IDirectDrawSurface. СОМ-интерфейс определяется как группа семантически связанных функций (или методов). Например, методы интерфейса IDirectDrawSurface предназначены для работы с поверхностями DirectDraw, а методы интерфейса IDirectDraw- Clipper управляют отсечением поверхностей DirectDraw. Давайте посмотрим, как организовать мониторинг этих методов. Таблица виртуальных функций Методы СОМ-интерфейса вызываются через интерфейсный указатель, который фактически представляет собой указатель на объект C++ с неизвестным представлением данных. Единственное, что известно клиентской стороне, — то, что СОМ-объект начинается с указателя на таблицу виртуальных функций. Эта таблица содержит указатели на функции, реализующее все методы интерфейса и следующие в определенном порядке. Все СОМ-интерфейсы являются производными от интерфейса IUnknown, в котором определяются три метода: Query- Interface, AddRef и Release. Это означает, что первые три указателя в таблице виртуальных функций СОМ всегда реализуют эти три метода. Обычно таблица виртуальных функций C++ или СОМ генерируется компилятором в глобальной области данных, доступной только для чтения или для чтения/записи. Прослеживается аналогия с внутренними переменными, используемыми каталогом импорта модуля для хранения адресов импортируемых функций. С технической точки зрения перехватывать вызовы методов СОМ-интерфейса или DirectDraw-интерфейса совсем несложно. Все, что для этого необходимо, — найти адреса всех интересующих нас таблиц виртуальных функций, а затем заменить хранящиеся в них указатели на функции указателями на заглушки DLL- разведчика. Получить адрес таблицы виртуальных функций для обычного СОМ-интерфейса просто. Найдите идентификаторы GUID класса и интерфейса, вызовите CoCreatelnstance — и реализация СОМ операционной системы загрузит нужный сервер СОМ, создаст СОМ-объект и вернет вам интерфейсный указатель. Первые 4 байта блока, на которые ссылается интерфейсный указатель, и дадут вам искомый адрес таблицы виртуальных функций. Большинство интерфейсов DirectDraw не создается стандартным вызовом CoCreatelnstance. Например, создать интерфейсный указатель для IDirectDrawSurface можно лишь одним способом — вызовом метода CreateSurface для интерфейса IDirectDraw. В контексте DirectDraw это абсолютно логично, поскольку поверхности DirectDraw всегда находятся под управлением объектов DirectDraw. Разведывательная библиотека DLL должна оказывать минимальное воздействие на работу системы. Следовательно, создавать объект DirectDraw и поверхность DirectDraw лишь для получения таблицы виртуальных функций IDirectDrawSurface было бы нежелательно. Альтернативное решение — получать данные
Отслеживание СОМ-интерфейсов DirectDraw 269 таблиц виртуальных функций автономно, в отдельной программе, сохранять их в ini-файле и затем использовать в процессе отслеживания. Программа Query- DDraw действует именно так. Она пытается создать как можно больше различных интерфейсных указателей DirectDraw и регистрирует адреса таблиц виртуальных функций, количество методов, а также имя и GUID интерфейса. В C++ определить количество методов класса практически невозможно, однако программа DirectDraw, написанная на С, должна знать это количество, поскольку таблица виртуальных функций имитируется при помощи массива указателей на функции. В следующем фрагменте показано, как получить необходимую информацию для интерфейса IDirectDraw. #define СINTERFACE #include <ddraw.h> IDirectDraw * lpdd; HRESULT hr - DirectDrawCreateCNULL. & lpdd. NULL); Dumplnterface("IID_IDirectDraw", IID_IDirectDraw, lpdd->lpVtbl. sizeof(*lpdd->lpVtbl) ); Перед включением ddraw.h, заголовочного файла DirectDraw, определяется макрос CINTERFACE. Тем самым активизируется определение СОМ-интерфейсов в стиле С, где таблица виртуальных функций имитируется массивом указателей на функции, а указатель на таблицу виртуальных функций хранится в одном из полей структуры (lpVtbl). Определение СОМ-интерфейсов в стиле С позволяет использовать функцию sizeof(*lpdd->1pVtbl) для вычисления размера таблицы виртуальных функций, а следовательно, — и количества функций в таблице. Вызов метода C++ несколько отличается от обычного вызова функции в С или Pascal. Вызываемому методу неявно передается дополнительный указатель на текущий объект (так называемый указатель this). Хотя компилятор C++ поддерживает возможность передачи указателя this в регистрах процессора для повышения быстродействия, СОМ-интерфейсы и интерфейс DirectDraw всегда передают указатель this в стеке. Остается лишь сообщить функции вывода параметров DLL-разведчика о наличии дополнительного параметра. Определение DirectDraw API Следующая задача — сгенерировать для всех методов DirectDraw ini-файл в формате управляющей программы Pogy. Для этого в простейший анализатор заголовочных файлов С, Skimmer, необходимо внести несколько изменений. Во-первых, начало объявления СОМ-интерфейса должно определяться по префиксу DECLAREINTERFACEj во-вторых, программа должна обрабатывать макросы STDMETHOD и STDMETHOD_ для восстановления типа возвращаемого значения и имени функции; в-третьих, макросы THIS и THIS_ также должны обрабатываться для передачи указателя this в качестве дополнительного параметра. Ниже приведена отредактированная версия полного, точного и недвусмысленного определения DirectDraw API. [ddraw] HRESULT DiYectDrawEnumerateW(LPPENUMCALLBACKW.LPVOID)
270 Глава 4. Мониторинг графической системы Windows HRESULT DirectDrawEnumerateA(LPPENUMCALLBACKA.LPVOID) HRESULT Di rectDrawEnumerateExW(LPPENUMCALLBACKEXW,LPVOID.DWORD) HRESULT DirectDrawEnumerateExACLPPENUMCALLBACKEXA.LPVOID.DWORD) HRESULT DirectDrawCreate(GUID*.LPDIRECTDRAW*.Iunknown*) HRESULT DirectDrawCreateClipper(DWORD.LPDIRECTDRAWCLIPPER*. Iunknown*) [COM_ddraw] 728405aO 7283Ш8 23 {6cl4db80-a733-llce-a5-21-00-20-af-0b-e5-60} 11D_IDirectDraw 728408eO 728318a8 24 {b3a6f3e0-2b43-llcf-a2-de-00-aa-00-b9-33-56} IID_IDirectDraw2 728407a0 7283Ш8 28 {9c59509a-39bd-lldl-8c-4a-00-c0-4f-d9-30-c5} IID_IDirectDraw4 72840940 7282f034 36 {6cl4db81-a733-llce-a5-21-00-20-af-0b-e5-60} IID_IDi rectDrawSurface [IDirectDraw] HRESULT QueryInterface(THIS.REFIID.LPVOID*) ULONG AddRef(THIS) ULONG Release(THIS) ULONG Compact(THIS) HRESULT Createdipper(THIS.DWORD.LPDIRECTDRAWCLIPPER*.Iunknown) HRESULT CreatePalette(THIS.DWORD,LPPALETTEENTRY. LPDIRECTDRAWPALETTE*.Iunknown) HRESULT CreateSurface(THIS.LPDDSURFACEDESC. LPDIRECTDRAWSURFACE*.Iunknown) В первой секции перечисляются обычные функции, экспортируемые из DDRAW.DLL Эта библиотека экспортирует довольно много функций. Здесь перечислены функции, документированные в ddraw.h; другие функции (такие, как DIlGetClassObject) принадлежат к числу стандартных экспортируемых функций СОМ; третьи документируются в других заголовочных файлах или вовсе не документируются. Во второй секции приведена информация о СОМ-интерфейсах, сгенерированная программой QueryDDraw. Для каждого СОМ-интерфейса DirectDraw указывается адрес таблицы виртуальных функций, адрес первой виртуальной функции (Querylnterface), количество методов, GUID и имя интерфейса. Эта секция строится заново для каждой ОС и установленных обновлений Service Packs. В дальнейших секциях подробно описываются прототипы методов (по одной секции на каждый интерфейс). Выше приведена секция лишь для интерфейса IDirectDraw. В интерфейсе IDirectDraw2 по сравнению с IDirectDraw добавляется всего один новый метод, а в IDirectDraw4 интерфейс IDirectDraw2 дополняется двумя новыми методами. Модификация таблицы виртуальных функций Содержимое файла определений DirectDraw API читается управляющей программой и передается DLL-разведчику. Разведчик строит таблицу интерфейсов, в которой для каждого интерфейса указывается имя, GUID, адрес таблицы виртуальных функций, адрес Querylnterface и количество методов. В процессе ини-
Отслеживание системных вызовов GDI 271 циализации он загружает COM DLL (в данном случае ddraw.dll), находит все перечисленные таблицы виртуальных функций и убеждается в том, что ее первый элемент совпадает с известным нам адресом метода Querylnterface. Затем DLL-разведчик вызывает следующую функцию для модификации всех перечисленных методов DirectDraw: BOOL HackMethod(uns1gned vtable, int n, FARPROC newfunc) { DWORD cBytesWritten; WnteProcessMemory(GetCurrentProcess(), (LPVOID) (vtable + n * 4). & newfunc. sizeof(newfunc). &cBytesWritten); return cBytesWritten — sizeof(newfunc); } Параметр newfunc указывает на функцию-заглушку, описанную в разделе «Отслеживание вызовов функций Win32 API». После модификации все работает так же, как и при правке каталога импорта. Ниже приведен маленький пример для интерфейса IDirectDraw. HRESULK О). ddraw.dll!SetCooperativeLevel. 0x893b28. HWND(800cc). 17 HRESULK0). ddraw.dll!SetDisplayMode. 0x893b28. 640. 480. 24 HRESULT(O). ddraw.dllICreateSurface. 0x893b28. LPDDSURFACEDESC(12fe54). LPDIRECTDRAWSURFACE*(12ff2c). Iunknown*(0) ULONG(O). ddraw.dll .'Release. 0x893b28 Как видно из приведенной последовательности вызовов, после создания объекта DirectDraw приложение вызывает методы SetCooperati veLevel, SetDi spl ay- Mode и CreateSurface интерфейса IDirectDraw, после чего уничтожает объект методом Release. Первый параметр, 0х893Ь28, представляет собой указатель this. Как видите, декодер типов данных DirectDraw приносит несомненную пользу. Отслеживание системных вызовов GDI От рассмотрения трех разных типов отслеживания вызовов API мы переходим к тому, о чем не пишут в документации. Да, речь идет о системных функциях GDI, интерфейсе между клиентом GDI пользовательского режима и графическим механизмом режима ядра. Как упоминалось выше, DirectDraw, Direct3D и OpenGL используют GDI для обращения к служебным функциям графического механизма. Из материала главы 2, посвященной архитектуре графической системы Windows NT/2000, следует, что системные функции графической системы играют очень важную роль — они отвечают за передачу запросов графического вывода
272 Глава 4. Мониторинг графической системы Windows из пользовательского режима графическому механизму режима ядра и драйверам устройств. Однако системные функции (и особенно системные функции графической системы) в официальной документации не упоминаются. В главе 2 была представлена программа SysCall, предназначенная для поиска вызовов системных функций в клиентских библиотеках DLL подсистем Win32 — а именно в GDI32.DLL, USER32.DLL и KERNEL32.DLL С помощью отладочных файлов символических имен программа перечисляет все вызовы системных функций с индексами, количеством параметров, адресами и символическими именами. Она даже может вывести данные о таблице системных функций в адресном пространстве ядра. Но поскольку обработчики системных функций в графическом механизме соответствуют вызовам функций в пользовательском режиме, нам будет гораздо проще следить за пользовательской стороной этого недокументированного интерфейса. Листинг, сгенерированный программой SysCall, не совсем отвечает нашим потребностям. Необходимо внести некоторые усовершенствования, чтобы программа генерировала список прототипов функций. В отличие от других модификаций, мы хотим, чтобы вместе с прототипами выводились и адреса этих функций. В противном случае программе-разведчику во время работы придется использовать отладочные символические имена, что сделает ее менее универсальной. В программу SysCall была добавлена новая команда меню, GDI32 system calls for Роду (Вызовы системных функций в GDI32 для Pogy). Ниже приведена лишь небольшая часть списка вызовов системных функций в GDI32.DLL [gdisyscall] D NtGdiCreateEllipticRgn(D.D.D.D). 77F725AB. 1020 D NtGdiDdGetBltStatus(D.D), 77F726A7. 1047 D NtGdiGetDeviceGammaRamp(D.D). 77F728D7. 10a8 D NtGdiSTROBJ_dwGetCodePage(D). 77F72CB7. 1274 D NtGdiGetTextExtentExW(D.D.D.D,D.D,D,D). 77F43C51. 10c9 D NtGdiGetColorAdjustment(D.D), 77F728BB. lOal D NtGdiFlushO. 77F413F9, 1093 D NtGdiDdSetOverlayPosition(D.D.D). 77F7274F, 105d D NtGdiPATHOBJJ)EnumCllpLines(D.D.D). 77F72CD3. 1279 D NtGdiEngCreateBitmap(D,D.D.D.D,D). 77F4B5CD. 1240 D NtGdiColorCreatePalette(D.D.D.D.D.D). 77F7258F, 1011 D NtGdiDdDestroySurface(D.D). 77F5AAB2. 1041 D NtGdiDdRenderMoComp(D.D), 77F72717. 1057 Возможно, вы заметили, что мы не располагаем точной информацией о типах параметров и возвращаемых значениях. Единственное, что нам известно, — это количество параметров, которое определяется по количеству байт, извлекаемых из стека при возвращении. Поэтому мы просто помечаем каждый параметр типом D (сокращение от DWORD) и откладываем их замену более осмысленными типами до появления дополнительной информации. В DLL-разведчика приходится внести ряд изменений. Во-первых, адрес функции известен, поэтому ухищрения типа GetProcAddress для Win32 API не понадобятся. Однако программа должна убедиться в том, что по этому адресу находится код в формате вызова системной функции:
Отслеживание системных вызовов GDI 273 NtGdi_SysCall_xx mov eax. function_index NtGdi_SysCall_xx+5: lea edx. [esp+4] int 0x2e ret parameterjnumber * 4 В принципе можно было бы воспользоваться методом модификации с перемещением, использованным для перехвата вызовов API на уровне процесса, но нетрудно заметить, что инструкции после NtGdi_SysCall_xx+5 существуют в ограниченном количестве вариантов — по одному для каждого количества параметров. Количество параметров, передаваемых при вызове системных функций GDI, лежит в пределах от 0 до 15. Следовательно, нам понадобится только 16 функций для замены кода, следующего после первой инструкции (сохранения индекса). После модификации код принимает следующий вид: NtGdi_SysCall_xx mov eax. functiorMndex NtGdi_SysCall_xx+5: jmp Stub_NtGdi_SysCall_xx Stub_NtGdi_SysCall_xx push func_id jmp ProxyProlog При возвращении из ProxyProlog мы должны передать управление одной из функций, в которых и происходит непосредственный вызов системных функций: // Для системных функций с двумя параметрами declspec(naked) void SysCall_08(void) { _asm lea edx, [esp+4] _asm int 0x2e _asm ret 0x08 } Перехват системных вызовов осуществлялся бы гораздо проще, если бы мы не использовали базовые функции разведчика ProxyProlog, ProxyEntry, ProxyEpilog и ProxyExit. Мониторинг графических системных функций — дело весьма увлекательное, поскольку он сильно отличается от обычного мониторинга функций API. Подробные описания на эту тему редко встречаются в книгах и журналах. А отслеживать вызовы GDI API вместе с вызовами системных функций еще интереснее. Не исключено, что этим еще никто не занимался. GDI API представляет собой интерфейс между приложением и механизмом поддержки ОС пользовательского режима, а графические системные функции образуют интерфейс между GDI и графическим механизмом режима ядра. Таким образом, мы следим за обеими сторонами клиентской DLL GDI GDI32.DLL Различия между ними наглядно показывают, что же именно происходит в клиентской DLL GDI. В табл. 4.1 представлена отредактированная версия протокола с одновременным мониторингом вызовов GDI и графических системных функций.
274 Глава 4. Мониторинг графической системы Windows Таблица 4.1. Пример протокола вызовов функций Win32 GDI и системных функций Уровень вложенности Результат Вызов функции 1 1 1 1 2 1 1 1 2 1 3 2 2 (SHF0ND21 WHITE 0 BLACK TRUE TRUE TRUE (SHBRUSH)IO (SHPENU7 (SHPEN)17 (HF0NT)3el (HF0NT)3el TRUE TRUE 3 2 1 2 1 HBITMAP(3d9) HBITMAP(3d9) HBITMAP(3d9) TRUE TRUE Se1ectObject((HDC)407, (HF0NT)3el) SetBkColor((HDC)305.a9c8a2) SetTextAlign((HDC)305,0) SetTextColor((HDC)305.BLACK) NtGdiDeleteObjectAppC(HPEN)4d9) De1ete0bject((HPEN)4d9) Delete0bject((HBRUSH)3e8) GetStockObject(O) NtGdiGetStock0bject(7) GetStock0bject(7) NtGdiHfontCreate(0xl2f8c0,0x164,0x0,0x0.0x137468) CreateFontInd1rectExW(ENUML0GF0NTEXDVW*(l2f8c0)) NtGdi GetWi dthTable((HDC)407,Oxb,0xl37b28.0x106. 0xl37d3e,0xl378b8) GetTextExtentPointW((HDC)407,LPCWSTR(1135a394),ll. LPSIZE(12fac4)) NtGdi CreateCompatibl eBi tmap( (HDO407, 0x20, 0x24) CreateCompatibleBitmap((HDC)407,32,36) CreateDiscardableBitmap((HDC)407,32.36) NtGdiRectangle((HDC)2e9.0x60,0x3.0x64,0x7) Rectangle((HDC)2e9,96,3,100,7) Помните о том, что функции с более высоким уровнем вложенности вызываются функциями более низкого уровня, следующими в протоколе после них. Полученные протоколы подтверждают некоторые факты, упоминавшиеся в предыдущих главах. О Часть структуры данных контекста устройства реализуется в пользовательском режиме, поэтому простые запросы к контексту легко и эффективно обрабатываются в пользовательском режиме без обращения к системным функциям режима ядра. О Таблица объектов GDI находится под управлением графического механизма, поэтому при создании и уничтожении объектов вызываются системные функции. Кисти и прямоугольные регионы занимают особое место — GDI
Отслеживание интерфейса DDI 275 кэширует удаленные объекты для повторного использования. Мы видим, что при удалении HPEN вызывается функция NtGdi Del eteObjectApp, тогда как удаление HBRUSH не всегда приводит к вызову системной функции. О CreateDiscardableBitmap — это просто CreateCompatibleBitmap. О Графические команды обычно напрямую транслируются в системные функции. О Системные функции GDI работают практически с теми же типами данных, что и функции Win32 GDI API. В сущности, у вас появился отличный инструментарий для самостоятельных исследований GDI. Вы можете спланировать собственный эксперимент в интересующей вас области GDI API, установить соответствующие параметры мониторинга, провести тест и проанализировать результаты. Отслеживание интерфейса DDI В предыдущих четырех разделах этой главы мы подробно рассмотрели возможности наблюдения за графической системой Windows в пользовательском режиме. Теперь мы можем отслеживать как входной, так и выходной интерфейс GDI32. А сейчас пора переходить на новую «территорию» — к графическому механизму режима ядра. DLL подсистем Win32 обращаются к графическому механизму посредством вызова системных функций. В главе 2 была представлена программа SysCall, которая выводит полный список вызовов системных функций (как графических, так и относящихся к управлению окнами). Списки функций GDI32 и USER32, использующих системные вызовы, практически полностью совпадают со списком обработчиков системных функций WIN32K.SYS. Единственное различие состоит в том, что некоторые системные функции не вызываются в системных DLL пользовательского режима. Мониторинг графических системных функций режима ядра приносит не так уж много новой информации, поскольку мы можем легко отслеживать системные вызовы в пользовательском режиме. Конечно, у отслеживания этого интерфейса со стороны ядра есть и свои преимущества — оно осуществляется на уровне всей системы, а не на уровне конкретного процесса. С другой стороны, подобные эксперименты слишком сильно отражаются на работе всей системы. Самым интересным графическим аспектом режима ядра является интерфейс DDI между графическим механизмом и драйверами устройств. В главе 2 уже упоминалось о том, что графическому механизму приходится изрядно потрудиться над преобразованием вызовов GDI в вызовы DDI, поскольку они находятся на разных уровнях абстракции. В разделе «Драйверы принтеров» главы 2 был представлен простой драйвер принтера, генерирующий документы HTML вместо принтерных команд. HTML-страницы содержат списки вызовов DDI с ше- стнадцатеричными дампами параметров и 24-битные цветные растры, воспроизведенные с разрешением 96 dpi. Наш простой драйвер HTML хорошо подходит для экспериментов с интерфейсом DDL Впрочем, этот вариант ограничивается драйвером принтера и фиксированным набором параметров. Конечно, желательно иметь более общее решение, которое
276 Глава 4. Мониторинг графической системы Windows бы позволяло следить за всеми графическими драйверами, экранами, принтерами, плоттерами и даже факсами. В главе 3 чрезвычайно подробно рассматриваются основные внутренние структуры данных GDI и графического механизма. В частности, там говорилось о том, что объект ядра каждого контекста устройства содержит указатель на структуру PDEV, содержащую всю информацию о физическом устройстве для графического механизма. Структура PDEV создается после загрузки драйвера экрана при вызове функций DrvEnableDriver, DrvEnablePDEV и, наконец, DrvCompletePDEV. Следовательно, PDEV содержит всю информацию, полученную от драйвера графического устройства при вызове этих функций, включая и точки входа DDL В Windows 2000 последний блок данных структуры PDEV содержит 89 указателей на функции; в Windows NT 4.0 он может содержать до 65 указателей на функции. Работать с указателями на функции при мониторинге вызовов API очень просто. Нам уже приходилось модифицировать указатели в таблице импорта DLL и в таблицах виртуальных функций С++/СЮМ. Массив указателей на функции в структуре PDEV имеет много общего с таблицей виртуальных функций. Среди этих 89 указателей довольно многие не используются, остаются зарезервированными или обычно не реализуются драйвером устройства. Даже мониторинг 20-30 вызовов DDI означал бы, что мы неплохо справились с поставленной задачей. Учитывая, что это число совсем невелико по сравнению с тремя сотнями функций GDI, мы могли бы просто написать 20-30 промежуточных функций DDI вместо того, чтобы разбираться с ассемблерными командами в адресном пространстве ядра. Программа для мониторинга вызовов GDI (как и для мониторинга функций API в пользовательском режиме) состоит из двух компонентов. Драйвер режима ядра загружается в адресное пространство ядра, включается в цепочку обработки вызовов DDI, получает информацию о параметрах и передает ее управляющей программе. Управляющая программа пользовательского режима запускает и завершает работу драйвера, передает ему команды и получает перехваченные данные. Драйвер режима ядра, DDISpy.SYS, представляет собой слегка расширенную версию драйвера Periscope, использованного в главе 3. Драйвер Periscope обрабатывал всего одну команду ввода-вывода для чтения блока данных из адресного пространства ядра, что так помогло нам при исследованиях внутренних структур данных GDI. DDISpy обрабатывает четыре команды ввода-вывода, перечисленные в табл. 4.2. Таблица 4.2. Команды ввода-вывода DDISpy Код команды Параметры Функция DDISPYREAD Адрес, размер То же, что и у Periscope, — чтение блока данных из адресного пространства ядра DDISPYSTART Адрес таблицы Модификация содержимого таблицы функ- функций DDI, ций (начало мониторинга вызовов DDI) количество
Отслеживание интерфейса DDI 277 Код команды Параметры Функция DDISPYEND Адрес таблицы Восстановление содержимого таблицы функ- функций DDI, ций (конец мониторинга вызовов DDI) количество DDISPYREPORT Размер Передача собранной информации управляющей программе Для каждой функции DDI создается соответствующая промежуточная функция, которая регистрирует данные, вызывает исходную функцию и, возможно, регистрирует ее возвращаемое значение. Хронометраж в данном случае не производится, поскольку общее быстродействие GDI мы измеряем в пользовательском режиме. Мы также не беспокоимся о сохранении регистров, зная, что по правилам DDI регистры используются только для возвращения значения функции. Словом, никакого ассемблера — сплошной код С. Ниже приведена самая интересная часть DDISpy — промежуточные функции. typedef struct PFN рРгоху; PFN pReal; PROXYFN; <YFN DDI_Proxy [] = // Перечисление в г (PFN) DrvEnablePDEV, (PFN) DrvCompletePDEV, (PFN) DrvDisablePDEV. (PFN) DrvEnableSurface, (PFN) DrvDisableSurface. (PFN) NULL. (PFN) NULL. (PFN) DrvResetPDEV. NULL. NULL. NULL. NULL. NULL. NULL. NULL. NULL. void DDISpy_Start(unsigned fntable. int count) { unsigned * pFuncTable = (unsigned *) fntable; // Очистить буфер for (int i=0; i<count; i++) if ( pFuncTable[i] > OxaOOOOOOO ) // Действительный указатель if ( DDIJ>roxy[i].pProxy != NULL ) // Есть промежуточная функция { // Запомнить настоящий адрес вызываемой функции DDI_Proxy[i].pReal - (PFN) pFuncTable[i]; // Подправить pFuncTable[i] - (unsigned) DDI_Proxy[i].pProxy: }
278 Глава 4. Мониторинг графической системы Windows void DDISpy_Stop(unsigned fntable. int count) { unsigned * pFuncTable = (unsigned *) fntable; for (int i=0: i<count; i++) if ( pFuncTable[i] > OxaOOOOOOO ) // Действительный указатель if ( DDI_Proxy[i].pProxy != NULL ) // Есть промежуточная функция { // Вернуть старый адрес pFuncTable[i] = (unsigned) DDI_Proxy[i].pReal; #define Call(name) (*(PFN_ ## name) \ DDI_Proxy[INDEX_ # namej.pReal) void APIENTRY DrvDisableDriver(void) { WriteC'DisableDriver"); Call(DrvDisableDriver)(); BOOL APIENTRY DrvTextOut(SURFOBJ *pso. STROBJ *pstro. FONTOBJ CLIPOBJ RECTL RECTL BRUSHOBJ BRUSHOBJ POINTL MIX *pfo, *pco. *prclExtra. *prclOpaque, *pboFore, *pboOpaque, *pptlOrg. mix) WriteCDrvTextOut"): // ... return Call(DrvTextOut) (pso, pstro. pfo. pco. prclExtra, prclOpaque. pboFore. pboOpaque. pptlOrg. mix); } Применение макроса Call оправдывается тем, что он делает программу более наглядной. Этот макрос направляет указатель на настоящую функцию DDI в таблицу DDI_Proxy, преобразует его к правильному типу указателя на функцию DDI и вызывает эту функцию. Кстати, вы обратили внимание на недостаток подобных перехватов API на языках высокого уровня? При вызове настоящей функции DDI происходит дублирование кадра стека. Управляющая программа DDIWatcher не вызывает особых проблем, поскольку она имеет много общего с программой TestPeriScope из главы 3. Ниже приведена самая важная функция, вызываемая после установки драйвера ядра. KDDIWatcher::SpyOnDDI(void) { unsigned buf[2048]; HDC hDC = GetDC(NULL); // Создать контекст устройства
Итоги 279 typedef unsigned (CALLBACK * ProcO) (void); ProcO pGdiQueryTable = (ProcO) GetProcAddress( GetModuleHandle("GDI32.DLL"), "GdiQueryTable"); assert(pGDIQueryTable); // Получить адрес таблицы объектов GDI unsigned * addr = (unsigned *) (pGDIQueryTableO + (unsigned) hDC & OxFFFF) * 16); // Элемент таблицы для hDC addr = (unsigned *) addr[0]; // Указатель на объект ядра scope.Read(buf. addr. 32): // Прочитать 8 двойных слов #ifdef NT4 unsigned pdev = buf[5]; // PDEV * unsigned fntable = pdev + 0x3F4; // Таблица функций #else unsigned pdev - buf[7]; // PDEV * unsigned fntable - pdev + 0xB8C; // Таблица функций #endif // Прочитать таблицу функций для проверки, ..DrvScope scope.ReadCbuf, (void *) fntable. 25 * 4); unsigned cmd[2] - { fntable. 25 }; unsigned long dwRead; // Начать отслеживание DDI IoControKDDISPYjTART. and. sizeof(cmd). buf. 100. &dwRead): // Добавить графические вызовы или переместить окно на рабочем столе // Прекратить отслеживание DDI IoControKDDISPYJND. cmd. sizeof(cmd). buf. 8. &dwRead); cmd[l] - sizeof(buf); // Прочитать зарегистрированные данные IoControKDDISPYJREPORT. cmd. sizeof(cmd). buf. sizeof(buf), &dwRead); // Отобразить полученные данные } Итак, у нас появилась программа для отслеживания интерфейса DDI, работающая с любым драйвером графического устройства. Работа программы основана на модификации структуры данных механизма GDI в памяти. Даже если это приведет к сбою компьютера и появлению «синего экрана» (что маловероятно), вы всегда сможете перезагрузить компьютер и восстановить его работоспособность. Итоги В этой главе были представлены различные инструменты для исследования логики работы GDI. В разделе «Отслеживание вызовов функций Win32 API» описаны общие принципы мониторинга вызовов API. Раздел «Отслеживание вызовов Win32 GDI» иллюстрирует методику мониторинга всех вызовов функ-
280 Глава 4. Мониторинг графической системы Windows ций GDI в процессе — как из GDI32.DLL, так и из внешних модулей. В разделе «Отслеживание СОМ-интерфейсов DirectDraw» мы сосредоточили свое внимание на СОМ-интерфейсах, используемых в DirectDraw. В разделе «Отслеживание системных вызовов GDI» подробно рассматривается мониторинг вызов графических системных функций. Глава завершается разделом «Отслеживание интерфейса DDI», посвященным перехвату функций интерфейса DDI с использованием нового драйвера режима ядра. При помощи инструментов, разработанных в этой главе, вы сможете следить за работой Win32 GDI/DirectDraw API и наблюдать за динамикой вызовов GDI/ DirectDraw на уровне процесса или модуля. Отслеживая недокументированные системные функции графической системы, вы увидите, как GDI32.DLL опирается в своей работе на поддержку со стороны графического механизма. Если вас больше интересуют реальные подробности взаимодействия графического механизма с драйвером устройства — в вашем распоряжении также имеется простое, но мощное средство мониторинга вызовов DDL Более того, у вас даже появляется утилита для автоматического построения файлов определений API и механизм для написания модулей расширения, обеспечивающих усовершенствованную или нестандартную обработку типов данных. Хотя в этой главе основное внимание уделяется GDI и DirectDraw, представленные решения носят общий характер и могут использоваться в отношении других частей Win32 API и других СОМ-интерфейсов. «Дайте мне функцию API, и я покажу вам, куда вас заведет этот вызов... или, по крайней мере, у меня есть все инструменты, чтобы это узнать». Теперь вы можете заявить это с полным правом. Примеры программ Примеры программ главы 4 (табл. 4.3), как и примеры программ главы 3, не принадлежат к числу обычных примеров графического программирования. Скорее, это изощренные системные утилиты, предназначенные для анализа работы графической подсистемы Windows и общих принципов внутреннего устройства операционной системы Windows. Пользуйтесь на здоровье. Таблица 4.3. Программы главы 4 Каталог проекта Описание Samples\Chapt_04\Patcher Библиотека модификации пролога функций для перехода к коду заглушки Samples\Chapt_04\Skimmer Программа для извлечения определений API из заголовочных файлов SDK Samples\Chapt_04\Diver Разведывательная библиотека DLL, внедряемая в исследуемый процесс для сбора информации Samples\Chapt_04\Pogy Управляющая программа мониторинга; устанавливает перехватчик Windows для внедрения DLL-разведчика по имени Diver
Итоги 281 Каталог проекта Описание Samples\Chapt_04\PogyGDI Декодер типов данных GDI (загружается из Diver) Samples\Chapt_04\QueryDDraw Вспомогательная программа для построения файла определений DirectDraw API Samples\Chapt_04\DDISpy Драйвер режима ядра для мониторинга функций интерфейса DDI Samples\Chapt_04\DDIWatcher Тестовая программа для мониторинга вызовов DDI с использованием программы DDISpy
Глава 5 Абстракция графического устройства Как известно, среди всех интерфейсов API графического программирования Windows центральное место занимает интерфейс GDI (Graphics Device Interface). DirectDraw, новый API двумерной графики от Microsoft, ориентирован на программирование игр, а интерфейс Direct3D предназначен для игр и приложений, строящих объемное изображение. Все эти графические API являются аппаратно-независимыми программными интерфейсами, что позволяет приложениям, написанным с их применением, работать на разных графических устройствах. Для обеспечения аппаратной независимости графического API необходима хорошая абстракция, которая бы позволяла представлять различные графические устройства и маскировать их различия без потери производительности. В этой главе описан главный механизм абстрагирования графических устройств в GDI — контекст устройства (device context, DC). Мы познакомимся с возможностями современных видеоадаптеров, представлением абстрактного графического устройства в виде контекста устройства, а также взаимодействием контекста устройства с модулем управления окнами ОС. Современные видеоадаптеры Графический API в системе Windows прежде всего ориентируется на работу с видеоадаптером как с главным средством взаимодействия пользователя и компьютера. Возможно, вас удивит, насколько сложным устройством является современный видеоадаптер. В индустрии PC 64-разрядные компьютеры едва замаячили на горизонте, а в руководстве к видеоадаптеру заявлено, что он использует 128-разрядную архитектуру. Возможно, ваши программы работают в Windows NT
Современные видеоадаптеры 283 с 32 мегабайтами памяти, а ваш видеоадаптер использует тот же объем памяти с 128-разрядной адресацией для собственных целей. Но самое невероятное заключается в том, что видеоадаптер способен выполнять в секунду до 9 миллиардов вещественных операций, а код режима ядра Windows NT имитирует вещественные операции с использованием целых чисел. Рассмотрим основные компоненты современного видеоадаптера. Кадровый буфер Все современные видеоадаптеры работают на растровом принципе; это означает, что информация в них хранится в виде двумерных массивов пикселов в области памяти видеоадаптера. Такая область памяти называется кадровым буфером (frame buffer). Кадровые буферы имеют различные размеры. Когда говорят о размере экрана, обычно имеют в виду «разрешение» (resolution). Эта характеристика принципиально отличается от разрешения, измеряемого в точках на дюйм (dots per inch, dpi), широко используемого для принтеров. Под разрешением экрана обычно понимается количество пикселов, которые могут отображаться на экране по вертикали и горизонтали; под разрешением принтера обычно понимают количество независимо адресуемых пикселов на один дюйм. Минимальный кадровый буфер, поддерживаемый в ОС Windows, имеет размеры, стандартные для VGA, — 640 пикселов в строке на 480 строк. Впервые этот размер был использован фирмой IBM на компьютерах PS/2. Обычно размер 640 х 480 встречается лишь при загрузке компьютера в безопасном режиме или при запуске старых программ, которые заставляют вас переключить экран в этот размер. Максимальные размеры кадрового буфера могут достигать 1600 х 1200 и даже 1920 х 1200 пикселов. Обратите внимание: для большинства разрешений ширина и высота экрана находятся в пропорции 4:3 — например, 640 х 480, 800 х 600, 1024 х 768 и даже 1600 х 1200. Эта пропорция соответствует отношению ширины к высоте самого монитора, благодаря чему соседние пикселы на экране находятся на одинаковом расстоянии по вертикали и по горизонтали. В зависимости от количества цветов, воспроизводимых в кадровом буфере, пикселы могут представляться разным количеством бит. В монохромном кадровом буфере пиксел представляется всего одним битом, а в 16-цветном буфере используются 4 бита на пиксел. В видеоадаптерах нового поколения эти цветовые режимы практически не встречаются. В наши дни кадровый буфер содержит минимум 256 цветов, при этом каждый пиксел представляется 8 битами (или одним байтом). Часто используются так называемые режимы High Color с кодировкой одного пиксела 15 или 16 битами; это позволяет представить 32 768 (32К) или 65 536 (64К) цветов, хотя в обоих случаях пиксел кодируется 2 байтами. Все чаще встречаются видеоадаптеры с поддержкой режимов True Color, в которых 24 бита используются для представления 224 (16М) разных цветов. В некоторых режимах даже используется 32-разрядная кодировка пикселов, хотя это и не значит, что в этих режимах имеет место 232 цветов; 8 бит обычно требуются для хранения данных альфа-канала, в результате для представления цветовой информации остается всего 24 бита.
284 Глава 5. Абстракция графического устройства Чтобы драйвер графического устройства мог осуществлять вывод в кадровый буфер, последний необходимо отобразить в адресное пространство процессора. На ранних моделях PC использовались 20-разрядные адресные линии, что позволяло работать с адресным пространством объема до 1 Мбайт. Видеоадаптеру отводилось всего 64 или 128 Кбайт из общего 1-мегабайтного адресного пространства. Для первых видеоадаптеров Super-VGA с разрешением 1024 х 768 и 256 цветами требовался кадровый буфер объемом 768 Кбайт, что значительно превосходило жалкие 128 Кбайт. Поэтому вместо хранения кадрового буфера в виде одного непрерывного блока 1024 х 768 х 1 байт оборудования приходилось делить его на восемь цветовых плоскостей (planes) 1024 х 768 х 1 бит. Каждая плоскость занимала всего 96 Кбайт, что делало возможным использование видеоадаптера на PC. В результате деления пикселов на восемь плоскостей для записи одного пиксела в кадровый буфер приходилось заносить в аппаратный регистр команду отображения плоскости в адресное пространство процессора, обновлять один бит, переходить к следующей плоскости и т. д. Иногда производители оборудования делили большие кадровые буферы на несколько банков (banks) или использовали плоскости одновременно с банками. Как нетрудно догадаться, все это изрядно затрудняло реализацию аппарат- но-независимого интерфейса GDI. В результате компания Microsoft выдвинула концепцию аппаратно-зависимых растров (DDB), которая позволяла производителям оборудования обеспечивать поддержку быстрого перевода растров в свой собственный формат кадрового буфера и обратно. В Windows NT/2000 вся система, включая графическую подсистему, работает в 32-разрядном адресном пространстве. Объем этого пространства (4-гигабайтный) оставляет достаточно места для любых кадровых буферов. В связи с активным продвижением DirectX компания Microsoft требует, чтобы новые видеоадаптеры поддерживали линейные кадровые буферы с упакованными пикселами. «Упаковка» означает, что все пикселы должны находиться вместе, без деления на цветовые плоскости. Линейность означает, что весь кадровый буфер может отображаться в 32-разрядное линейное адресное пространство. По мере увеличения количества бит;на пиксел и разрешения для хранения всего кадрового буфера требуется все больше памяти. В табл. 5.1 перечислены объемы памяти, необходимые для хранения одного кадрового буфера при разном разрешении и формате пикселов. Таблица 5.1. Геометрия кадрового буфера Разрешение Отношение Объем памяти для хранения кадрового буфера, Кбайт «ширина/ высота» 8бит 15,16 бит 24 бита 32 бита 640 х 480 800 х 600 1024 х 768 1152x864 4:3 4:3 4:3 4:3 300 469 768 972 600 938 1536 1944 900 1407 2304 2916 1200 1875 3072 3888
Современные видеоадаптеры 285 Разрешение Отношение Объем памяти для хранения кадрового буфера, Кбайт «ширина/ высота» 8 бит 15'1б бит 24 бита 32 бита 1280 х 1024 1600 х 1200 1920 х 1080 1920 х 1200 5:4 4:3 16:9 8:5 1280 1875 2025 2250 2560 3750 4050 4500 3840 5625 6075 6750 5120 7500 8100 9000 В максимальном режиме, поддерживаемом видеоадаптером автора, используется разрешение 1920 х 1200 с 32-разрядным кадровым буфером и частотой вертикальной развертки 60 Гц. Это означает, что каждую секунду этот видеоадаптер 60 раз читает весь 9000-килобайтный кадровый буфер и преобразует его в видеосигнал. Таким образом, в секунду видеоадаптер должен обрабатывать 540 Мбайт информации; становится понятно, почему для него нужна 128-разрядная быстрая синхронная память. Шаг Высота В G R Ширина х байт на пиксел Рис. 5.1. Геометрия кадрового буфера В разрешении 1024 х 768 при 24 бит/пиксел одна строка развертки представляется минимум 2304 байтами. Спецификация Microsoft требует, чтобы для повышения быстродействия при работе с памятью строки развертки выравнивались в кадровом буфере по 32-разрядной границе двойных слов. Объем в 2304 байта соответствует этому требованию. При этом строка развертки вовсе не обязана иметь точную длину в 2304 байта — она лишь должна быть не меньше
286 Глава 5. Абстракция графического устройства этой величины. Таким образом, производителям оборудования предоставляется определенная гибкость при выравнивании строк развертки. Размер одной строки развертки в кадровом буфере называется шагом (pitch). В структуре кадрового буфера, изображенной на рис. 5.1, буфер представляет собой массив строк развертки, а размер каждого элемента массива определяется шагом буфера. В строке развертки каждый пиксел представляется определенным количеством смежных битов или байтов. Например, для кадрового буфера с кодировкой 24 бит/пиксел один пиксел представляется тремя байтами, определяющими интенсивность цветовых составляющих в последовательности «синий — зеленый — красный». Следующая функция вычисляет адрес пиксела по начальному адресу кадрового буфера, шагу, размеру и относительной позиции пиксела в буфере: char *GetPixelAddress(chaг * buffer, int pitch, int byteperpixel, int x, int y) { return buffer + у * pitch + x * byteperpixel; } Формат пикселов Когда вы смотрите на какой-нибудь предмет, отраженный им свет попадает вам в глаза. Свет является таким же электромагнитным излучением, как, например, радиоволны, микроволны, инфракрасное и рентгеновское излучение или гамма- лучи. Человеческий глаз воспринимает лишь малую часть всего электромагнитного спектра, которая называется видимым светом и лежит в интервале длин волн от 400 до 700 нм. Различные цвета соответствуют разным длинам волн в спектре видимого света. В наших глазах находятся особые клетки, так называемые колбочки; они чувствительны к этим длинам волн и позволяют нам видеть мир в цвете. Три разных типа колбочек подвержены воздействию света в красной, зеленой и синей частях спектра. Эти три цвета называются основными. Свет, порождаемый разными источниками, относится к разным частям спектра и воспринимается как имеющий тот или иной цвет. В компьютерной промышленности цвет обычно описывается совокупностью трех основных цветовых составляющих — красной, зеленой и синей. Цвет можно рассматривать как точку в цветовом трехмерном пространстве, в котором составляющие соответствуют трем осям (так называемое цветовое пространство RGB). В литературе по компьютерной графике цветовые составляющие обычно представляются вещественными числами в интервале от 0 до 1, что позволяет описывать бесконечное количество цветов. Но в дискретном мире современных видеоадаптеров каждый компонент обычно преобразуется к целому числу в интервале от 0 до 255, представленному в пространстве памяти восемью битами или одним байтом. Таким образом, цвет одного пиксела описывается тремя байтами — по одному для красной, зеленой и синей составляющей, а 24-разрядное дискретное цветовое пространство RGB может описывать до 16 777 216 различных цветов. В монохромном кадровом буфере каждый пиксел представляется одним битом памяти. Информация о восьми пикселах упаковывается в один байт, при этом
Современные видеоадаптеры 287 старший бит соответствует первому пикселу, а младший бит — последнему пикселу. Скорее всего, вам не придется использовать монохромный буфер для непосредственного вывода, но и в наши дни монохромные буферы играют значительную роль в Windows-программировании. В цветном кадровом буфере цветовые плоскости представляются в формате монохромных буферов. Монохромный формат часто используется для представления растровых изображений в памяти — например, глифы шрифтов обычно преобразуются в монохромные растры перед выводом на экран или отправкой на принтер. Одноцветные принтеры также работают с некоторыми разновидностями монохромных растров на уровне языка принтера или внутреннего микрокода. Представление одного пиксела 8 битами позволяет использовать до 256 разных цветов. Если бы эти цвета жестко фиксировались, нам пришлось бы выбрать универсальный набор точек для представления всего цветового пространства RGB — для нормального отображения нашего многоцветного мира этого явно недостаточно. Поэтому вместо фиксированного набора цветов видеоадаптер использует цветовую таблицу, называемую палитрой (palette). Для кадрового буфера с 8-разрядной кодировкой палитра состоит из 256 элементов, каждый из которых соответствует 24-разрядному значению RGB. В кадровом буфере сохраняются не цвета, а индексы в палитре. При косвенном представлении цветов с использованием палитры кадровый буфер, как и раньше, в любой момент времени содержит только 256 разных цветов, однако эти цвета выбираются из 16 миллионов кандидатов 24-разрядного цветового пространства. Например, при помощи палитры с 256 оттенками серого цвета можно выводить рентгенограммы, а палитра теплых тонов красновато-оранжевой гаммы хорошо подойдет для изображения заката. При обновлении кадрового буфера, использующего палитру, видеоадаптер должен прочитать индексы из буфера, пропустить их через цветовую таблицу и отправить полученные данные в видеопорт. На аппаратном уровне этот процесс реализуется весьма эффективно. При этом драйвер устройства должен предоставить программам высокого уровня точки входа для управления аппаратной палитрой. Если в графических командах вместо индексов палитры указываются конкретные цветовые значения в формате RGB, они должны быть преобразованы в индексы палитры при записи пикселов или наоборот, — при чтении пикселов. Процесс преобразования значений RGB в индексы палитры сводится к просмотру таблицы и поиску наиболее точного совпадения. Если найти точное совпадение не удается, цвет можно имитировать узором из пикселов, входящих в палитру, с использованием алгоритма смешения (dithering). Преобразование индексов палитры в значения RGB осуществляется простой индексацией. На рис. 5.2 иллюстрируется процесс определения цветов для кадрового буфера с кодировкой 8 бит/пиксел. В 15-разрядных кадровых буферах High Color каждая из основных цветовых составляющих представляется 5 битами. Информация об одном пикселе хранится в 16-разрядном слове; старший бит остается неопределенным, а за ним следуют 5 бит красной, 5 бит зеленой и младшие 5 бит синей составляющих. 15- разрядный формат пикселов часто обозначается сокращением «5:5:5». Кадровый буфер в этом формате может содержать до 32 768 разных цветов.
288 Глава 5. Абстракция графического устройства 8-разрядный Аппаратная палитра з кадрового буфера 1 1 1 1 1 1 1 1 1 00 00 00 00 00 FF 00 FF 00 FF АА 55 FF FF FF FF АА 55 Красный сигнал Зеленый сигнал Синий сигнал Рис. 5.2. Поиск в палитре для 8-разрядного кадрового буфера Формат High Color (16 разрядов) слегка улучшает 15-разрядный формат. Вместо простой потери старшего бита в 16-разрядном слове зеленая составляющая расширяется до 6 бит, поскольку человеческий глаз обладает повышенной чувствительностью к зеленому цвету. В 16-разрядном кадровом буфере один пиксел по-прежнему представляется 16-разрядным словом, обычно в формате 5:6:5. По сравнению с кадровыми буферами True Color использование формата High Color обеспечивает экономию памяти при нормальном количестве цветов и высоких разрешениях. Например, видеоадаптер всего с 2 мегабайтами памяти в 16- разрядном режиме может поддерживать разрешения вплоть до 1152 х 864. Впрочем, есть и обратная сторона — скорость. Запись цветового пиксела из 24-разрядного формата RGB в кадровый буфер High Color не сводится к простому копированию. 8-разрядные составляющие приходится сокращать до 5 или 6 бит, объединять их в соответствии с форматом пикселов и только потом сохранять данные. Преобразование пиксела из кадрового буфера High Color в 24-разрядный формат True Color означает выделение каждой цветовой составляющей при помощи маски и его расширение до 8 бит. Существует несколько вариантов внутреннего формата пикселов, однако Microsoft требует, чтобы производители оборудования использовали фиксированную структуру кадрового буфера. Более того, видеоадаптер, который поддерживает 15-разрядные кадровые буферы, но не поддерживает 16-разрядных, все равно должен имитировать свою поддержку 16-разрядных буферов. Эти требования улучшают совместимость программ с различными устройствами. На рис. 5.3 показан формат пикселов в 15- и 16-разрядных кадровых буферах и маски для выделения красной, зеленой и синей составляющих. В приложениях с особо качественной графикой и играх даже 15- и 16-разрядные кадровые буферы не обеспечивают необходимого разнообразия цветов и плавности цветовых переходов. Например, при выводе изображения в оттенках серого цвета с использованием 15-разрядного кадрового буфера удается вывести лишь 32 уровня серого цвета — каждая цветовая составляющая RGB хранится в 5 битах, что позволяет задействовать 25 уровней интенсивности. В таких
Современные видеоадаптеры 289 приложениях просто необходимо использовать самое лучшее — 24- или 32-разрядные кадровые буферы True Color. И в 24-, и в 32-разрядных кадровых буферах красная, зеленая и синяя составляющие представляются 8 битами. В старых видеоадаптерах старшие 8 бит 32-разрядного пиксела обычно оставались неиспользованными. У новых видеоадаптеров для ОС Windows 98 или Windows 2000 в старших 8 битах хранится информация о прозрачности (transparency). 15-разрядный формат пикселов | Красный (0х7С00) | Зеленый (ОхОЗЕО) | Синий (0x001 F) | R R R R R G G G G G В В В В В Старший бит Младший бит 16-разрядный формат пикселов | Красный (0xF800) | Зеленый (0х07Е0) | Синий (0x001 F) | R R R R R G G G G G G В В В В В Рис. 5.3. Формат пикселов в кадровых буферах High Color Составляющая прозрачности обычно называется альфа-каналом (alpha channel). Эта характеристика определяет весовой коэффициент исходного пиксела при выводе на поверхность. Минимальный альфа-коэффициент равен 0; это означает, что пиксел абсолютно прозрачен и на поверхность вообще не выводится. Максимальный альфа-коэффициент в 8-разрядном альфа-канале равен 255. В этом случае пиксел совершенно не прозрачен, поэтому он просто заменяет соответствующий пиксел принимающей поверхности. При любых промежуточных значениях новый пиксел поверхности вычисляется как взвешенная сумма копируемого пиксела и старого пиксела принимающей поверхности. 24-разрядный формат пикселов часто называется «форматом RGB», а 32-разрядный формат — «форматом ARGB». Структура пиксела в обоих форматах изображена на рис. 5.4. 16 777 216 разных цветов, представляемых 24- или 32-разрядным кадровым буфером, хватает для всех — от фотографа-любителя до профессионала экстракласса. Однако постепенно выяснилось, что с широким распространением режимов High Color и True Color была утрачена гибкость, присущая аппаратным палитрам. Небольшое изменение аппаратной палитры немедленно отражалось на всем экране. Например, если художник хотел слегка отрегулировать насыщенность цветовой гаммы рисунка, ему приходилось изменять значения RGB не более чем в 256 элементах палитры. Но при традиционной структуре кадровых буферов High Color и True Color требуется изменять все пикселы буфера, общий объем которого при разрешении 1024 х 768 и 24-разрядной кодировке составлял 2304 Кбайт. При выводе высококачественных изображений возникала и другая проблема — сопоставление цветов на экране с цветами печатного изображения. Цвет, отображаемый на электронном устройстве, воспринимается нашим глазом не
290 Глава 5. Абстракция графического устройства так, как на бумаге. Профессиональные художники используют так называемую гамма-коррекцию, обеспечивающую дополнительное преобразование цветных пикселов кадрового буфера. Чтобы таблица преобразования имела разумные размеры, каждая из составляющих RGB преобразуется по отдельной таблице, для чего необходимы три таблицы по 256 байт каждая. На аппаратном уровне такое преобразование выполняется микросхемой RAMDAC (RAM digital- to-analog converter). Microsoft требует, чтобы видеоадаптеры поддерживали программируемые (downloadable) микросхемы RAMDAC для кадровых буферов True Color с целью выполнения гамма-коррекции на аппаратном уровне. Stetefftae* !то1зриш ,<t, fisPfyteeC-ap» Graphics Blaster Riva TNT ATTACHED_TO_DESKTOP ;V| PCI WEN ШЕШЕУ 0020&SUBSYS 1 \R E GIS T RY\M achine\Sy sterrAControlS et 640 by 480,8 bits, 320 by 200,8 bits, 320 by 200,8 bits, 320 by 200,8 bits, 320 by 240,8 bits, 320 by 240,8 bits, 320 by 240,8 bits, 320 by 240,8 bits, 400 by 300,8 bits, 400 by 300,8 bits, 400 by 300,8 bits, 400 by 300,8 bits, 480 by 360,8 bits. ***!.'•:*&'Av&s* ■",', 60 Hz. 300 Kb 70 Hz, 62 Kb 72 Hz, 62 Kb 75 Hz, 62 Kb 60 Hz, 75 Kb 70 Hz, 75 Kb 72 Hz, 75 Kb 75 Hz, 75 Kb 60 Hz, 117 Kb 70 Hz, 117 Kb 72 Hz, 117 Kb 75 Hz, 117 Kb 60.Hz,.168Kb.% .^~\ ^f Рис. 5.4. 24- и 32-разрядные форматы пикселов Двойная буферизация, z-буфер и текстуры В компьютерных играх одно из центральных мест занимает анимация — небольшие изображения и даже целые экраны, вид которых меняется с течением времени. Для каждого кадра в анимационной последовательности программа должна стереть некоторые части кадрового буфера и вывести поверх стертых областей новое изображение. В схеме с одним кадровым буфером видеосигнал генерируется по содержимому кадрового буфера в то время, когда программа стирает и перерисовывает его. В результате возникает раздражающее мерцание.
Современные видеоадаптеры 291 Проблема решается посредством использования двух буферов — основного (экранного) и вспомогательного (внеэкранного). Пользователь всегда видит на экране лишь содержимое готового основного буфера, а приложение в это время работает над заполнением внеэкранного буфера. Когда вывод завершается, происходит переключение — основной и вспомогательный буферы меняются местами. Пользователь видит новый основной буфер, а программа начинает работу над новым внеэкранным буфером. При этом пользователь никогда не видит незавершенного изображения, а анимация становится плавной. Методика использования двух буферов называется двойной буферизацией (double buffering). Видеоадаптеры нового поколения должны обеспечивать двойную буферизацию для всего кадрового буфера. Применение двойной буферизации удваивает объем необходимой видеопамяти. В соответствии с табл. 5.1, для поддержки 32-разрядного кадрового буфера в разрешении 1920 х 1200 видеоадаптеру теперь требуется 17,5 Мбайт видеопамяти. Получив запрос на переключение буферов, видеоадаптер должен дождаться вертикального обратного хода луча, то есть момента, когда один цикл обновления изображения полностью завершен, а новый цикл еще не начался. Если переключение буферов не синхронизируется с вертикальным обратным ходом луча, на экране возникают неприятные искажения. В процессе ожидания программа не может записывать данные ни в один из двух буферов, что приводит к напрасным потерям процессорного времени. Для экономии времени на синхронизацию используется схема тройной буферизации, при которой запись может осуществляться в третий кадровый буфер. В этом случае первый буфер содержит изображение, выводимое на экран, второй буфер ожидает вывода, а в третьем буфере строится новый кадр. В трехмерных играх сцена состоит из различных объектов, находящихся на разных расстояниях от зрителя. Ближние объекты блокируют линию видимости, в результате чего дальние объекты частично или полностью скрываются. Для прорисовки трехмерной сцены программа должна рассортировать объекты по расстоянию от зрителя, а это очень сложный и длительный процесс. Ситуация осложняется тем, что пикселы графического объекта тоже могут находиться на разных расстояниях от пользователя (в соответствии с их расположением на объекте). Два соприкасающихся объекта тоже могут частично перекрывать друг друга. Как обычно, эффективное решение проблемы связано с дополнительными затратами памяти. В нем используется дополнительный буфер глубины, называемый z-буфером (по названию третьей координатной оси). В z-буфере хранится глубина каждого пиксела, то есть расстояние от пиксела объекта до зрителя. При запросе на вывод нового пиксела его глубина сравнивается с соответствующей глубиной из z-буфера. Выводятся лишь пикселы с меньшей глубиной, при этом одновременно обновляется содержимое z-буфера. Объем z-буфера в памяти зависит от того, сколько дискретных уровней глубины должна различать программа. 8-разрядный z-буфер обеспечивает 256 уровней глубины; для сколько-нибудь нетривиальных целей этого недостаточно. 16- разрядные z-буферы повышают количество уровней глубины до 65 536 и очень часто используются в современных видеоадаптерах, представленных на рынке. Но в наши дни требования к детализации изображений в играх непрерывно рас-
292 Глава 5. Абстракция графического устройства тут и даже 16-разрядного z-буфера может оказаться недостаточно. При выводе объектов с ошибочным порядком глубин возникает так называемая z-размывка (z-aliasing). Все чаще встречаются видеоадаптеры с 24- и 32-разрядными z-буфе- рами. Некоторые видеоадаптеры поддерживают вещественные z-буферы, повышающие точность измерения глубины. 16-разрядный z-буфер увеличивает размер видеопамяти еще на 0,6-4,4 Мбайт. При использовании 32-разрядного z-буфера эта величина удваивается и доходит до 1,2-8,8 Мбайт. Трехмерные объекты и сцены образуются из трехмерных поверхностей, которые обычно строятся из тысяч элементарных треугольников. Затем на эти треугольники накладываются растры, называемые текстурами, благодаря которым поверхность имитирует вид одежды, песчаной отмели или кирпичной стены. При аппаратном ускорении наложение текстур выполняется аппаратурой видеоадаптера, а не процессором вашего компьютера. Ключевым фактором производительности в этом случае является быстрый доступ к текстурам; для этого видеоадаптер должен хранить растры текстур в видеопамяти вместо того, чтобы извлекать их из системной памяти по медленной и сильно загруженной системной шине. Итак, в памяти видеоадаптера хранятся основной и внеэкранный буферы, z-буфер и, кроме того, мегабайты текстурных растров. В табл. 5.2 приведены возможные конфигурации вашего видеоадаптера. Таблица 5.2. Распределение видеопамяти Использование 8 Мбайт 16 Мбайт 32 Мбайт Основные буферы Внеэкранные буферы Z-буферы Текстуры 1875 Кбайт 800 х 600 х 32 3750 Кбайт 800 х 600 х 32 х 2 938 Кбайт 800 х 600 х 16 629 Кбайт 3072 Кбайт 1024 х 768 х 32 6144 Кбайт 1024 х 768 х 32 х 2 2304 Кбайт 1024 х 768 х 24 4864 Кбайт 3888 Кбайт 1152x864x32 7776 Кбайт 1152x864x32x2 2304 Кбайт 1152x864x32 17 216 Кбайт Как показано в таблице, если на вашем видеоадаптере установлено 32 Мбайт памяти, при разрешении 1152 х 864 с 32-разрядным цветом основной буфер занимает 3888 Кбайт, два внеэкранных буфера в общей сложности занимают 7777 Кбайт и 32-разрядный z-буфер требует еще 3888 Кбайт; для текстурных растров остается 17 216 Кбайт. Но если переключиться в разрешение 1600 х 1200, которое остается намного ниже максимального 1920 х 1440, для текстур остается всего 2768 Кбайт. Одним из способов решения этой проблемы является сжатие, уменьшающее размер текстур. Другой возможный путь — ускорение загрузки текстур из системной памяти в память видеоадаптера. В современной аппаратной архитектуре PC пересылка данных, включая пересылку из системной памяти в видеопамять, осуществляется по шине PCI (Peripheral Component Interconnect).
Современные видеоадаптеры 293 Максимальная скорость передачи шины PCI равна 100 Мбит/с. Новая шина AGP (Accelerated Graphics Port), спроектированная компанией Intel, представляет собой специализированную высокоскоростную шину, обеспечивающую быстрый доступ к текстурам, находящимся в системной памяти. Например, скорость передачи по шине AGP 2X составляет 528 Мбит/с. Видеоадаптеры также могут поддерживать оверлейные поверхности, то есть поверхности, накладываемые на основной экран. В частности, это позволяет выводить телевизионный сигнал поверх обычного экрана. Аппаратное ускорение Функции современного видеоадаптера не ограничиваются предоставлением кадровых буферов, на которых программа выводит изображение, и генерацией видеосигнала по содержимому буфера. В противном случае вычислительной мощности даже самого быстрого процессора общего назначения не хватило бы для воспроизведения трехмерного ролика с приемлемой частотой кадров. Ниже перечислены некоторые возможности, поддерживаемые большинством видеоадаптеров. О Вывод курсора, в том числе с альфа-каналом. О Поддержка двумерной графики: линии и кривые с возможным использованием дробных координат, заливка областей, блиттинг растров, альфа-наложение, градиентные заливки, множественная буферизация и программирование RAMDAC. О Вывод текста, включая сглаживание с использованием глифов нескольких уровней. О Поддержка трехмерной графики: конвейер операций трехмерной графики, различные варианты сглаживания текстур, наложение текстур с учетом перспективы на уровне пикселов, z-буфер, сглаживание краев, сглаживание на уровне пикселов, анизотропная фильтрация, текстуры на базе палитр и т. д. О Видео: декодирование MPEG, декодирование DVD, плавное масштабирование с фильтрацией, вывод видеоинформации в нескольких окнах с преобразованием цветового пространства и фильтрацией, назначение цветовых ключей на уровне пикселов, оверлеи и т. д. Экранное устройство и перечисление режимов Windows 2000 позволяет использовать в одной системе несколько экранных устройств для отображения главного рабочего стола, вывода вспомогательной информации или зеркального воспроизведения экранов в NetMeeting. Многоэкранная поддержка позволяет установить на PC несколько видеоадаптеров, каждый из которых подключается к отдельному монитору. Эти мониторы либо образуют большой виртуальный рабочий стол, либо работают независимо друг от друга. Первый вариант удобен в приложениях, у которых желаемый размер рабочего стола превышает размеры монитора — например, при широкоформатной печати, в программах компьютерной верстки или системах автоматиза-
294 Глава 5. Абстракция графического устройства ции проектирования. Второй вариант хорошо подходит для компьютерных игр, отладки, обучающих программ и презентаций. Зеркальное копирование незаменимо в тех случаях, когда вы хотите передавать содержимое своего экрана другому пользователю по сети. Все графические команды передаются в виде сетевых пакетов на другой компьютер, где и воспроизводится зеркальная копия исходного экрана. Интерфейс Windows GDI/DDI первоначально проектировался как протокол локального вывода — другими словами, предполагалось, что графические команды GDI передаются драйверу экрана на том же компьютере. В этом он отличается от протокола XWindow, используемого в мире UNIX, который проектировался как протокол удаленного вывода. Использование XWindow на рабочих станциях UNIX позволяет вам прийти домой, зарегистрироваться на компьютере, находящемся в офисе за несколько километров от вас, и получить на домашнем компьютере содержимое экрана офисного компьютера, чтобы управлять им в удаленном режиме. Существуют приложения, позволяющие работать с терминальными окнами XWindow в Microsoft Windows. Более того, экран XWindow можно передать нескольким сторонам и позволить каждой из них вносить в него изменения. До этапа официальной поддержки зеркального копирования разработчикам приложений приходилось модифицировать или переписывать драйвер экрана, чтобы организовать передачу графических команд по сети. В Windows 2000 появился отдельный зеркальный драйвер, который «видит» данные, переданные настоящему драйверу экрана. С каждым экранным устройством связывается уникальное имя, по которому на данное устройство можно ссылаться в пользовательских приложениях. Имя задается в форме WADISPLAYx, где х — номер в последовательности, начинающейся с 1. Обратите внимание: в программах C/C++ строка должна записываться в виде WW.WDISPLAYx, поскольку служебный символ \ в строках должен экранироваться. В Windows 2000 появилась новая функция EnumDisplayDevices, предназначенная для перечисления всех экранных устройств, установленных в системе. Приведенная ниже функция фиксирует экранные устройства и заполняет список их именами. void AddDisplayDevices(HWND hList) { DISPLAY_DEVICE Dev; Dev.cb = sizeof(Dev); SendMessageChList. CB_RESETC0NTENT. 0, 0); for (unsigned i-0. EnumDisplayDevices(NULL, i. & Dev, 0); i++) SendMessage(hList. CB_ADDSTRING. 0, (LPARAM) Dev.DeviceName): SendMessage(hList. CB_SETCURSEL. 0, 0); } Для каждого экранного устройства EnumDisplayDevices заносит в структуру DISPLAY_DEVICE имя устройства (например, WADISPLAY1), строку описания устрой-
Современные видеоадаптеры 295 ства (например, NVIDIA RIVA TNT), флаги состояния (например, ATTACHED_TO_DESKTOP | MODESPRUNED|PRIMARY_DEVICE), идентификатор устройства Plug-and-Play и ключ реестра. Зная имя экранного устройства, можно воспользоваться функцией EnumDisplaySettings для получения списка всех форматов кадрового буфера, поддерживаемых устройством, с частотами вертикальной развертки. Код следующего примера выполняет перечисление и заносит в список строку с краткими сведениями о каждом формате. int FrameBufferSizednt width, int height, int bpp) { int bytepp - ( bpp + 7 ) / 8; // Байт на пиксел int byteps - ( width*bytepp + 3 ) / 4*4; // Байт на строку развертки return height * byteps; // Байт на кадровый буфер } void AddDisplaySettingsCHWND hList. LPCTSTR pszDeviceName) { DEVMODE dm; dm.dmSize • sizeof(DEVMODE); dm.dmDriverExtra - 0; SendMessage(hList. LB_RESETCONTENT. 0. 0); for (unsigned i - 0; EnumDisp1aySettings(pszDeviceName. i. & dm); i++ ) { TCHAR szTemp[MAX_PATH]; wsprintf(szTemp. JCXd by Xd. Xd bits. Xd Hz. Xd KB"). dm.dmPelsWidth. dm.dmPelsHeight. dm.dmBitsPerPel. dm.dmDi splayFrequency. FrameBufferSi ze(dm.dmPelsWi dth. dm.dmPelsHei ght. dm.dmBitsPerPel) / 1024 ); SendMessage(hList. LB_ADDSTRING. 0. (LPARAM)szTemp); } } Для каждого режима EnumDisplaySettings заполняет структуру DEVMODE информацией о ширине и высоте кадрового буфера, количестве бит на пиксел, частоте развертки и т. д. Приведенная функция вычисляет размер одного кадрового буфера на основании полученной информации. Учтите, что при использовании структуры DEVMODE необходима осторожность. В Win32 API документирована открытая структура DEVMODE. Драйвер графического устройства может присоединить к открытым полям DEVMODE закрытые данные, для чего размер дополнительных данных указывается в поле dmDriverExtra. Перед вызовом EnumDisplaySettings программа заполняет поле dmSize размером
296 Глава 5. Абстракция графического устройства (открытой части) DEVMODE и обнуляет поле dmDriverExtra, чтобы драйвер экрана не пытался получить дополнительные данные. На прилагаемом компакт-диске имеется программа DEVICE, использующая функции EnumDisplayDevices и EnumDisplaySettings. На странице DisplayDevic.es отображается список экранных устройств, установленных в системе, и поддерживаемые ими форматы кадровых буферов. На рис. 5.5 изображен примерный вид окна программы DEVICE. "jj*i Device Name: Device String:. ' State Flag*: DevicelD: g.etDeviceCaps| DC Attributes Graphics Blaster Riva TNT ATTACHED TO DESKTOP PCISVEN 10DE&DEV 0020&SUBSYS 1 \REGISTRY\Machine\System\ControlSef 640 by 480, 320 by 200, 320 by 200, 320 by 200, 320 by 240, 320 by 240, 320 by 240, 320 by 240, 400 by 300, 400 by 300, 400 by 300, 400 by 300, 480 bv 360. 8 bi 8bi 8 b 8 bi 8 b 8 bi 8 b 8 b 8 b 8bi 8 bi 8 bi 8bi 60 Hz, 70 Hz, 72 Hz, 75 Hz, 60 Hz, 70 Hz, 72 Hz, 75 Hz, 60 Hz, 70 Hz, 72 Hz, 75 Hz, 60 Hz. 300 Kb 62 Kb 62 Kb 62 Kb 75 Kb 75 Kb 75 Kb 75 Kb 117 Kb 117 Kb 117 Kb 117 Kb 168 Kb Рис. 5.5. Перечисление экранных устройств и режимов Контекст устройства Видеоадаптеры образуют лишь один класс графических устройств, поддерживаемых графической системой Windows. Другим важным классом графических устройств являются устройства создания жестких копий — принтеры, плоттеры и факсы. Графическая система Windows NT/2000 имеет многоуровневую архитектуру. Верхний уровень состоит из 32-разрядных клиентских DLL, предоставляю-
Контекст устройства 297 щих функции API в распоряжение пользовательских приложений. Например, GDI32.DLL поддерживает функции GDI API для традиционной двумерной графики; DDRAW.DLL — функции DirectDraw API для программирования двумерной графики в играх, а D3DRM.DLL и D3DIM.DLL — функции Direct3D API для программирования игровой трехмерной графики. Клиентские DLL отображаются в адресное пространство приложения (в часть пользовательского режима). На среднем уровне находится графический механизм, работающий в адресном пространстве режима ядра и обеспечивающий поддержку графического API для всей системы. В графическом адресном пространстве режима ядра находятся сотни обработчиков графических системных функций, вызываемых клиентскими DLL. В Windows NT/2000 графический механизм и часть системы управления окнами, работающая в режиме ядра, объединены в большую DLL режима ядра WIN32K.SYS. Нижний уровень графической системы состоит из драйверов графических устройств, предоставленных производителями оборудования и реализующими интерфейс DDI (Device Driver Interface) в соответствии со спецификацией Microsoft DDK (Device Driver Kit). За подробным описанием графической системы Windows, графических системных функций, интерфейса DDI и драйверов графических устройств обращайтесь к главе 2. Для взаимодействия с драйверами графических устройств графическая система Windows NT/2000 использует внутреннюю структуру данных, называемую контекстом устройства (device context). На самом деле контекст устройства представляет собой сложную иерархию структур и объектов, объединенных посредством указателей и находящихся как в адресном пространстве пользовательского режима приложения, так и в системном адресном пространстве ядра. В главе 3 подробно проанализировано внутреннее строение контекста устройства и других структур данных графической системы. Контекст устройства решает две важные задачи в графической системе. Главной задачей является абстракция графического устройства, чтобы все компоненты, расположенные выше драйвера устройства (в том числе графический механизм, клиентские DLL Win32 и пользовательское приложение), могли быть аппаратно-независимыми. Кроме того, в контексте устройства сохраняются часто используемые графические атрибуты — например, цвет фона, растровая операция, перо, кисть, шрифт и т. д., чтобы их значения не приходилось задавать в каждой графической команде. Клиентская DLL Win32 GDI скрывает реальный контекст устройства от пользовательских приложений. Приложению предоставляется лишь манипулятор контекста устройства — 32-разрядное число недокументированного формата. Манипулятор возвращается при создании контекста устройства средствами GDI, а затем передается GDI при всех последующих запросах на выполнение графических операций. Механизм манипуляторов скрывает реализацию надежнее, чем указатели this в C++ и интерфейсные указатели СОМ. Кроме того, он существенно расширяет свободу действий Microsoft по созданию совместимых реализаций в разных системах. Например, программы Win32 работают как в Windows 95/95, так и в Windows NT/2000, хотя в этих классах систем манипуляторы контекстов устройств реализованы по-разному.
298 Глава 5. Абстракция графического устройства Создание контекста устройства Контекст устройства создается функцией Win32 CreateDC: HDC CreateDC (LPCSTR pszDriver, LPCSTR pszDevice, LPCSTR pszOutput. CONST DEVMODE * pdvmlnit): Первый параметр, pszDriver, в программах Win32 для передачи имени драйвера графического устройства не используется — это пережиток эпохи Win 16 API. Допустимыми значениями этого параметра являются только DISPLAY (контекст экранного устройства), NULL и WINSP00L (контекст устройства для принтера). Второй параметр, pszDevice, определяет имя графического устройства. В нем может передаваться как имя экранного устройства, возвращаемое функцией Enum- DisplayDevices, так и имя принтера, указанное в мини-приложении Printers (Принтеры) панели управления. Например, значение WW.WDISPLAY1 или NULL обозначает первичное экранное устройство; значение WW.WDISPLAY2 обычно соответствует вторичному экранному устройству, WW. WDISPLAY3 может обозначать драйвер NetMeeting, обеспечивающий зеркальное воспроизведение экрана на другом мониторе. Третий параметр определяет имя порта, в который передается задание печати. В Win32 для передачи имени порта используется новая функция StartDoc, поэтому этот параметр всегда должен быть равен NULL. Последний параметр, pdvmlnit, содержит указатель на структуру DEVMODE с описанием параметров инициализации. Обычно передается значение NULL — это означает, что драйвер устройства должен использовать текущую конфигурацию устройства, хранящуюся в реестре. Для экранных устройств pdvmlnit может указывать на структуру DEVMODE, возвращаемую функцией EnumDisplaySettings и содержащую высоту, ширину, количество бит на пиксел и частоту вертикальной развертки. Для устройств создания жестких копий pdvmlnit может указывать на структуру DEVMODE, возвращаемую функцией DocumentProperties. Функция CreateDC возвращает манипулятор контекста устройства GDI, который используется при последующих вызовах функций GDI вплоть до последнего вызова DeleteDC, который освобождает системные ресурсы, занятые контекстом устройства; после этого манипулятор контекста становится недействительным. Процесс создания контекста устройства очень сложен. По имени устройства операционная система получает из реестра имя драйвера устройства и загружает драйвер. Для экрана монитора драйвер загружается только при первом вызове CreateDC, а при последующих вызовах используется ранее загруженный драйвер. Фактическая загрузка и инициализация драйвера принтера также происходит при вызове CreateDC. Информация о создании нового контекста устройства для принтера также передается спулеру и библиотеке DLL, обеспечивающей пользовательский интерфейс с драйвером принтера. При загрузке драйвера графического устройства вызывается его главная точка входа DrvEnableDriver. Функция DrvEnableDriver заполняет структуру с номером версии и адресами точек входа всех функций DDI, реализуемых драйвером. Затем графический механизм вызывает функцию DrvEnablePDEV, требуя, чтобы драйвер описал свои атрибуты и возможности, а также создал свою структуру
Контекст устройства 299 данных физического устройства. Параметры pdvmlnit и pszDevice, переданные при вызове CreateDC, передаются функции DrvEnablePDEV. Функция DrvEnablePDEV заполняет две важные структуры, GDI INFO и DEVINF0, информацией об атрибутах, возможностях, форматах пикселов и стандартных параметрах устройства. Графический механизм создает свою внутреннюю структуру физического устройства с информацией, возвращаемой функциями DrvEnableDriver и DrvEnablePDEV. Теперь он знает возможности графического устройства и адреса точек входа, к которым следует обращаться при вызове различных графических команд DDL В завершающей фазе создания контекста устройства графический механизм вызывает функцию драйвера DrvEnableSurface; драйвер создает графическую поверхность, на которой и происходит фактический вывод. За подробностями обращайтесь к главе 2 (описание интерфейса DDI) и главе 3 (внутренние структуры данных). Получение информации о возможностях устройства Контекст устройства хранит (непосредственно или в виде ссылок) большой объем данных о графическом устройстве и его драйвере. Часть информации можно получить при помощи вызовов Win32 API; другая часть хранится во внутренних структурах GDI для упрощения взаимодействия с драйвером. Функция GetDeviceCaps возвращает информацию об атрибутах или возможностях графического устройства по целочисленному индексу: int GetDeviceCaps (HDC hDC. int nlndex); При помощи функции GetDeviceCaps приложение получает конкретную информацию о графическом устройстве — например, формате кадрового буфера, возможности обработки цветов, разрешении, палитре, физических размерах, размерах полей, поддержке альфа-наложения и градиентных заливок, поддержке ICM (Image Color Management), а также о возможностях и ограничениях DDL Большинство возможностей не представляет интереса для прикладных программ; это всего лишь рекомендации, управляющие взаимодействием графического механизма с драйвером устройства. Некоторые флаги ориентированы на 16-разрядные драйверы графических устройств, используемые в Windows 3.1, Windows 95 и Windows 98. Например, флаг CC_ELLIPSES в запросе CURVECAPS показывает, способен ли драйвер устройства нарисовать эллипс. В интерфейсе DDI систем Windows NT/2000 этот флаг отсутствует, поскольку все эллиптические кривые до передачи драйверу устройства преобразуются в кривые Безье. Когда приложение обращается с запросом по индексу CURVECAPS, Windows NT/2000 возвращает стандартный ответ просто для того, чтобы не нарушить работу старых приложений. В табл. 5.3 перечислены индексы и возвращаемые значения функции GetDeviceCaps. При выводе на экран вызов GetDeviceCaps позволяет получить очень важную информацию о том, поддерживает ли контекст устройства аппаратную палитру. Программа, осуществляющая вывод в контексте с поддержкой палитры, должна создать логическую палитру, выбрать ее в контексте устройства перед началом вывода и обрабатывать сообщения, связанные с палитрой.
300 Глава 5. Абстракция графического устройства Таблица 5.3. GetDeviceCaps: индексы и возвращаемые значения (Windows NT/2000) Индекс Пример Возвращаемое значение и смысл DRIVERVERSION TECHNOLOGY H0RSIZE VERTSIZE LINECAPS 0x4001 DT RASDISPLAY 320 240 H0RZRES VERTRES BITSPIXEL PLANES NUMBRUSHES NUMPENS NUMFONTS NUMCOLORS CURVECAPS 1024 768 8,16,24,32 1 -1 -1 0 -1 OxlFF OxFE Версия драйвера; 16 бит в формате OxXYZZ, где X — основная версия ОС, Y — дополнительная версия ОС, a ZZ — номер версии драйвера. Информация сообщается драйвером Информация сообщается драйвером. DTPLOTTER — для плоттеров, DTRAS DIS P LAY — для растровых видеоадаптеров, DTRASPRINTER — для растровых принтеров, DTRASCAMERA — для растровых камер, DTCHARSTREAM — для символьных потоков Ширина физической поверхности в миллиметрах. Для экрана выводится приблизительное значение. Информация сообщается драйвером Высота физической поверхности в миллиметрах. Для экрана выводится приблизительное значение. Информация сообщается драйвером Ширина физической поверхности в пикселах. Информация сообщается драйвером Высота физической поверхности в пикселах. Информация сообщается драйвером Количество смежных битов в каждой цветовой плоскости. Информация сообщается драйвером Количество цветовых плоскостей. Информация сообщается драйвером Количество кистей устройства Количество перьев устройства. Для плоттеров — количество физических перьев Количество шрифтов устройства Количество элементов в палитре устройства или -1, если палитра отсутствует Возможности вывода кривых. Стандартный ответ: CC_CH0RD | CC_CIRCLES | CCJLLIPSES | CCJNTERI0RS | CC_PIE | CC_R0UNDRECT|CC_STYLED|CC_WIDE|CCWIDESTYLED Возможности вывода линий. Стандартный ответ: LC_P0LYLINE|LC_MARKER|LC_P0LYMARKER|LC_WIDE|LC_STYLED| LC_WIDESTYLED | LCJNTERI0RS
Контекст устройства 301 Индекс Пример Возвращаемое значение и смысл P0LYG0NALCAPS OxFF TEXTCAPS CLIPCAPS RASTERCAPS ASPECTX ASPECTY ASPECTXY LOGPIXELSX LOGPIXELSY SIZEPALETTE NUMRESERVED 0x7807 0х7е99 36 36 51 96 или 120 96 или 120 0, 16 или 256 2 или 20 Возможности вывода многоугольников. Стандартный ответ: PC_P0LYG0N|PC_RECTANGLE|PC_WINDP0LYG0N| PC_SCANLINE | PC_WIDE | PC_STYLED | PC_WIDESTYLED | PC_INTERI0RS. Обратите внимание: PCP0LYP0LYG0N и PCPATHS не включаются в стандартный ответ, но это не значит, что они не поддерживаются устройством — просто при сообщении возможностей допущена ошибка Возможности вывода текстовых строк. Информация сообщается драйвером Возможности отсечения. Стандартный ответ: CPRECTANGLE. Обратите внимание: это не означает, что устройство не может выполнять отсечение по сложным регионам Растровые возможности. Частично сообщаются драйвером, частично берутся из стандартного ответа: RC_BITBLT|RC_BITMAP64|RC_GDI20_OUTPUT|RC_DI_BITMAP| RC_DIBT0DEV | RCJIGF0NT | RCJTRETCHBLT | RCJLOODFILL | RC_STRETCHDIB|RC_0P_DX_0UTPUT Относительная ширина пиксела устройства в интервале от 1 до 100. Информация сообщается драйвером Относительная высота пиксела устройства в интервале от 1 до 100. Информация сообщается драйвером Относительная диагональ пиксела устройства, Sqrt(ASPECTX*2+ASCPECTY*2). Информация сообщается драйвером Логическое разрешение в точках на дюйм (dpi) по ширине. Информация сообщается драйвером Логическое разрешение в точках на дюйм (dpi) по высоте. Информация сообщается драйвером Количество элементов в системной палитре. Значение действительно лишь в том случае, если RASTERCAPS содержит флаг RC_PALETTE Количество зарезервированных элементов в системной палитре. Значение действительно лишь в том случае, если RASTERCAPS содержит флаг RCPALETTE. Ответ генерируется в зависимости от системных настроек C0L0RRES 24 Количество бит в представлении одного пиксела Продолжение &
302 Глава 5. Абстракция графического устройства Таблица 5.3. Продолжение Индекс Пример Возвращаемое значение и смысл PHYSICALWIDTH О PHYSICALHEIGHT О PHYSICALOFFSETX О PHYSICALOFFSETY О SCALINGFACTORX 100 SCALINGFACTORY 100 VREFRESH 60 DESKT0PH0RZRES 1024 DESKT0PVERTRES 768 BLTALIGNMENT 0 SHADEBLENDCAPS 0 C0L0RMGMT CAPS 2 Для устройств создания жестких копий: ширина физической страницы в единицах устройства. Информация сообщается драйвером Для устройств создания жестких копий: высота физической страницы в единицах устройства. Информация сообщается драйвером Для устройств создания жестких копий: ширина непечатаемого левого поля в единицах устройства. Информация сообщается драйвером Для устройств создания жестких копий: высота непечатаемого верхнего поля в единицах устройства. Информация сообщается драйвером Для устройств создания жестких копий: коэффициент масштабирования по оси х Для устройств создания жестких копий: коэффициент масштабирования по оси у Вертикальная частота развертки для текущего видеорежима. Информация сообщается драйвером Ширина рабочего стола в пикселах (отличается от ширины одного экрана при работе с несколькими мониторами) Высота рабочего стола в пикселах (отличается от высоты одного экрана при работе с несколькими мониторами) Предпочтительное выравнивание при блиттинге на устройство. Нулевое значение означает, что для устройства используется аппаратное ускорение, поэтому выравнивание может быть произвольным. Информация сообщается драйвером Возможности альфа-наложения и градиентной заливки. Информация сообщается драйвером Возможности управления цветом. Информация сообщается драйвером Перед выводом на устройство создания жестких копий хорошо написанная программа всегда должна проверять точные размеры бумаги и полей. Следует помнить, что приложение может изменить ориентацию листа и перейти от стандартной книжной ориентации к альбомной; при этом ширина и высота листа, а также левые и верхние поля меняются местами.
Контекст устройства 303 Для приложений, работающих под управлением Windows NT/2000, проверка графических возможностей устройства (CURVECAPS, LINECAPS, POLYGONCAPS, CLIPCAPS и т. д.) уже не столь важна, поскольку при необходимости графический механизм помогает драйверу устройства в обработке графических команд GDI. Проверяя возможности контекста устройства, приложение также может оптимизировать свое быстродействие. Например, если устройство не поддерживает градиентных заливок, приложение может имитировать заливку своими средствами, упростить или вообще отказаться от градиентной заливки. Приложения, работающие в 8-разрядных режимах с 256 цветами, могут использовать графические заготовки с уменьшенным количеством цветов (вместо 24-разрядных). В системе Windows NT/2000 графический механизм хранит гораздо больше информации об устройстве, чем можно получить при помощи функции Get- DeviceCaps, предназначавшейся для 16-разрядного GDI. Драйвер графического устройства должен сообщить информацию о выводе стилевых линий, о многочисленных полутоновых параметрах, о поддержке устройством цветового пространства CIE (Commission Internationale de L'Eclairage) и некоторых внутренних графических возможностях. Например, графическому механизму может потребоваться информация о том, поддерживает ли устройство непрозрачные прямоугольники при выводе текста, поддерживается ли спулинг EMF, допускается ли закраска непрозрачных текстовых прямоугольников произвольной кистью, поддерживаются ли дробные координаты при выводе текста, поддерживается ли 4-разрядное сглаживание текста и т. д. Щшш<ЩШ Б щ ?<4% TECHNOLOGY DRIVERVERSION H0RZSIZE VERTSIZE H0RZRES VERTRES L0GPIXELSX L0GPIXELSY BITSPIXEL PLANES NUMBRUSHES NUMPENS NUMMARKERS NUMFONTS NUMCOLORS PDEVICESIZE CURVECAPS LINECAPS ЩМШШ. 1 0x4000 320 mm 240 mm 1152 pixels 864 pixels 96 dpi 96 dpi 32 bits 1 planes -1 -1 0 0 -1 0 Iff fe Ытштшшж , , •з$Г~* Рис. 5.6. Получение информации о возможностях графического устройства
304 Глава 5. Абстракция графического устройства Функция GetDeviceCaps работает элементарно и не требует пространных комментариев. В программе DEVICE кнопка GetDeviceCaps на странице Display Devices открывает диалоговое окно с перечнем всех флагов устройства (рис. 5.6). Атрибуты в контексте устройства В графических командах GDI используются два вида данных — атрибуты, общие для разных команд, и значения, специфические для конкретной команды. Конечно, было бы крайне неэффективно требовать, чтобы общие параметры и атрибуты снова и снова указывались в программе. В Windows GDI в контексте устройства хранятся следующие атрибуты: О система координат, режим отображения и мировое преобразование; О основной цвет, цвет фона, палитра и параметры управления цветом; О параметры вывода линий; О параметры заливки областей; О шрифт, межсимвольные интервалы и выравнивание текста; О режим масштабирования растров; О регион отсечения; О ряд других атрибутов. У каждого атрибута имеется набор допустимых значений и значение по умолчанию, заносимое в контекст устройства при создании. Для каждого атрибута обычно определяется пара функций Win32 API, предназначенных для чтения и присваивания ему нового значения. В табл. 5.4 перечислены атрибуты контекста устройства, значения по умолчанию и функции для работы с ними. Таблица 5.4. Атрибуты контекста устройства (Windows 2000) Атрибут Ассоциированный манипулятор окна Базовая точка контекста Растр Графический режим Режим отображения Габариты области просмотра Значение по умолчанию NULL Растр 1x1 GM_C0MPATIBLE MMJTEXT {1,1} Функции доступа WindowFromDC (только чтение) GetDCOrgEx (только чтение) GetCurrentObject, SelectObject (только для совместимых контекстов устройств) GetGraphicsMode, SetGraphicsMode GetMapMode, SetMapMode GetVi ewportExtEx, SetVi ewPortExtEx, Базовая точка области про- {0, 0} смотра SealeViewportExtEx GetVi ewportOrgEx, SetViewportOrgEx, OffsetVi ewportOrgEx
Контекст устройства 305 Атрибут Значение по умолчанию Функции доступа Габариты окна Базовая точка окна {1,1} {0,0} GetWindowExtEx, SetWindowExtEx, ScaleWindowExtEx GetWindowOrgEx, SetWindowOrgEx, OffsetWindowOrgEx Преобразование Цвет фона Цвет текста Палитра Регулировка цвета Цветовое пространство Режим ICM Профиль ICM Текущая позиция пера Бинарная растровая операция Режим вывода фона Логическое перо Цвет пера DC Направление дуг Угловой лимит Логическая кисть Цвет кисти DC Базовая точка кисти Режим заполнения многоугольников Матрица тождественного преобразования Системный цвет фона Черный DEFAULT_PALETTE {0,0} R2_C0PYPEN OPAQUE BLACK_PEN 10.0 WHITE_BRUSH {0,0} ALTERNATE GetWorldTransform, SetWorldTransform, Modi fyWorldTransform GetBkColor, SetBkColor GetTextColor, SetTextColor GetCurrentObject, EnumObjects, SelectPalette GetColorAdjustment SetColorAdjustment GetColorSpace, SetColorSpace SetlCMMode GetlCMProfile, SetlCMProfile GetCurrentPositionEx, MoveToEx, LineTo, BezierTo,... GetR0P2, SetR0P2 GetBkMode, SetBkMode SelectObject, GetCurrentObject GetDCPenColor, SetDCPenColor GetArcDirection, SetArcDirection GetMiterLimit, SetMiterLimit SelectObject, GetCurrentObject GetDCBrushCoior, SetDCBrushColor GetBrushOrgEx, SetBrushOrgEx GetPolyFillMode, SetPolyFillMode Продолжение &
306 Глава 5. Абстракция графического устройства Таблица 5.4. Продолжение Атрибут Значение по умолчанию Функции доступа Режим масштабирования растров Логический шрифт Дополнительные межсимвольные интервалы Флаги подстановки шрифтов Выравнивание текста Выключка текста (выравнивание по ширине) Раскладка Траектория Область отсечения Метарегион Ограничивающие прямоугольники STRETCH_ANDSCANS Системный шрифт TA_TOP|TA__LEFT {0,0} Клиентская область, вся поверхность устройства GetStretchBltMode, SetStretchBltMode SelectObject, GetCurrentObject, GetCharWidth32, GetKerningPairs, GetTextMetrics,... GetTextCharacterExtra, SetTextCharacterExtra SetMapperFlags GetTextAl1gn, SetTextAl1gn SetTextJustification (только присваивание) GetLayout, SetLayout BeginPath, ClosePath, EndPath, GetPath SelectObject, GetClipBox, GetClipRgn, SelectCli pRgn, ExcludeCli pRect, IntersectClipRect GetMetaRgn, SetMetaRgn GetBoundsRect, SetBoundsRect Эти атрибуты контекста устройства будут подробно рассмотрены ниже. А пока вы лишь получили общее представление о количестве информации, хранящейся в контексте устройства. В таблице перечислены атрибуты контекстов устройств, поддерживаемые в Windows 2000. Список включает почти все атрибуты, поддерживаемые на разных Windows-платформах. Большинство атрибутов было унаследовано из 16- разрядного Windows API. Некоторые атрибуты (например, мировые преобразования координат) в полной мере поддерживаются только в Windows NT/2000. В Windows 98 и Windows 2000 появился ряд новых атрибутов — например, кисть DC, перо DC и атрибуты ICM. В программе DEVICE кнопка DC Attributes на странице Display Devices вызывает диалоговое окно для вывода списка всех доступных атрибутов контекста (рис. 5.7). В этом диалоговом окне предусмотрено несколько возможностей получения манипулятора контекста устройства. В частности, программа может создать новый контекст функцией CreateDC или получить контексты устройств, связанные с различными окнами.
Контекст устройства 307 Рис. 5.7. Атрибуты контекста устройства Связь контекста устройства с окном Контекст устройства, созданный функцией CreateDC, можно рассматривать как графическую поверхность, распространяющуюся на всю площадь устройства — на весь экран для экранных устройств или на всю страницу для принтеров. Однако функция CreateDC не предоставляет стандартный способ получения контекста устройства в среде Microsoft Windows и обычно применяется только при работе с устройствами создания жестких копий (таких, как принтеры). Графический вывод в многооконной среде Графический вывод в первую очередь ориентируется на экран монитора — ресурс, совместно используемый несколькими приложениями в операционной системе Windows. Обычные приложения Windows работают в оконном режиме, при котором вывод каждого приложения ограничивается определенной частью экрана. Окно обычно имеет прямоугольную форму, а его параметры указываются при вызове функции CreateWindow. Впрочем, операционная система позволяет создать окно произвольной формы — в виде прямоугольника с закругленными углами, эллипса или многоугольника. Чтобы изменить форму окна, достаточно создать объект региона и передать его манипулятор функции SetWindowRgn. Регион задается в экранных координатах относительно базовой точки окна. Ниже приведен простой пример создания обычного окна с последующим преобразованием его к эллиптической форме.
308 Глава 5. Абстракция графического устройства const TCHAR szProgram [] - JT'Window Region"); const TCHAR szRectWin [] = _T("Rectangular Window"); const TCHAR szEptcWin [] = _T("Elliptic Window"); int WINAPI WinMain(HINSTANCE hlnstance. HINSTANCE. LPSTR, int) { HWND hWnd = CreateWindow(_T("EDIT"), NULL. WS_OVERLAPPEDWINDOW, 10. 10. 200. 100. GetDesktopWindowO. NULL, hlnstance. NULL): ShowWindow(hWnd. SW_SH0W); SetWindowText(hWnd. szRectWin); MyMessageBoxCNULL. szRectWin. szProgram. MB_0K); HRGN hRgn « CreateEllipticRgnCO. 0. 200. 100): SetWindowRgn(hWnd. hRgn. TRUE); SetWi ndowText(hWnd. szEptcWi n); MessageBox(NULL. szEptcWin. szProgram. MB_0K); DestroyWindow(hWnd); return 0; } На рис. 5.8 изображены два окна: обычное прямоугольное и эллиптическое. Прямоугольное окно (слева) имеет обычную для окон верхнего уровня рамку и строку заголовка, тогда как у эллиптического окна рамка и строка заголовка отсекаются по границам эллиптического региона. Rectangular Window elliptic Window Рис. 5.8. Окна разной формы ПРИМЕЧАНИЕ ■ Непрямоугольные окна и специализированные неклиентские области считаются новым течением в разработке пользовательских интерфейсов Windows-приложений. При выводе в окнах нетрадиционной формы используются регионы, определяемые при помощи растровых или векторных изображений. Обработка неклиентских сообщений заменяет стандартную обработку неклиентской области.
Контекст устройства 309 Несколько окон, одновременно находящихся на экране, могут перекрывать друг друга. В старой реализации Microsoft Windows передние окна полностью или частично блокировали окна, находящиеся сзади, хотя в своей последней реализации Windows 2000 компания Microsoft стремится к интерпретации окон как экранных объектов, которые могут объединяться с использованием различных операторов. В результате перекрытия у каждого окна появляется еще один важный атрибут — видимая часть. Окно делится на две части: клиентскую и неклиентскую. Неклиентская часть включает рамку, строку заголовка, строку меню, панели инструментов, полосы прокрутки и прочие служебные элементы. К клиентской части относится вся площадь окна, не входящая в неклиентскую часть, — как правило, это прямоугольная область в середине окна. Вывод в неклиентской области обычно обеспечивается стандартной функцией окна, DefWindowProc, представляемой модулем управления окнами (user32.dll). DefWindowProc имеет доступ к стилю окна, тексту окна, информации фокуса и другим данным, необходимым для прорисовки неклиентской области. В большинстве случаев пользовательское приложение обеспечивает вывод в клиентской части окна. В многозадачных средах типа Windows состояние экрана неустойчиво. В любой момент времени какое-нибудь приложение может вызвать временное окно, вывести информацию и прекратить свое существование. Приложения, продолжающие работать, несут полную ответственность за восстановление нормального изображения на экране. В этом случае операционная система посылает окнам, чье изображение было нарушено, сообщения с запросом на перерисовку. Окно- получатель не знает, что произошло на экране, поэтому ему нужно точно сообщить, как часть изображения нуждается в перерисовке. В противном случае перерисовка всего окна приведет к напрасному расходованию драгоценных ресурсов. Ниже перечислены факторы, которые должны учитываться контекстом устройства при выводе в окне. О Базовая точка — левый верхний угол окна. О Размеры — ширина и высота окна. О Регион окна — подмножество прямоугольной области, определяемой базовой точкой и размерами окна (для окон нетрадиционной формы). О Видимость — перерисовывается только видимая (не перекрытая) часть региона окна. О Все окно или клиентская область — хочет ли приложение обеспечить специализированную прорисовку неклиентской области или его интересы ограничиваются клиентской областью? О Обновляемая область — участок окна, реально нуждающийся в обновлении. Получение контекста устройства, связанного с окном Функция CreateDC не позволяет приложению создать контекст устройства, связанный с конкретным окном. В Win32 API предусмотрено несколько функций для создания контекстов устройств, связанных с окнами:
310 Глава 5. Абстракция графического устройства HDC GetWindowDC (HWND hWnd); HDC GetDC(HWND hWnd); HDC GetDCEx(HWND hWnd, HRGN hrgnClip. DWORD flags); HDC BeginPaintCHWND hWnd, LPPAINTSTRUCT lpPaint); Функция GetWindowDC возвращает контекст устройства, подготовленный к выводу во всем окне, включая строку заголовка, меню и полосы прокрутки. Базовая точка контекста устройства совпадает с базовой точкой окна. Функция GetDC возвращает контекст устройства, подготовленный к выводу только в границах клиентской части окна. Базовая точка контекста устройства совпадает с базовой точкой клиентской области. Ни GetWindowDC, ни GetDC не учитывают флагов стиля WS_CLIPCHILDREN и WS_CLIPSIBLINGS. Другими словами, возвращаемые ими манипуляторы позволяют осуществлять вывод поверх дочерних и соседних (sibling) окон. После завершения вывода контекст устройства необходимо вернуть операционной системе. Функция ReleaseDC освобождает ресурсы, связанные с манипулятором контекста устройства, полученным при вызове GetWindowDC, GetDC или GetDCEx. Вызову BeginPaint должен быть сопоставлен парный вызов EndPaint. Контекст устройства содержит ряд параметров, отражающих его связь с конкретным окном. Функция WindowFromDC возвращает манипулятор окна, с которым связан данный контекст. Базовая точка контекста устройства, возвращаемая функцией GetDCOrgEx и всегда равная {0,0} для контекстов, созданных функцией CreateDC, содержит экранные координаты базовой точки окна или его клиентской области. Кроме этих документированных атрибутов, доступных только для чтения, контекст устройства содержит ряд недокументированных полей. В частности, в контексте устройства хранится базовая точка и размеры окна, связанного с контекстом. Мы будем называть этот прямоугольник прямоугольником вывода (display rectangle), хотя на самом деле он относится только к выводу клиентской области окна (отсюда и его «официальное» называние erclWindow). Информация о видимой части региона окна хранится в объекте-регионе. Если окно полностью видно на экране, видимый регион контекста представляет собой прямоугольник, размеры которого совпадают с размерами изображения. Если угол окна закрыт другим окном, видимый регион контекста изменяется и представляет собой объединение двух прямоугольников. Например, видимый регион контекста устройства, возвращаемого функцией GetWindowDC, может объединять два прямоугольника: {10,10,210,62} и {10,62,92,110}. Обратите внимание: значение атрибута видимого региона присваивается перед возвращением из GetWindowDC или GetDC, однако атрибут продолжает обновляться операционной системой по мере того, как другие окна создаются и уничтожаются, передают фокус, изменяют размеры или положение на экране. Такой подход гарантирует, что связь манипулятора контекста устройства с конкретным окном будет сохраняться и при этом вам не придется беспокоиться об изменениях в атрибутах окна. Если с окном связан нестандартный регион окна, назначенный функцией SetWindowRgn (в приведенном выше примере — эллипс), то видимый регион контекста устройства представляет собой видимое подмножество пересечения региона окна с прямоугольником окна. Другими словами, в видимый регион контекста устройства включаются только те пикселы, которые удовлетворяют всем
Контекст устройства 311 трем условиям — они входят в регион окна, назначенный функцией SetWindowRgn, принадлежат прямоугольнику окна и являются видимыми. В нашем примере с эллиптическим окном видимый регион состоит из десятков прямоугольников или, выражаясь точнее, — из десятков структур SCAN, используемых для более эффективного представления регионов в Windows NT/2000. Третий способ получения контекста устройства, связанного с конкретным окном, предоставляет функция GetDCEx. Функция GetDCEx по сравнению с GetDC или GetWindowDC получает два дополнительных параметра — объект-регион и флаг. Хотя в документации Microsoft утверждается, что регион определяет границы области отсечения, он не совпадает с регионом отсечения, которым приложение управляет при помощи функций SelectClipRgn и ExtSelectClipRgn. Поскольку в электронной документации MSDN не упоминаются два важных флага функции GetDCEx, мы перечислим все флаги здесь (табл. 5.5). Таблица 5.5. Флаги GetDCEx Флаг Описание DCX_WINDOW DCX_CACHE DCX_PARENTCLIP DCX_CLIPSIBLINGS DCX_CLIPCHILDREN DCX_NORESETATTRS DCX_LOCKWINDOWUPDATE DCXJXCLUDERGN DCXJNTERSECTRGN DCXJXCLUDEUPDATE DCXJNTERSECTUPDATE DCXJALIDATE 0x10000 Вернуть контекст устройства для прямоугольника окна (вместо прямоугольника клиентской области) Использовать контекст устройства из кэша диспетчера окон, даже если у класса окна установлен флаг стиля CS0WNDC или CS_CLASSDC Использовать прямоугольник и видимый регион родительского окна, не обращая внимания на флаги стиля родительского окна WSCLIPCHILDREN и CS_PARENTDC Исключить из видимого региона все регионы соседних окон Исключить из видимого региона все регионы дочерних окон Не восстанавливать значения по умолчанию для атрибутов контекста устройства Игнорировать блокировку обновления, установленную функцией LockWindowUpdate Исключить регион, заданный параметром hrgnClip, из видимого региона Построить новый видимый регион как пересечение региона, заданного параметром hrgnClip, с текущим видимым регионом Исключить обновляемый регион окна из видимого региона Построить новый видимый регион как пересечение обновляемого региона с текущим видимым регионом Объявить содержимое окна действительным — другими словами, сбросить обновляемый регион Недокументированный флаг, который автоматически делает вызов GetDCEx успешным
312 Глава 5. Абстракция графического устройства При таком обилии флагов GetDCEx может использоваться для замены других функций. Скажем, вызов GetDCEx(hWnd, NULL, DCX_WINDOW|DCX_NORESETATTRS) легко заменяет GetWindowDC(hWnd), а вызов GetDCEx(hWnd. NULL, DCXNORESETATTRS) заменяет GetDC(hWnd). При помощи дополнительных флагов можно отменить использование системой контекста устройства, принадлежащего окну или классу, а также исключить из видимого региона соседние и дочерние окна. Кроме того, функция GetDCEx позволяет видоизменить видимый регион с использованием дополнительного параметра-региона или обновляемого региона окна и даже сбросить данные обновляемого региона окна. Мы добрались до последнего способа создания контекста устройства, связанного с конкретным окном. Функция BeginPaint возвращает контекст устройства, предназначенный для обработки сообщения WMPAINT. Если рассматривать BeginPaint только с точки зрения возвращаемого контекста, ее можно реализовать следующим образом: HDC BeginPaintO(HWND hWnd. LPPAINTSTRUCT lpPaint) { DWORD flags = 0; if ( GetWindowLong(hWnd, GWLJTYLE) & WS_CLIPCHILDREN) flags |= DCX_CLIPCHILDREN; if ( GetWindowLong(hWnd. GWL_STYLE) & WS_CLIPSIBLINGS) flags |= DCX_CLIPSIBLINGS; return GetDCEx(hWnd. NULL, flags | DCXJNTERSECTUPDATE | DCXJALIDATE); } Функция BeginPaint проверяет стиль окна и определяет, нужно ли исключить из видимого региона дочерние и соседние окна, после чего определяет пересечение видимого региона с обновляемым регионом окна. При этом содержимое окна объявляется действительным, то есть обновляемый регион сбрасывается. Флаги DCXCACHE и DCXNORESETATTRS в данном случае не используются, поэтому функция GetDCEx должна проверить стиль окна и узнать, как следует поступить в этой ситуации. Впрочем, настоящая реализация BeginPaint решает и другие задачи. Например, если каретка находится в выводимом регионе, BeginPaint скрывает ее, чтобы предотвратить возможное стирание каретки. Функция BeginPaint посылает сообщение WMERASEBKGND обработчику сообщений окна. Если приложение обрабатывает это сообщение, оно получает возможность вывести однородный или растровый фон. Если сообщение передается стандартной функции окна DefWindowProc и в классе окна имеется кисть для закраски фона, то эта кисть используется для стирания перерисовываемого региона. Кроме того, функция BeginPaint также должна занести в структуру PAINTSTRUCT манипулятор созданного контекста устройства, ограничивающий прямоугольник перерисовываемого региона и некоторые флаги. Вероятно, вы достаточно четко представляете себе отличия между контекстом устройства, связанным с конкретным окном, и контекстом, созданным функцией CreateDC. Главное отличие заключается в том, что к числу атрибутов первого относится прямоугольник вывода, являющийся подмножеством поверхности
Контекст устройства 313 устройства, и объединенный видимый регион, который строится с учетом таких факторов, как регион окна, отсечение дочерних и соседних окон, видимых частей и обновляемого региона окна. Общий контекст устройства Необходимо ответить еще на один вопрос, который нередко приводит к недоразумениям, — откуда берутся контексты устройства, возвращаемые функциями GetDC, GetWindowDC, GetDCEx и BeginPaint? В прежние времена операционная система Windows работала в реальном режиме на компьютерах с 640 Кбайт памяти. Контекст устройства, занимавший почти 200 байт памяти, считался большой структурой, а создание контекста устройства с загрузкой драйвера, поиском точек входа и настройкой атрибутов на 20-мегагерцовых компьютерах происходило довольно медленно. Модуль управления окнами (USER) пять раз вызывал функцию CreateDC и создавал кэш с пятью контекстами устройств. Функции GetDC, GetWindowDC и BeginPaint просто брали готовые контексты из кэша. Приложения должны были освобождать манипуляторы контекста устройства сразу же после завершения вывода, чтобы ими могли воспользоваться другие приложения. В 16-разрядных версиях Windows отсутствие свободного контекста в кэше приводило к сбоям в выводе приложения. Обычный контекст устройства, полученный из кэша контекстов, называется общим (common) контекстом устройства. Ограничение в пять контекстов устройств относится только к 16-разрядным реализациям Windows. В Windows 95, 98, NT и 2000 такое ограничение уже не действует. Если в системе кончаются кэшированные контексты устройств, она создает и использует новый контекст. В этой сфере Windows NT/2000 отличается от Windows 95/98. Реализация Windows NT/2000 основана на полноценной 32-разрядной архитектуре, при которой каждый процесс работает в собственном адресном пространстве. Хотя большинство ресурсов GDI совместно используется на уровне системы, манипуляторы объектов GDI привязываются к конкретным процессам; это означает, что контекст устройства может использоваться только процессом, создавшим этот контекст. Windows 95 и 98 основаны на усовершенствованной 16-разрядной реализации GDI, в которой большие структуры данных (такие, как контексты устройств) перемещаются в отдельную 2-мегабайтную кучу. Для сравнения стоит заметить, что в 16-разрядных версиях Windows объем кучи GDI равен 64 Кбайт. Классовый контекст устройства Флаг CSCLASSDC поля стилей структуры WNDCLASS сообщает модулю управления окнами, что для данного класса следует создать контекст устройства, совместно используемый всеми окнами класса. Такой контекст называется классовым контекстом устройства (class device context). Классовый контекст создается при создании первого экземпляра окна данного класса и инициализируется значениями по умолчанию.
314 Глава 5. Абстракция графического устройства При вызове функций GetDC, GetWindowDC и BeginPaint для окна, относящегося к этому классу, возвращается контекст устройства, связанный с классом окна, с обновленным прямоугольником вывода, видимым регионом и пустой областью отсечения. Все остальные атрибуты классового контекста (логическое перо, цвет текста, режим отображения и т. д.) сохраняют прежние значения. После завершения вывода функция ReleaseDC или EndPaint возвращает контекст устройства классу, не уничтожая его и не сбрасывая значения атрибутов. Классовый контекст устройства уничтожается лишь с уничтожением последнего окна класса. Если вы где-нибудь читали, что для классового контекста устройства можно опускать вызовы ReleaseDC и EndPaint, поскольку они все равно ничего не делают, — забудьте об этом. Подобные рекомендации вредны; ради ничтожной выгоды вы можете нарваться на большие неприятности. Кстати, именно EndPaint восстанавливает каретку, скрываемую при вызове BeginPaint. Классовые контексты устройств удобно использовать для управляющих окон, которые выводятся с одними и теми же атрибутами, поскольку это сокращает время на подготовку контекста к выводу и его освобождение. К числу других преимуществ классовых контекстов устройств относится экономия памяти. В наше время классовые контексты устройств поддерживаются для обеспечения совместимости. Их преимущества становятся несущественными на фоне увеличения объема памяти и быстродействия процессора, а также архитектуры защищенных адресных пространств Win32. Классовые контексты устройств не рекомендуется использовать в программировании Win32. Закрытый контекст устройства Флаг CS_0WNDC поля стилей структуры WNDCLASS сообщает модулю управления окнами, что для каждого окна, созданного на базе данного класса, должен создаваться отдельный контекст устройства. Таким образом, каждое окно на протяжении всего жизненного цикла связано со специальным контекстом устройства. Такие контексты устройств называются закрытыми (private). Закрытый контекст устройства всего один раз инициализируется значениями по умолчанию. При каждом вызове GetDC, GetWindowDC и BeginPaint загружается закрытый контекст окна с новым прямоугольником вывода и видимым регионом. Получив контекст устройства, приложение может изменять его атрибуты и выполнять графические команды. Функции ReleaseDC и EndPaint возвращают контекст устройства окну, не изменяя его, поэтому при следующем получении контекста его атрибуты (такие, как перо и кисть) сохраняют прежние значения. В документации MSDN закрытые контексты устройств описаны невразумительно (см. раздел «Private Display Device Contexts»). В частности, там утверждается, что приложение должно получать манипулятор закрытого контекста только один раз и многократно использовать его, а также — что приложение может включить обновляемый регион в обработку сообщения WM_PAINT при помощи функции BeginPaint. Закрытые контексты устройств обеспечивают максимальное быстродействие ценой максимальных затрат памяти. Контекст устройства использует ресурсы трех типов — манипулятор GDI, память в адресном пространстве пользовательского приложения и память в адресном пространстве ядра. Закрытые контексты
Контекст устройства 315 устройств имеют смысл только для окон со сложными атрибутами, подготовка которых занимает много времени, и для окон, нуждающихся в частом обновлении. Рекомендуется использовать приватные контексты лишь в тех случаях, когда фактор быстродействия значительно важнее повышенных затрат памяти и ресурсов GDI. Родительский контекст устройства Флаг CS_PARENTDC не связан с той проблемой, которую пытаются решать закрытые и классовые контексты устройств. При установке этого флага функция GetDC или BeginPaint для дочернего окна использует прямоугольник вывода и видимый регион родительского окна для подготовки контекста устройства; вот почему эти контексты устройства называются родительскими (parent). Родительский контекст устройства выделяется из кэша, поэтому его атрибуты инициализируются значениями по умолчанию. Отличие состоит в том, что родительский контекст устройства наследует прямоугольник вывода и видимый регион родительского окна, что позволяет сэкономить время на вычисление прямоугольника вывода и видимого региона дочернего окна. Флаг CS_PARENTDC учитывается лишь в простом случае, когда дочернему окну хочется задействовать параметры родительского окна при своей прорисовке. Он игнорируется в ситуациях, когда родительское окно использует закрытый или классовый контекст устройства, когда родительское окно отсекает свои дочерние окна, а также когда дочернее окно отсекает свои дочерние или соседние окна. Прочие контексты устройств До настоящего момента мы ограничивались рассмотрением контекстов устройств, созданных функцией CreateDC, и контекстов, связанных с окнами. Эти два типа контекстов обеспечивают полноценные средства для работы с устройством, то есть они позволяют получать информацию и передавать графические команды видеоадаптеру или принтеру. В среде Windows существуют и другие разновидности контекстов устройств — а именно, информационные, совместимые и метафайловые контексты. Информационный контекст устройства Иногда потребности приложения ограничиваются простым получением атрибутов графического устройства. Например, при загрузке документа текстовый редактор должен узнать у стандартного принтера размеры бумаги и полей, чтобы правильно отформатировать документ в стиле WYSIWYG. В таких ситуациях Windows позволяет создать усеченный контекст устройства, называемый информационным контекстом. Информационный контекст создается функцией CreateIC: HDC CreateIC(LPCTSTR pszDriver, LPCTSTR pszDevice. LPCTSTR pszOutput. CONST DEVMODE * pdvmlnist);
316 Глава 5. Абстракция графического устройства Функция CreateIC аналогична CreateDC, однако она работает быстрее и расходует меньше памяти. Попытки графического вывода по манипулятору информационного контекста, возвращенному CreateIC, просто игнорируются. Информационный контекст удаляется функцией DeleteDC (функции DeleteIC не существует). Совместимый контекст устройства Предполагается, что контекст устройства обеспечивает аппаратно-независимый интерфейс приложения с графическими устройствами. Однако контексты устройств, возвращаемые описанными выше функциями, позволяют выводить графику только на физических устройствах — таких, как видеоадаптеры и принтеры. Работа этих устройств обеспечивается драйверами, получающими низкоуровневые команды от графического механизма. Но в некоторых ситуациях бывает очень удобно осуществлять вывод на графическом устройстве, имитируемом в памяти в виде растра. Совместимый контекст устройства (memory device context) позволяет выполнять вывод на связанном с ним растре или копирование растра на поверхность другого графического устройства. HDC CreateCompatibleDCCHDC hDC); Параметр hDC определяет существующий контекст устройства, с которым должен быть «совместим» создаваемый контекст. Совместимый контекст устройства использует растр в качестве графической поверхности. По умолчанию при создании совместимого контекста этот растр состоит из одного пиксела. Win32 содержит функции для создания растров и их связывания с поверхностью (функция SelectObject). Совместимые контексты удаляются функцией DeleteDC. Совместимый контекст наследует многие атрибуты от своего «эталонного» контекста. Более того, для совместимого контекста функция GetDeviceCaps возвращает те же результаты, как и для эталонного контекста. Совместимый контекст устройства с флагом DT_RASPRINTER в атрибуте TECHNOLOGY способен сбить с толку функцию, работающую как с совместимыми, так и с обычными контекстами устройств. Совместимые контексты устройств — весьма полезная штука. Мы подробно рассмотрим их в главе 10, при описании аппаратно-зависимых растров (DDB) и DIB-секций. Метафайловый контекст устройства Другой разновидностью контекста, не соответствующего реальному физическому устройству, является метафайловый контекст устройства. Совместимый контекст позволяет сформировать растр с использованием графических команд GDI; метафайловый контекст устройства позволяет сохранить команды GDI в виде потока данных или дискового файла, который затем воспроизводится как аудиозапись или видеоклип. Главное отличие между этими типами контекстов состоит в том, что совместимый контекст для хранения результатов вывода создает растр с фиксированными размерами и разрешением, а метафайловый контекст
Контекст устройства 317 сохраняет векторные и растровые команды, которые затем точно масштабируются по разным размерам. Метафайловые контексты устройств создаются двумя функциями. Одна функция генерирует метафайлы Winl6, а другая — расширенные метафайлы Win32: HDC CreateMetaFi1е(LPCTSTR IpszFile); HDC CreateEndMetaFileCHDC hdcRef. LPCTSTR IpszFileName, CONST RECT * lpRect. LPCTSTR lpDescription); Как метафайлы Windows (метафайлы Win 16), так и расширенные метафайлы широко используются в коммерческих приложениях для хранения графических заготовок (cliparts). Расширенные метафайлы также занимают важное место в реализации спулинга в Windows 95, 98, NT и 2000. Метафайлам посвящена глава 16 настоящей книги. Подведем итог: контекст устройства — удобная концепция, используемая в Windows API для обеспечения аппаратно-независимого графического вывода. В этом разделе мы познакомились с разными классами контекстов устройств, рассмотрели способы их создания, атрибуты и методы для работы с атрибутами. В табл. 5.6 приведена краткая сводка разных контекстов устройств и их характеристик. Таблица 5.6. Краткая сводка контекстов устройств Тип контекста Создание, уничтожение Применение Общий контекст устройства Контекст устройства, связанный с окном CreateDC, DeleteDC GetWindowDC, GetDC, GetDCEx, BeginPaint, EndPaint, ReleaseDC Информационный контекст Совместимый контекст Метафайловый контекст CreateIC, DeleteDC CreateCompatibleDC, DeleteDC CreateMetaFile, CreateEnhMetaFile, DeleteDC Доступ ко всей поверхности устройства, вывод на первичный и вторичный экран, зеркальное воспроизведение и устройства создания жестких копий Вывод в части экранной поверхности, соответствующей видимому региону окна или его клиентской области с исключением регионов дочерних и соседних окон. Контекст устройства, возвращаемый функцией BeginPaint, ограничивает область вывода той частью, которая входит в обновляемый регион окна. Контексты этого типа делятся на обычные, классовые, закрытые и родительские Получение информации о возможностях устройства и драйвера Вывод на растровой поверхности в памяти и передача изображения в другой контекст устройства Запись команд GDI в поток данных или в файл с последующим воспроизведением
318 Глава 5. Абстракция графического устройства Формальное представление контекста устройства В предыдущем разделе были описаны разные типы контекстов устройств и их общие атрибуты. В этом разделе мы внесем некоторые уточнения на концептуальном уровне на основании информации, полученной при анализе реализации GDI в Windows 2000. Итак, контекст устройства представляет собой структуру данных, которая в графической системе Windows решает две основные задачи. Во-первых, контекст устройства обеспечивает аппаратно-независимую абстракцию, позволяющую выводить графику на различных графических устройствах, как физических (скажем, на видеоадаптере), так и логических (например, в метафайл). Во- вторых, в контексте устройства хранятся различные параметры и графические объекты, используемые графическими командами. Базовая графическая поверхность, поддерживаемая контекстом устройства, представляет собой двумерный массив пикселов, отдельно адресуемых и доступных для чтения и записи. Модель графической поверхности идеально подходит для растровых видеоадаптеров и принтеров, однако она не универсальна. Устройства, не соответствующие этой модели, не поддерживают некоторые графические операции. Например, принтеры с поддержкой PostScript позволяют только выводить на поверхность, но не разрешают читать ее содержимое, поэтому реализовать бинарные или тернарные операции на принтерах PostScript было бы затруднительно. Другой пример — метафайловые контексты, которые не позволяют получить цветовое значение пиксела. Обычно контекст устройства поддерживает запись для каждого пиксела устройства, хотя у принтера возникают проблемы с печатью пикселов на краях листа; именно поэтому контекст устройства позволяет пользовательскому приложению получить информацию о размере бумаги. Для поддержки вывода в условиях многооконной и многозадачной среды контекст устройства может быть связан с окном. Участки контекста, в которых разрешен вывод, определяются по довольно сложной схеме, находящейся под управлением модуля управления окнами. Для определения подмножества поверхности, на котором возможен вывод, контекст устройства использует следующие атрибуты. О Прямоугольник окна (window rectangle). Прямоугольная область поверхности устройства, на которой осуществляется вывод. Соответствует ограничивающему прямоугольнику окна, указанному при вызове CreateWindow. Все перемещения и изменения размеров окна автоматически отслеживаются системой и отражаются в прямоугольнике окна контекста. Функция GetDCOrgEx возвращает позицию левого верхнего угла прямоугольника окна. О Системный регион (system region). Регион, вычисляемый с учетом нескольких факторов. Первоначально системный регион совпадает с регионом окна, который обычно имеет прямоугольную форму, но также может представлять собой любой регион, указанный при вызове SetWindowRgn. Из системного региона исключаются участки, занятые дочерними или соседними окнами, если в стиле класса окна установлены соответствующие флаги. Затем исключаются все участки, закрытые в z-порядке окон на рабочем столе. Далее систем-
Формальное представление контекста устройства 319 ный регион пересекается с обновляемым регионом окна, если для получения контекста устройства была использована функция BeginPaint. В системном регионе контекста автоматически отслеживаются все перемещения окна и все изменения в z-порядке. О Метарегион (meta region) и регион отсечения (clipping region). Определяемые приложением подмножества поверхности устройства, на которые должен происходить вывод. При создании контекста устройства или его получении у модуля управления окнами метарегион и регион отсечения всегда сбрасываются в состояние всей поверхности устройства. Метарегионы плохо документированы в Win32 API; они обеспечивают дополнительный уровень отсечения. В Wn32 API существуют многочисленные функции для модификации региона отсечения в контексте устройства. О Регион Pao (Rao region). Заранее вычисленное пересечение системного региона, метарегиона и региона отсечения. Системный регион, метарегион и регион отсечения хранятся в независимых полях контекста устройства, хотя из документации следует, что системный регион определяет исходное значение региона отсечения при получении контекста устройства. Однако все функции графического вывода работают только в пересечении системного региона, метарегиона и региона отсечения. Чтобы это пересечение не рассчитывалось заново при каждом графическом вызове, графический механизм вычисляет его заранее, обновляет при всех изменениях системного региона, метарегиона и региона отсечения и сохраняет результат в специальном поле. Результат называется регионом Рао в честь программиста Microsoft по имени Рао Ре- мала (Rao Remala), который, согласно «Undocumented Windows», настоял на включении этого поля в контекст устройства. Контекст устройства обычно связывается с драйвером графического устройства, несущим полную ответственность за передачу команд графическому устройству. Интерфейс между графическим механизмом и драйвером графического устройства называется DDI (Device Driver Interface) и документируется в Microsoft DDK (Device Development Kit). Драйвер графического устройства передает графическому механизму таблицу функций косвенного вызова, реализующих вызовы интерфейса DDL Графический механизм создает структуру логического устройства для хранения информации о драйвере графического устройства. Драйвер графического устройства может существовать в нескольких воплощениях с различными параметрами — например, форматом пикселов кадрового буфера или настройками печати. Это позволяет осуществлять динамическое переключение видеорежимов и одновременный спулинг нескольких заданий. При создании очередного экземпляра драйвера устройства ему по запросу графического механизма или приложения передается структура DEVM0DE; драйвер возвращает две структуры с информацией об атрибутах и возможностях устройства и драйвера. Графический механизм сохраняет полученную информацию в структуре физического устройства. Графический драйвер также создает собственную структуру данных и передает ее манипулятор графическому механизму. В структуре физического устройства хранится большой объем информации о драйвере — размеры, возможности, ограничения, особые штриховые кисти, полутоновые узоры и т. д.
320 Глава 5. Абстракция графического устройства Структура контекста устройства содержит указатели на структуры логического и физического устройства. Из структуры физического устройства берется информация, возвращаемая пользовательскому приложению в ответ на запрос GetDeviceCaps. Кроме того, структура физического устройства сообщает графическому механизму, каким образом графические команды GDI должны разбиваться на вызовы DDI, обслуживаемые драйвером устройства. Например, поддерживает ли драйвер работу с кривыми Безье или же они должны разбиваться на сегменты? По указателям на функции косвенного вызова в структуре логического устройства графический механизм выходит на функцию, обрабатывающую тот или иной вызов DDL Таким образом, все аппаратно-зависимые аспекты графической системы Windows инкапсулируются в этих двух структурах. Кроме того, в контекст устройства входят поля, обеспечивающие его связь с окном, растром или метафайлом, а также объекты и атрибуты Win32 API. С одними полями можно работать средствами Win32 API, другие частично или полностью скрыты от пользовательских приложений. Некоторые поля доступны только для чтения (скажем, базовая точка контекста устройства); другие доступны как для чтения, так и для записи. По соображениям быстродействия некоторые часто используемые атрибуты контекстов устройств хранятся в пользовательском адресном пространстве, что позволяет легко получить их значения без переключения в режим ядра и обратно. Впрочем, основная часть контекста устройства хранится в адресном пространстве режима ядра, в котором работают графический механизм и нормальные драйверы графических устройств. GDI предпринимает особые меры по защите контекстов устройств — впрочем, как и остальных объектов GDI, которые будут рассмотрены в следующей главе. При создании контекста пользовательскому приложению предоставляется только его манипулятор, по которому приложение ссылается на контекст при вызове функций GDI. По манипулятору контекста устройства GDI находит элемент таблицы объектов GDI (см. следующую главу), содержащий указатели на обе структуры данных контекста (пользовательского режима и режима ядра). Структуру контекста устройства в Windows 2000 в некоторой степени иллюстрирует рис. 5.9. В пользовательском режиме контекст устройства представлен структурой DCATTR, содержащей практически все атрибуты и объекты, задействованные средствами Win32 API (кроме палитры, цветового пространства и т. д.). В режиме ядра используется структура DC0BJ, в которой содержится прямоугольник окна, регион отсечения, системный регион (prgnVis на рисунке), регион Рао и другие регионы. Поле PPDEV содержит указатель на структуру физического устройства, PDEVWIN32K. В полях GDI INFO, DEVINF0 и AHSURF структуры PDEVWIN32K хранится информация, полученная от драйвера устройства. Поле PLDEV содержит указатель на структуру логического устройства, LDEVWIN32K. Большая часть структуры LDEVWIN32K занята таблицей из 89 указателей на функции косвенного вызова, APFN, которая также дублируется в структуре PDEVWIN32K. Контексты устройств имеют много общего с объектами в объектно-ориентированных системах и языках типа C++. Различные атрибуты, хранящиеся в контексте устройства, соответствуют переменным класса, а таблица функций, хранящаяся в глубинах контекста устройства, в точности аналогична таблице виртуальных функций в объекте C++, содержащем виртуальные функции. Драйвер
Пример: родовой класс рамочного окна 321 графического устройства обеспечивает актуализацию абстрактного контекста, реализуя функции DDL После того как контекст устройства должным образом подготовлен, приложение работает с ним при помощи стандартного набора функций, не беспокоясь о том, как реализуются графические вызовы. ppdevnext hsemdvlck hsempointer spritestate hlfntdefault ahsurf I devinfo 1 gdiinfo 1 psurface 1 hspooler ddglobal pgraphisdev 1 pdevmode pldev 1- apfn[89] 1 1 1 nextldev 1 1 prevldev 1 1 levtype 1 1 cRefs J 1 pgdidrvinfo 1 uldrvversion 1 apfn[89] dcattr dcobj pdev_win32k Idev_win32k Рис. 5.9. Контекст устройства Windows 2000 и его структуры данных Рисунок 5.9 ни в коем случае не претендует на полноту. Обратитесь к главе 3 за дополнительной информацией или займитесь собственными исследованиями, используя либо WinDbg с расширением отладчика GDI, либо программу Fosterer из главы 3. В главе 4 представлена программа, которая модифицирует таблицу функций GDI в структуре PDEVWIN32K с целью отслеживания вызовов интерфейса DDL В главе 2 приведен пример реализации интерфейса DDI в драйвере принтера. Пример: родовой класс рамочного окна Начиная с этой главы, работа практически всех графических примитивов GDI будет поясняться конкретными примерами. Почти во всех программах, написанных ранее, пользовательский интерфейс состоял из диалогового окна с несколькими вкладками-страницами — для демонстрации GDI API этого явно недостаточно. В этом разделе мы разработаем родовой набор классов для написания Windows-программ, удовлетворяющий перечисленным ниже условиям. О Главное окно программы имеет строку заголовка, меню, системное меню и рамку, которую можно перетаскивать для изменения размеров главного окна. О В главном окне находится панель инструментов для ускоренного вызова часто используемых команд. Кнопки панели инструментов снабжаются наглядными растровыми изображениями и всплывающими подсказками (tooltips). Ill Таблица объектов GDI 1 Логическое перо 1 Логическое перо Цвет фона | Основной цвет 1 | Графический режим 1 гор2 Режим заполнения фона Режим заливки фигур strchbltmode 1 xform I 1 Базовая точка окна I ■j dhpdev dctype flgraphics Палитра 1 Цветовое пространство hdcsave Траектория prgnClip prgnMeta prgnAPI : prgnVis prgnRao erclWindow ppdev 1 hsem
322 Глава 5. Абстракция графического устройства О В главном окне присутствует строка состояния, разделенная на несколько панелей. О В оставшейся части главного окна (клиентской области) программа может выводить все, что считает нужным. В дальнейшем эта область называется «холстом» (canvas). Программа, реализующая эти требования, функционально эквивалентна базовой программе, сгенерированной мастером приложений MFC при выборе од- нодокументного интерфейса (SDI), отключении архитектуры «документ/представление» и без поддержки баз данных и элементов ActiveX. Чтобы библиотека была действительно универсальной, в нее включаются классы C++, содержащие виртуальные функции. Все классы C++ в этой книге начинаются с префикса «К»; это позволяет использовать их совместно с классами MFC, обычно начинающихся с буквы «С». Класс панели инструментов Панели инструментов реализуются следующим классом: class KToolbar { HWND mJiToolTip; UINT m_ControlID; HINSTANCE m_ResInstance: UINT m_ResId; public: HWND m_hWnd; KToolbarO { m_hWnd = NULL; mJiToolTip = NULL; m_ControlID = 0; m_ResInstance = NULL; m_ResId = 0; } void Create(HWND hParent. HINSTANCE hlnstance. UINT nControlID, const TBBUTTON * pButtons. int nCount); void Resize(HWND hParent. int width, int height); }: Класс KToolbar устроен очень просто, поэтому виртуальные функции в нем отсутствуют. Главный метод класса, Create, получает массив определений TBBUTTON, создает дочернее окно панели инструментов с кнопками и окно подсказок. Подсказки соответствуют растрам на кнопках панели. В поле dwData каждого определения TBBUTTON хранится идентификатор строкового ресурса, по которому метод Create загружает строку и включает ее в окно подсказки. Метод Resize изменяет
Пример: родовой класс рамочного окна 323 размеры окна панели инструментов в соответствии с новой шириной клиентской области родительского окна. Класс строки состояния Окно строки состояния тоже устроено очень просто. Объявление класса KStatus- Wi ndow выглядит следующим образом: typedef enum { pane_l. рапе_2. pane_3 }: class KStatusWindow { public: HWND m_hWnd; UINT m_ControlID; KStatusWindowO { m_hWnd = NULL; m_ControlID = 0; } void Create(HWND hParent, UIN.T nControlID); void Resize(HWND hParent, int width, int height); void SetText(int pane. HINSTANCE hlnst. int messid. int param=0); void SetText(int pane. LPCTSTR message); }: Метод Create создает окно строки состояния как дочернее по отношению к основному окну. Метод Resize изменяет ширину окна в соответствии с шириной клиентской части родительского окна и делит строку состояния на три панели. Два метода SetText предназначены для вывода сообщений в строке состояния. Класс холста Класс KCanvas описывает окно холста, в котором происходит весь основной вывод приложения. Класс KCanvas создается как производный от класса KWindow, описанного в главе 1. Он содержит четыре виртуальные функции, которые могут переопределяться в производных классах. class KStatusWindow; class KCanvas : public KWindow { public: virtual LRESULT WndProcCHWND hWnd. UINT uMsg, WPARAM wParam. LPARAM IParam); virtual void OnDraw(HDC hDC. const RECT * rcPaint); HINSTANCE m hlnst;
324 Глава 5. Абстракция графического устройства public: virtual BOOL OnCommand(WPARAM wParam. LPARAM lParam); KStatusWindow * m_pStatus; KCanvasO; void SetStatus(HINSTANCE hlnst. KStatusWindow * pStatus) { m_hlnst = hlnst; m_pStatus = pStatus; } virtual -KCanvasO; }: Виртуальный метод WndProc обрабатывает все сообщения, отправленные окну холста. В реализации по умолчанию обрабатываются сообщения WM_CREATE и WM_PAINT. В ходе обработки сообщения WMPAINT вызываются функции BeginPaint, KCanvas::OnDraw и EndPaint. Метод OnCommand обрабатывает сообщения WM_COMMAND, отправленные из главного окна. Позднее мы создадим класс, производный от KCanvas, который будет обрабатывать сообщения изменения масштаба и прокрутки. Класс рамочного окна Главное окно программы абстрагируется в виде класса KFrame, также производного от класса KWindow. В терминологии Windows класс KFrame воплощает рамочное окно (frame window) с интерфейсом SDI (Single Document Interface), но позднее мы создадим производный класс для реализации рамочных окон многодокументного интерфейса MDI (Multiple Document Interface). class KStatusWindow; class KCanvas; class KToolbar; class KFrame : public KWindow { typedef enum { ID_STATUSWINDOW = 101, ID TOOLBAR = 102 KToolbar * m_pToolbar; KCanvas * m_pCanvas: KStatusWindow * m_pStatus; const TBBUTTON * m_pButtons; int mjiButtons; int mjiToolbarHeight; int m_nStatusHeight; virtual LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM lParam);
Пример: родовой класс рамочного окна 325 virtual LRESULT OnCreate(void); virtual LRESULT OnSize(int width, int height): virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam); public: KFrame(HINSTANCE hlnstance. const TBBUTTON * pButtons. int nCount. KToolbar * pToolbar. KCanvas * pCanvas, KStatusWindow * pStatus); virtual -KFrameO; }: Виртуальный метод WndProc обеспечивает основную обработку сообщений окна. В процессе обработки сообщения WMCREATE он вызывает метод OnCreate, в процессе обработки сообщения WMSIZE — метод OnSize, а в процессе обработки сообщения WM_COMMAND — метод OnCommand. В самом главном рамочном окне рисовать нечего, поскольку его клиентская область полностью перекрывается окнами панели инструментов, холста и строки состояния. Однако конструктор KFrame:: KFrame (...) заслуживает внимания. Мы хотим, чтобы этот класс был по возможности универсальным и подходящим для многократного использования, поэтому экземпляры KToolbar, KCanvas и KStatusWindow создаются вне класса KFrame. Указатели на них передаются конструктору класса KFrame вместе с определениями кнопок панели инструментов. Обратите внимание: вы можете создать класс, производный от KCanvas, и передать указатель на него вместо указателя на KCanvas. Реализация конструктора чрезвычайно проста: он просто сохраняет переданные параметры для последующего использования в OnCreate и других методах. Метод OnCreate — единственный метод класса, содержащий реальный код. Он вызывает методы для создания окон панели инструментов, холста и строки состояния, имеющих нужные размеры и находящиеся в нужной позиции. LRESULT KFrame::OnCreate(void) { RECT rect; // Окно панели инструментов находится // в верхней части клиентской области if ( m_pToolbar ) { m_pToolbar->Create(m_hWnd. mjilnst, ID_STATUSWINDOW, m_pButtons, mjiButtons): GetWindowRect(m_pToo1bar->m_hWnd. & rect); mjiToolbarHeight = rect.bottom - rect.top; } else mjiToolbarHeight = 0; // Окно строки состояния находится // в нижней части клиентской области
326 Глава 5. Абстракция графического устройства if ( m_pStatus ) { m_pStatus->Create(m_hWnd, ID_STATUSWINDOW); GetWindowRect(m_pStatus-<(a062>m_hWncl. & rect); m_nStatusHeight = rect.bottom - rect.top; } else m_nStatusHeight = 0; // Создать окно холста, расположенное над окном строки состояния if ( m_pCanvas ) { GetClientRect(m_hWnd, & rect); m_pCanvas->SetStatus(m_hInst. m_pStatus); m_pCanvas->CreateEx(0, _T("Canvas Class"). NULL, WSJISIBLE | WS_CHILD. 0, m_nToolbarHeight. rect.right, rect.bottom - mjnToolbarHeight - m_nStatusHeight. mJiWnd. NULL. m_r.Ir.st); } return 0; } Программа проверяет указатели на все объекты дочерних окон и вызывает методы их создания лишь в том случае, если указатель проходит проверку. В результате ни одно из дочерних окон не является строго обязательным — программа работает и без них. Система управления окнами ОС следит за тем, чтобы панель инструментов занимала верхнюю часть клиентской области, а строка состояния находилась внизу. Вызов CreateEx для окна холста учитывает это обстоятельство и производит соответствующую регулировку позиции и высоты холста. Стандартная реализация KFrame: :0nSize обеспечивает правильную позицию и размеры трех дочерних окон при изменении размеров главного окна. Первичная обработка команд меню осуществляется методом OnCommand. По умолчанию полученное сообщение передается функции KCanvas:: OnCommand. Тестовая программа Главным фактором при оценке классов рамочного окна являются их удобство и универсальность при программировании. Мы рассмотрим лишь самые интересные фрагменты программ, чтобы не повторять одно и то же снова и снова. В приведенной ниже простой тестовой программе используются все четыре класса окон. Программа создает окно со строкой заголовка, панель инструментов с двумя кнопками и подсказками, холст и окно строки состояния. По сравнению с базовой программой MFC, сгенерированной мастером, здесь многого не хватает, в том числе макросов, глобальных переменных, выделения памяти из кучи и обращений к системным DLL.
Пример: родовой класс рамочного окна 327 const TBBUTTON tbButtons[] « { { STDJILENEW. IDM_FILE_NEW. TBSTATEJNABLED. TBSTYLE_BUTTON. { 0. 0 }. IDSJILENEW, 0 }. { STDJELP. IDM_APP_ABOUT, TBSTATEJNABLED, TBSTYLEJUTTON. { 0. 0 }. IDSJELPABOUT. 0 } }: int WINAPI WinMain(HINSTANCE hinst. HINSTANCE. LPSTR IpCmd. int nShow) { KToolbar toolbar; KCanvas canvas; KStatusWindow status; KMyFrame frame(hlnst. tbButtons, 2, & toolbar, & canvas. & status); frame.CreateEx(0. JT'ClassNarne"). _J("Program Name"). WSJVERLAPPEDWINDOW, CWJJSEDEFAULT. CWJJSEDEFAULT. CWJJSEDEFAULT. CWJJSEDEFAULT. NULL. LoadMenu(hInst. MAKEINTRESOURCE(IDR_MAIN)). hinst); frame.ShowWi ndow(nShow); frame.UpdateWindowO; frame.MessageLoopO; return 0; } Примерный вид окна нашей программы показан на рис. 5.10. Create a New Document! Рис. 5.10. Пример программы, использующей родовые классы рамочного окна
328 Глава 5. Абстракция графического устройства Если вы думаете, что структура TBBUT0N здесь используется неправильно, вероятно, вы читали неверную документацию. Структура Win32 TBBUTTON состоит из семи полей. В MSDN и прочей документации не упоминается пятое поле: BYTE bReserved[2]. Компилятор C++ прощает неточности до тех пор, пока вы не начнете работать с двумя последними полями, в которых программа хранит идентификаторы строковых ресурсов подсказок. Пример программы: графический вывод в контексте устройства Графический вывод в среде Windows, как и большинство других процессов, управляется событиями. Предполагается, что приложение всегда должно уметь воспроизвести свое полное изображение, поскольку экран совместно используется несколькими окнами, принадлежащими разным приложениям. Когда возникает необходимость в перерисовке окна, функции окна посылается сообщение WMPAINT. Оно играет ключевую роль в графическом выводе, выполняемом в программах Windows, однако описать его с концептуальной точки зрения нелегко. Сообщение WM_PAINT автоматически генерируется диспетчером окон, когда окно является видимым, когда в системе нет более срочных сообщений и с окном связан непустой обновляемый регион. Обновляемый регион окна Обновляемый регион окна определяется несколькими факторами — ограничивающим прямоугольником окна; регионом, заданным функцией SetWindowRgn, и его связью с другими окнами на рабочем столе. Сообщение WMPAINT не ставится в очередь сообщений программного потока и не обрабатывается наравне с прочими сообщениями. Вместо этого при возникновении необходимости в перерисовке окна устанавливается бит, который заставляет планировщика окон напрямую вызвать обработчик сообщений окна при отсутствии других сообщений в очереди. Существует и другой способ выполнить форсированную перерисовку окна — вызвать функцию UpdateWindow. Изначально обновляемый регион окна пуст. Его состояние обновляется при вызове следующих функций: BOOL InvalidateRectCHWND hWnd. CONST RECT * lpRect. BOOL bErase): BOOL ValidateRect(HWND hWnd. CONST RECT * lpRect): BOOL InvalidateRgn(HWND hWnd. HRGN hRgn, BOOL bErase); BOOL ValidateRgnCHWND hWnd. HRGN hRgn); Функции InvalidateRect/InvalidateRgn включают прямоугольник или регион в обновляемый регион окна. Если при вызове передается параметр NULL, в обновляемый регион включается вся клиентская область окна. Функции Validate- Rect/ValidateRgn решают обратную задачу: они исключают прямоугольник или регион из обновляемого региона окна. Если при вызове передается параметр NULL, из обновляемого региона исключается вся клиентская область окна. Параметр
Пример программы: графический вывод в контексте устройства 329 bErase сообщает диспетчеру окон, следует ли генерировать сообщение стирания фона WM_ERASEBKGND при вызове BeginPaint. Обновляемый регион окна также изменяется при изменении размеров или прокрутке окна, а также при удалении, перемещении или изменении размеров другого окна, расположенного поверх данного. При изменении размеров окна генерируется сообщение WMSIZE; диспетчер окон проверяет флаги CS_HREDRAW и CS_VREDRAW в стиле класса окна (WNDCLASSEX.style), а не в стиле самого окна. Если флаг CSHREDRAW или CSJ/REDRAW установлен, то при изменении ширины или высоты окна вся клиентская область объявляется недействительной; в противном случае недействительной объявляется только добавленная область окна. Любые изменения размеров окна приводят к его немедленной перерисовке. Когда пользователь изменяет окно перетаскиванием рамки, диспетчер окон обычно лишь имитирует изменение размеров окна до того момента, когда будет отпущена кнопка мыши. В новых операционных системах семейства Windows (Windows 95, 98 и 2000) в приложении Display (Экран) панели управления имеется флажок, управляющий этим режимом. Если на вкладке Эффекты (Effects) установлен флажок Show window contents while dragging (Отображать содержимое окна при перетаскивании), сообщение об изменении размеров многократно генерируется в процессе перетаскивания. Если перерисовка окна выполняется медленно, это может привести к серьезным задержкам. Прокрутка окна или связанного с ним контекста устройства также приводит к перерисовке окна. При прокрутке окна или его клиентской области пикселы перемещаются вверх или вниз, налево или направо, и в окне появляются новые, не прорисованные участки содержимого. Такие участки тоже включаются в обновляемый регион окна. Сведения о текущем обновляемом регионе окна возвращаются двумя функциями: int GetUpdateRgnCHWND hWnd. HRGN hRgn. BOOL bErase); BOOL GetUpdateRectCHWND hWnd. LPRECT lpRect. BOOL bErase); Функция GetUpdateRgn возвращает обновляемый регион окна через манипулятор существующего региона hRgn; другими словами, перед вызовом функции манипулятор hRgn должен содержать действительный манипулятор объекта региона, а после вызова функции он содержит данные обновляемого региона окна. Функция GetUpdateRect просто возвращает ограничивающий прямоугольник для обновляемого региона окна. Параметр bErase управляет отправкой сообщения WM_ERASEBKGND в том случае, если обновляемый регион не пуст. Сообщение WM.PAINT Когда в функцию окна поступает сообщение WM_PAINT, приложение обычно вызывает функцию BeginPaint. Функция BeginPaint получает контекст устройства и инициализирует системный регион пересечением видимого региона окна с обновляемым регионом. Перед возвратом из BeginPaint обновляемый регион объявляется действительным (то есть сбрасывается), чтобы система могла начать новый цикл накопления данных обновляемого региона.
330 Глава 5. Абстракция графического устройства Помимо возвращения HDC, функция BeginPaint также заполняет структуру PAINTSTRUCT: typedef struct { HDC hdc; BOOL bErase; RECT rcPaint; BOOL fRestore; BOOL flncUpdate; BYTE rgbReserved[32]; } PAINTSTRUCT; Поле hdc содержит тот же манипулятор HDC, который возвращается функцией BeginPaint; значение используется функцией EndPaint для освобождения контекста устройства. Если флаг bErase равен TRUE, приложение должно само стереть фон окна, поскольку все попытки стирания фона завершились неудачей. Если при вызове InvalidateRect или InvalidateRgn был установлен флаг bErase (признак стирания фона), реализация BeginPaint отправляет сообщение WM__ERASEBKGND функции окна, которая должна либо обработать сообщение, либо передать его функции Def • WindowProc. Последняя использует для стирания фона манипулятор фоновой кисти, указанный в поле WNDCLASSEX. hbrBackground. Но если кисть не задана, считается, что стереть фон не удалось и эта задача должна быть решена самим приложением. Поле rcPaint содержит ограничивающий прямоугольник текущего системного региона контекста (то есть региона, нуждающегося в перерисовке). Существует несколько вариантов обработки сообщения WM_PAINT после вызова BeginPaint. Если вы пишете хоть сколько-нибудь нетривиальную программу, подумайте над тем, как оптимизировать обработку WM_PAINT. О В простейшем варианте функция окна выводит в окне все, что заблагорассудится, и перекладывает все хлопоты с отсечением на GDI. Если перерисовка связана со сложными вычислениями и большим количеством графических вызовов, могут возникнуть серьезные проблемы с быстродействием. О Нормальная реализация должна сама проверить прямоугольник rcPaint и перерисовать только те объекты, которые с ним пересекаются. При перерисовке небольших фрагментов изображения это приведет к существенному повышению быстродействия — особенно в ситуации, когда при перетаскивании рамки окна открываются новые участки. О Более изощренная реализация может напрямую работать с системным регионом. Поле rcPaint содержит ограничивающий прямоугольник системного региона, причем последний вовсе не обязан иметь прямоугольную форму. Системный регион может быть значительно меньше области, накрываемой прямоугольником rcPaint. Непосредственная прорисовка на уровне системного региона повышает быстродействие графического вывода. О Если вывод занимает много времени, стоит рассмотреть методику постепенного обновления окна. Например, на загрузку большого растрового изображения в web-браузере может потребоваться очень много времени. Обработчик сообщения WM_PAINT должен быстро отобразить информацию, имеющуюся на
Пример программы: графический вывод в контексте устройства 331 локальном компьютере и вернуть управление с последующим обновлением окна при поступлении новых данных. В промежутках пользователь может прокрутить окно, ознакомиться с отображаемой информацией и даже завершить просмотр. Системный регион контекста устройства в течение долгого времени оставался скрытым от программистов. В новых версиях заголовочных файлов Windows документируется функция GetRandomRgn, позволяющая получить информацию о системном регионе. Хотя эта функция давно экспортируется из GDI32.DLL, раньше она считалась недокументированной. int GetRandomRgn(HDC hDC, HRGN hrgn, INT iNum); Единственным документированным значением параметра iNum является значение SYSRGN, однако при вызове можно передать и другие недокументированные индексы для получения других регионов, связанных с DC (эта тема рассматривается в главе 7). Функция GetRandomRgn (hDC, hRgn, SYSRGN) копирует данные системного региона контекста устройства в данные региона, определяемого манипулятором hRgn; перед вызовом функции этот манипулятор должен соответствовать действительному объекту региона. Полученный регион раскладывается на прямоугольники функцией GetRegionData. Если все сказанное звучит слишком запутанно, не ломайте голову — весь процесс подробно рассматривается в главе 6. Перед возвращением из обработчика WM_PAINT функция окна должна вызвать функцию EndPaint, которая при необходимости освобождает ресурсы, связанные с контекстом устройства, или возвращает общий контекст в кэш. Наглядное представление сообщений перерисовки окна В нормальной реализации WM_PAINT обновляемый регион перерисовывается так, чтобы новое изображение идеально стыковалось с изображением, присутствующим на экране. Но нам как программистам хочется получить наглядное представление о сообщениях WM_PAINT — увидеть, когда они генерируются, какая часть изображения входит в системный регион и узнать, используется ли манипулятор контекста устройства многократно или каждый раз создается заново. Кроме того, нам хотелось бы понаблюдать за генерацией и обработкой других сообщений, связанных с перерисовкой (таких, как WM_NCCALCSIZE, WMJCPAINT, WMJRASEBKGND и WMJIZE). В листинге 5.1 приведена программа WinPaint, которая поможет вам лучше разобраться в использовании сообщения WM_PAINT. Программа построена на базе набора родовых классов окон, построенных в разделе «Пример: родовой класс рамочного окна». Листинг 5.1. Программа WinPaint: наглядное представление сообщений WM_PAINT // WinPaint.cpp #define STRICT #define WIN32 LEAN AND MEAN #include <windows.h> #include <assert.h> Продолжение &
332 Глава 5. Абстракция графического устройства Листинг 5.1. Продолжение #include <tchar.h> #include #include #include #include #include A. AincludeXwin.h" .\. .\include\Canvas.h" .\..\include\Status.h" A. AincludeXFrameWnd.h" A. Ainclude\LogWindow.hH #include "Resource.h" class KMyCanvas : public KCanvas { virtual void OnDrawCHDC hDC. const RECT * rcPaint); virtual LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam); int int HRGN KLogWindow DWORD mjiRepaint; m Red. m Green, m Blue m_hRegion; m_Log; m Redraw; public; BOOL OnCommand(WPARAM wParam. LPARAM IParam); KMyCanvasО { mjiRepaint = 0; m_hRegion = CreateRectRgn(0. 0. I. 1); m_Red = 0x4F m_Green = 0x8F m_Blue = OxCF m Redraw = 0; BOOL KMyCanvas:;OnCommand(WPARAM wParam, LPARAM IParam) switch ( LOWORD(wParam) ) { case IDM_VIEW_HREDRAW: case IDM VIEW VREDRAW: { HMENU hMenu = GetMenu(GetParent(m_hWnd)); MENUITEMINFO mii; memset(&mii. 0. sizeof(mii)): mii.cbSize - sizeof(mii); mii.fMask = MIIMJTATE: if ( GetMenuState(hMenu. LOWORD(wParam).
Пример программы: графический вывод в контексте устройства 333 MFJYCOMMAND) & MF_CHECKED ) mi i.fState = MFJJNCHECKED: else mii.fState - MF_CHECKED; SetMenuItemlnfoChMenu. LOWORD(wParam). FALSE. &mii); if ( LOWORD(wParam)==IDM_VIEW_HREDRAW ) m_Redraw A- WVR_HREDRAW; else m_Redraw A- WVR_VREDRAW; } return TRUE; case IDMJILEJXIT: DestroyWindow(GetParent(m_hWnd)); return TRUE; return FALSE; // Сообщение не обработано LRESULT KMyCanvas::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM 1 Param) { LRESULT lr; switch( uMsg ) { case WM__CREATE: m_hWnd = hWnd; m_Log.Create(m_hInst. "WinPaint"); m_Log.Log("WM_CREATE\r\n"); return 0: case WMJCCALCSIZE: m_Log.Log(,,WM_NCCALCSIZE\г\n,,); lr » DefWindowProcChWnd. uMsg. wParam. 1 Pa ram); m_Log. Log ("WMJCCALCSIZE returns Xx\r\n\ lr): if ( wParam ) { lr &- - (WVRJREDRAW | WVRJREDRAW); lr |= m Jed raw; } break; case WM_NCPAINT: m_Log.Log("WM_NCPAINT HRGN X0x\r\n\ (HRGN) wParam); lr = DefWindowProc(hWnd. uMsg. wParam. IParam); m_Log.Log("WN_NCPAINT returns\r\n"); break; case WMJRASEBKGND: m_Log.Log("WM_ERASEBKGND HOC ^0x\r\n". (HOC) wParam); lr = DefWindowProc(hWnd. uMsg. wParam. IParam); Продолжение^
334 Глава 5. Абстракция графического устройства Листинг 5.1. Продолжение m_Log.Log("WM_ERASEBKGND returns\r\n"); break; case WM_SIZE: m_Log.Log("WM_SIZE type *d. width *d. height *d\r\n\ wParam, LOWORD(lParam), HIWORD(lParam)); Ir = DefWindowProc(hWnd. uMsg. wParam, IParam); m_Log.Log("WM_SIZE returns\r\n"); break; case WM_PAINT: { PAINTSTRUCT ps: m_Log.Log("WM_PAINT\r\nM); m_Log.Log("BeginPaint\r\n"); HDC hDC = BeginPaint(m_hWnd. &ps); m_Log.LogCBeginPaint returns HDC *8x\r\n". hDC); OnDraw(hDC, &ps.rcPaint); m_Log.Log("EndPai nt\r\n"); EndPaint(m_hWnd. &ps); m_Log.Log("EndPaint returns \ "GetObjectTypeU08x)=Ud\r\n", hDC. GetObjectType(hDO): m_Log.Log("WM_PAINT returns\r\n"); } return 0; default: lr - DefWindowProcChWnd. uMsg. wParam. IParam): } return lr; } void KMyCanvas::OnDraw(HDC hDC. const RECT * rcPaint) { RECT rect; GetClientRect(m_hWnd. & rect); GetRandomRgnChDC. mJiRegion. SYSRGN): POINT Origin; GetDCOrgEx(hDC. & Origin); if ( ((unsigned) hDC) & OxFFFFOOOO ) OffsetRgn(m_hRegion. - Origin.x. - Origin.y): mjiRepaint ++; TCHAR mess[64]; wsprintf(mess. _T("HDC 0x*X, OrgUd, *d)"). hDC. Origin.x. Origin.y);
Пример программы: графический вывод в контексте устройства 335 if ( m_pStatus ) m_pStatus->SetText(pane_l. mess); switch ( mjiRepaint % 3 ) { case 0: m_Red - (m_Red + 0x31) & OxFF; break case 1: m_Green= (m_Green + 0x31) & OxFF; break case 2: m Blue = (m Blue + 0x31) & OxFF; break SetTextAlign(hDC. TAJOP | TA_CENTER); int size = GetRegionData(m_hRegion, 0. NULL); int rectcount = 0; if ( size ) { RGNDATA * pRegion = (RGNDATA *) new char[size]: GetRegionData(m_hRegion, size. pRegion); const RECT * pRect - (const RECT *) & pRegion->Buffer; rectcount - pRegion->rdh.nCount; TEXTMETRIC tm; GetTextMetrics(hDC. & tm); int lineheight - tm.tmHeight + tm.tmExternalLeading; for (unsigned i=0; i<pRegion->rdh.nCount; i++) { int x = (pRect[i].left + pRect[i].right)/2; int у = (pRect[i].top + pRect[i].bottom)/2; wsprintf(mess. "WM__PAINT Xd. rect JKd". mjiRepaint. i+1): ::TextOut(hDC. x. у - lineheight. mess, _tcslen(mess)): wsprintf(mess. "(*d. *d. *d. *d)\ pRect[i].left. pRect[i].top. pRect[i].right. pRect[i].bottom); ::TextOut(hDC. x. y. mess. Jxslen(mess)); } delete [] (char *) pRegion; wsprintf(mess. _T("WM_PAINT message %6. %6 rects in sysrgn"). mjiRepaint. rectcount); if ( m_pStatus ) mj)Status->SetText(pane_2. mess); HBRUSH hBrush = CreateSolidBrush(RGB(m_Red. m_Green. mjlue)); FrameRgn(hDC. m_hRegion. hBrush. 4. 4); FrameRgn(hDC. m_hRegion. (HBRUSH) GetStockObject(WHITE_BRUSH). 1. 1); Продолжение^
336 Глава 5. Абстракция графического устройства Листинг 5.1. Продолжение DeleteObject(hBrush); } int WINAPI WinMainCHINSTANCE hlnst, HINSTANCE. LPSTR. int nShow) { KMyCanvas canvas; KStatusWindow status; KFrame frame(hlnst, NULL. 0. NULL. & canvas. & status); frame.CreateEx(0. JCClassName"). JC'WinPaint"). WS_OVERLAPPEDWINDOW. CW_USEDEFAULT. CW_USEDEFAULT. CWJJSEDEFAULT. CWJJSEDEFAULT. NULL. LoadMenu(hInst. MAKEINTRESOURCE(IDR_MAIN)). hlnst): frame.ShowWindow(nShow); frame.UpdateWindowO; frame.MessageLoopO; return 0: } Вероятно, вы ждете подробных объяснений. Длинный список включаемых файлов — верный признак того, что мы используем готовые классы. В программе задействованы классы KWindow, KCanvas, KToolbar, KFrame и новый класс KLogWindow. Класс KLogWindow управляет многострочным временным окном «EDIT», в котором хранится зарегистрированная информация. Эти и другие классы скомпонованы в библиотеку, которая подключается к программе. Класс KMyCanvas создается производным от KCanvas. В нем переопределяется функция окна, а также обработчики командных сообщений и сообщений перерисовки. Новая функция OnCommand обрабатывает две команды меню, переключающие состояние флагов перерисовки при вертикальном и горизонтальном изменении размеров. Выше уже упоминались флаги CSHREDRAW и CSVREDRAW структуры WNDCLASSEX, определяющие необходимость перерисовки клиентской области при изменении размеров окна. Функция KMyCanvas::OnCommand позволяет переключать внутренний флаг m_Redraw, учитываемый при обработке WMNCCALCSIZE. Новая функция окна обрабатывает ряд сообщений, связанных с прорисовкой окна, - WM_NCCALCSIZE, WM_NCPAINT, WM_NCPAINT, WMJRASEBKGND, WM_SIZE и, наконец, WM_PAINT. В данном случае обработка сводится к вызову стандартной функции окна DefWindowProc (исключение составляет сообщение WMPAINT, обрабатываемое методом OnDraw). Однако программа не ограничивается простой передачей управления, а еще регистрирует данные до и после обработки сообщения. При обработке WMPAINT сохраненные данные передаются до и после вызовов BeginPaint и EndPaint. При обработке сообщения WMNCCALCSIZE окну предоставляется возможность вычислить размер клиентской области. Его обработка имеет один полезный аспект — когда параметр wParam равен TRUE, функция окна должна возвращать WVR_HREDRAW и/или WVR_VREDRAW, если изменение размеров окна приводит к перерисовке всей клиентской области. Таким образом, это сообщение фактически свя-
Пример программы: графический вывод в контексте устройства 337 зывает флаги CSVREDRAW и CSHREDRAW с диспетчером окон. Программа модифицирует результат, полученный от DefWindowProc, с учетом режима, выбранного пользователем в меню программы. Таким образом, за последствиями установки этих флагов можно понаблюдать без перекомпиляции тестовой программы. Функция KMyCanvas: :0nDraw написана таким образом, чтобы сообщение WMPAINT наглядно представлялось в окне программы. Работа функции начинается с получения информации о размерах клиентской области, системного региона и базовой точке контекста устройства. Существует две разные интерпретации системного региона. В Windows NT/2000 системный регион задается в экранной (или физической) системе координат; в Windows 95/98 системный регион задается в клиентской системе координат. Программа проверяет, работает ли она в Windows NT/2000, и если проверка дает положительный результат — переходит к клиентским координатам при помощи функции Of f setRgn. Поскольку мы знаем, что 32-разрядные манипуляторы GDI используются только в Windows NT/2000, программа определяет версию операционной системы простой проверкой старшего слова манипулятора HDC. Затем программа выводит манипулятор и базовую точку контекста в первой панели строки состояния и вычисляет цвет для вывода системного региона. При обработке каждого сообщения WMPAINT программа изменяет одну из цветовых составляющих (красную, зеленую или синюю). После этого все готово к анализу региона, который может быть пустым, состоять из одного прямоугольника или из сотен прямоугольников. Программа дважды вызывает функцию GetRegionData. В первый раз функция вызывается для получения размера данных региона, а во второй — для получения самих данных. И снова не стоит подолгу вникать в смысл происходящего; подробности будут приведены в главе 6. Для каждого прямоугольника программа выводит номер и координаты центра. После обработки всех прямоугольников программа выводит номер сообщения WMPAINT и количество прямоугольников во второй панели строки состояния. Наконец, контур системного региона обводится белой рамкой толщиной один пиксел и цветной рамкой толщиной три пиксела. Теперь запустите программу и поэкспериментируйте с ней. Вы поймете, как генерируются сообщения WMPAINT и какую область они занимают. На рис. 5.11 показано, как выглядит программа при поочередном изменении размеров окна по обеим осям. Первое сообщение WMPAINT перерисовывает окно стандартных размеров. Затем мы уменьшаем размер окна; при этом генерируется второе сообщение WMPAINT, системный регион которого не содержит ни одного прямоугольника. Затем окно сворачивается и восстанавливается, в результате чего генерируется третье сообщение для перерисовки уменьшенной клиентской области (первый прямоугольник на рис. 5.11). При изменении размеров окна в одном направлении генерируются сообщения WMPAINT с системным регионом, состоящим из одного прямоугольника. Но при одновременном масштабировании окна в обоих направлениях генерируется сообщение WMPAINT с системным регионом из двух прямоугольников (прямоугольники 1 и 2 для сообщения WM_PAINT с номером 7). Если открыть и закрыть меню, сообщение WMPAINT не генерируется, поскольку система сохраняет изображение при выводе меню и автоматически восстанавливает его. Но если
338 Глава 5. Абстракция графического устройства накрыть окно программы другим окном или перетащить окно за край экрана и вернуть его на место, сообщения перерисовки обязательно появятся. Если установить флаги CSHREDRAW и CS_VREDRAW в меню View, при изменении размеров окна перерисовывается вся клиентская область, а не только вновь появившиеся участки. Если в приложении Display (Экран) панели управления установлен флажок Show window contents while dragging (Отображать содержимое окна при перетаскивании), при перетаскивании окна за рамку генерируются частые сообщения перерисовки. BeginPaint WM_NCPAINT HRGN 8( sWN_NCPflINT return; UM_ERflSEBKGND HOC JWM_ERASEBKGND reti BeginPaint return* EndPaint EndPaint returns ( WM_PftINT returns WM__NCCALCSIZE WM_NCCflLCSIZE reti WM_SIZE type 0, wj WM_SIZE returns WM PftINT й&5£&ФЛ" лэш У*** - f/M^PAINT 3, rect 'M_PAINT 4, rect (0,0,122,70) (122,0,238,70), _PAINT 6, re PAINT 7, if 38,0,328,121,0,399,1 WM_PAINT 5, rect 1 (0,70,238,124) WM_PAINT 7, rect 2 (0,124,399,181) |HDCМ&ШЩЩ>ЭД; ''ЩШlmmmtZщй^f^^^ Рис. 5.11. Последовательность отправки сообщений WM_PAINT при изменении размеров окна В окне, расположенном слева, также выводятся довольно интересные результаты. Ниже приведен протокол изменения размеров одного окна, для наглядности снабженный отступами. WM_NCCALCSIZE WM_NCCALCSIZE returns 0 WMJIZE type 0, width 581, height 206 WMJIZE returns WM_PAINT BeginPaint WMJCPAINT HRGN 9e040469 WMJCPAINT returns WMJRASEBKGND HDC 3b0105ae WMJRASEBKGND returns BeginPaint returns HDC 3b0105ae EndPaint EndPaint returns GetObjectType(3b0105ae)=0 WM_PAINT returns Когда пользователь завершает перетаскивание границы окна в новое положение, генерируется сообщение WMNCCALCSIZE, за которым следуют сообщения WMSIZE и WMPAINT. Во время обработки WMPAINT функция BeginPaint генерирует сообщение WM_NCPAINT для перерисовки неклиентских областей и сообщение WMERASEBKGND для стирания фона. При вызове WM_ERASEBKGND передается манипулятор контекста устройства, возвращенный функцией BeginPaint. Интересно заметить, что в
Итоги 339 Windows NT/2000 после выхода из EndPaint манипулятор контекста устройства, возвращенный BeginPaint, становится недействительным (GetObjectType возвращает 0), но после нескольких повторных вызовов этот манипулятор HDC появляется снова. Это доказывает, что графический механизм поддерживает глобальный кэш манипуляторов контекстов устройств. Итоги Эта глава посвящена одной из важнейших концепций графического программирования в среде Windows — контекстам устройств. Мы рассмотрели важный класс графических устройств — видеоадаптеры; узнали, как составить список экранных устройств с поддерживаемыми видеорежимами и как получить информацию о возможностях устройств. Кроме того, в этой главе описаны различные типы контекстов устройств и способы их создания. Особое внимание было уделено контекстам устройств, связанным с конкретными окнами. Также было рассмотрено управление графическим выводом в окне с использованием обновляемого региона окна. В конце главы были созданы классы C++, демонстрирующие концепции графического программирования Windows на примере наглядной обработки сообщений WM_PAINT. Однако весь материал этой главы представляет собой лишь общее описание контекстов устройств и их связи с управлением окнами. Применение контекстов устройств при графическом выводе будет подробно рассмотрено в последующих главах. Примеры программ В отличие от глав 3 и 4 примеры этой главы являются вполне обычными программами Windows. Они демонстрируют некоторые неочевидные особенности контекстов устройств и их связи с выводом в окне (табл. 5.7). Таблица 5.7. Программы главы 5 Каталог проекта Описание Samples\Chapt_05\Device Получение списка экранных устройств, видеорежимов, получение информации о возможностях устройств и атрибутах контекстов Samples\Chapt_05\EllJpse Демонстрация возможности создания прямоугольных и непрямоугольных окон Samples\Chapt_05\FrameWindow Пример программы для тестирования семейства классов рамочного окна Samples\Chapt_05\WinPaint Наглядное представление сообщений перерисовки окна, системного региона, а также флагов CS HREDRAW и CS VREDRAW
Глава 6 Системы координат и преобразования Информация обо всех объектах, представляемых приложением, хранится в структурах данных. Например, программа компьютерной верстки хранит в структурах описания абзацев текста, изображений, векторных рисунков, заголовков и колонтитулов страниц, а приложение для проектирования садовых участков работает с объектами, представляющими растения, заборы, тропинки, лужайки и т. д. Подобные структуры данных называются моделями. В этой главе нас интересует особая категория моделей — геометрические модели, описывающие размеры и местонахождение объектов, их форму, цвет, поверхность и другие свойства. Размеры и позиция объектов обычно задаются в подходящих физических единицах — например, в дюймах или метрах. При описании формы объектов могут использоваться графические примитивы (прямоугольники, круги, многоугольники и т. д.). Разные приложения моделируют окружающий мир в разных системах координат. Например, в программах компьютерной верстки при моделировании макета в качестве базовой единицы обычно выбирается пункт (1/72 дюйма), а в системе автоматизированного проектирования базовая единица может быть равна 0,1 мм. Когда пользователь пытается создать макет страницы, узор для вышивки или план садового участка, приложение должно предоставить ему средства для изменения масштаба изображения и перемещения отображаемых объектов. С точки зрения приложения было бы крайне неэффективно пересчитывать все данные о местонахождении и размере объектов, сохраненные до поступления запроса. Довольно часто объекты обладают определенным сходством, что позволяет ограничиться подробным описанием только одного из них и сконструировать остальные экземпляры при помощи операций зеркального отражения, поворота, искажения или их комбинаций. Например, изображение розового сада можно построить многократным повторением картинки с изображением розы.
Физическая система координат 341 По практическим соображениям в Win32 GDI была реализована поддержка нескольких уровней систем координат, настраиваемых при помощи матрицы преобразования и других атрибутов контекста устройства. Традиционно в GDI используется двумерная декартова система координат с двумя осями, позволяющими задать положение любой точки плоскости. Реализация Win32 API в Windows NT/2000 поддерживает четыре уровня координат. О Мировая система координат — обеспечивает аффинные преобразования в страничные координаты. Мировая система координат состоит из 232 единиц по горизонтали и 232 единиц по вертикали, поскольку в Win32 координаты представляются в виде 32-разрядных чисел. О Страничная система координат — обеспечивает ограниченные преобразования в систему координат устройства. Страничная система координат состоит из 232 единиц по горизонтали и 232 единиц по вертикали. О Система координат устройства — описывает отдельные пикселы контекста устройства; поддерживает отображение на прямоугольные области физической системы координат. Система координат устройства состоит из 227 единиц по горизонтали и 227 единиц по вертикали, поскольку во внутреннем представлении графического механизма Windows NT/2000 используются знаковые числа с фиксированной точкой, в которых 4 бита отводится под дробную часть. О Физическая система координат — состоит из пикселов графической поверхности физического устройства. Физическая система координат состоит из 227 единиц по горизонтали и 227 единиц по вертикали. ПРИМЕЧАНИЕ В Windows 95/98 используется совершенно иная реализация систем координат и преобразований. Работа Windows 95/98 GDI в значительной степени основана на 16-разрядной реализации GDI, унаследованной от Windows 3.1, которая не поддерживала мировых преобразований и усекала все координаты до 16-разрядных значений. Точнее говоря, хотя в Windows 95/98 при вызовах 32-разрядных графических функций GDI передаются 32-разрядные координаты, они усекаются до 16- разрядных величин при обращении gdi32.dll к 16-разрядной реализации GDI. Давайте рассмотрим эти системы координат в обратном порядке — от физических координат к мировым. Физическая система координат Физическая система координат используется драйвером графического устройства и представляет собой матрицу пикселов фиксированной высоты и ширины. В левом верхнем углу находится точка с координатами (0,0). Ось х направлена слева направо, а ось у — сверху вниз. В графическом механизме Windows NT/2000 координаты представляются знаковыми числами с фиксированной точкой, состоящими из 28-разрядной целой части и 4-разрядной дробной части. Точки с отрицательными координата-
342 Глава 6. Системы координат и преобразования ми, а также с координатами, превышающими ширину и высоту поверхности устройства, считаются отсеченными. Таким образом, максимальный размер физического устройства равен 227 х 227 пикселов, или примерно 1400 х 1400 мс.» при разрешении 2400 точек на дюйм (dpi). Физическую систему координат иллюстрирует рис. 6.1. (0,0) Рис. 6.1. Физическая система координат Физические координаты используются в интерфейсе DDI между графическим механизмом и драйверами графических устройств. Следует учитывать, что физические координаты могут и не соответствовать итоговой системе координат, определяющей местонахождение каждого выводимого пиксела; они являются таковыми лишь с точки зрения графической системы Windows. Драйвер устройства может дополнить графические примитивы дополнительными преобразованиями координат. Скажем, драйвер PostScript преобразует полученные физические координаты в вещественные величины, заданные в пунктах (1/72 дюйма). Сгенерированные данные PostScript могут печататься на разных принтерах в разных разрешениях. Как ни странно, в Windows не существует простых средств определения размеров физического устройства по манипулятору контекста. Вызовы GetDeviceCapsChDC, HORZRES) и GetDeviceCapsChDC, VERTRES) обычно работают нормально, но для совместимых и метафайловых контекстов они возвращают размеры эталонного контекста устройства. Для совместимого контекста устройства размеры физической поверхности определяются размерами выбранного в нем растра. Можно воспользоваться функцией GetObjectType и определить, что вы имеете дело с объектом 0BJJ1EMDC, а затем вызвать GetObject и получить копию структуры BITMAP или DIBSECTI0N растра, выбранного в совместимом контексте; в структуре хранятся размеры выбранного растра. Метафайловый контекст устройства во время построения вообще не привязывается к конкретному устройству, а его размеры могут увеличиваться и уменьшаться в процессе записи графических
Система координат устройства 343 примитивов. Заголовок расширенного метафайла (структура ENHMETAFILEHEADER) содержит информацию о размерах изображения как в логических, так и в физических координатах. Однако не все пикселы физической поверхности могут отображаться устройством. На устройствах создания жестких копий (принтерах и т. д.) существуют механические ограничения на вывод точек у края страницы. Приложение должно получить информацию о печатной части страницы при помощи функции GetDeviceCaps. Для экранного устройства физические координаты также называются экранными координатами (screen coordinates). Экранные координаты удобно использовать в операциях управления окнами. Например, функция GetWindowRect возвращает ограничивающий прямоугольник окна в экранных координатах; в параметрах таких сообщений, как WM_NCM0USEM0VE, тоже передаются экранные координаты. Система координат устройства Координаты устройства (device coordinates) используются при работе с контекстами устройств в Win32 GDI API. В обобщенном виде система координат устройства является подмножеством соответствующей физической системы координат. Для контекстов устройств, созданных функциями CreateDC, CreateIC и Create- CompatibleDC, система координат устройства идентична физической системе координат. Для контекстов устройств, связанных с конкретными окнами (то есть возвращаемых при вызове GetDC, GetWindowDC и BeginPaint), система координат устройства определяется прямоугольником окна или его клиентской области. Как и в физической системе координат, левый верхний угол системы координат устройства имеет координаты (0,0), ось х направлена слева направо, а ось у направлена сверху вниз. Зная манипулятор контекста устройства, вы можете узнать относительную позицию системы координат устройства в его физических координатах при помощи функции GetDCOrgEx. Для определения размеров системы координат устройства необходимо выяснить, соответствует ли она клиентской области окна или всему окну. После этого для манипулятора окна, возвращенного WindowFromDC, вызывается функция GetWindowRect или GetCLientRect. Рисунок 6.2 иллюстрирует систему координат устройства и ее связь с физическими координатами. Физическая система координат изображена в виде большой сетки на заднем плане, а координатам устройства соответствует маленький прямоугольник. Левый верхний угол прямоугольника соответствует точке (24,18) в физической системе координат. Возможности вывода в системе координат устройства со стороны приложения ограничиваются двумя факторами: системным регионом и метарегионом/ регионом отсечения. Как упоминалось в главе 5, системный регион окна представляет собой пересечение видимого региона окна с обновляемым регионом и находится под контролем системы управления окнами, тогда как метарегион и регион отсечения контролируются приложением. Пересечение системного -
344 Глава 6. Системы координат и преобразования региона контекста с метарегионом/регионом отсечения и определяет совокупность пикселов, с которыми приложение может работать через контекст устройства. (0,0) Физические координаты ■ (24,18) Координаты устройства X' Г St IK Рис. 6.2. Связь системы координат устройства с физической системой координат Базовая точка, размеры и системный регион контекста устройства автоматически обновляются системой; GDI ими управлять не может. Следовательно, отображение логической системы координат на физическую выполняется простым смещением (переносом базовой точки). В интерактивных графических приложениях, в которых сообщения мыши используются для выбора, перемещения и редактирования графических объектов или проверки принадлежности, возникает необходимость преобразования между физическими (экранными) координатами и координатами устройства (в системе координат окна или клиентской области). В Win32 API предусмотрено несколько функций для решения этой задачи. BOOL ClientToScreen(HWND hWnd. LPPOINT lpPoint); BOOL ScreenToClienKHWND hWnd. LPPOINT lpPoint): int MapWindowPointsCHWND hWndFrom. HWND hWndTo.LPPOINT IpPoints. UINT cPoints); Функция ClientToScreen преобразует точку (POINT) в координатах клиентской области в экранные координаты; функция ScreenToClient производит обратное преобразование. Функция MapWindowPoints преобразует массив точек из координатной системы одного окна в координатную систему другого окна. Координаты устройства широко используются в Win32 API. В области GDI регионы отсечения задаются именно в координатах устройства, а не в страничных или мировых координатах, из-за чего нередко возникают всевозможные недоразумения и проблемы. Процесс отсечения подробно рассматривается в главе 7. В области управления окнами координаты устройства обычно воплощаются в координатах, заданных по отношению к клиентской области окна. Они исполь-
Страничная система координат и режимы отображения 345 зуются для определения параметров функций CreateWindow и SetWindowPos, а также в сообщениях, связанных с местонахождением курсора мыши — таких, как WM M0USEM0VE и WM LBUTTONDBLCLICK. Страничная система координат и режимы отображения Обе рассмотренные системы координат (физическая и устройства) ограничиваются представлением в виде аппаратно-зависимого массива пикселов. Размер окна на экране с высоким разрешением обычно отличается от размера окна при низком разрешении, а толщина напечатанной линии из трех пикселов будет зависеть от разрешения принтера. Чтобы графическое программирование в меньшей степени зависело от устройства, Windows GDI позволяет приложениям создавать собственные логические системы координат, приближенные к их геометрическим моделям. В таких системах координат удобнее работать, к тому же они в значительно меньшей степени зависят от оборудования. Одной из двух логических систем координат, поддерживаемых в Win32 GDI, является страничная (page) система координат. Кстати говоря, это единственная логическая система координат, поддерживаемая 16-разрядными ОС семейства Windows и даже реализациями Win32 в Windows 95/98/CE. Вторая логическая система координат — мировые координаты — поддерживается только в Windows NT/2000. По историческим причинам в документации Windows и даже в именах функций под «логической» системой координат обычно понимается страничная система. Страничная система координат позволяет приложению строить свою геометрическую модель в произвольных 32-разрядных координатах с произвольно выбранным направлением осей и физическим масштабом. Например, в программе планирования садового участка можно выбрать базовую единицу измерения 1/8 дюйма (или один сантиметр в метрической системе), расположить базовую точку в левом нижнем углу участка, направить ось х слева направо, а ось у — снизу вверх. Такая система координат изображена на рис. 6.3. Например, если ваша лужайка имеет размеры 41 фут на 16 футов 6 дюймов, то, как показано на рисунке, ее размеры задаются парой чисел (328, 132). При отображении геометрической модели на графическом устройстве необходимо ответить на три вопроса — какую часть модели требуется отобразить, где она должна располагаться на поверхности устройства и какими должны быть ее размеры? Это позволит вам отображать разные фрагменты модели в произвольном масштабе и в произвольных точках поверхности. При отображении страничных координат в координаты устройства в Win32 API используются два понятия, часто встречающиеся в компьютерной графике, — окно (window) и область просмотра (viewport). Окном называется любая прямоугольная область в страничной системе координат — например, область, покрытая лужайкой, на рис. 6.3. Областью просмотра называется прямоугольная область в системе координат устройства. Таким образом, окно определяет отображаемую часть геометрической модели, а область просмотра — ее местона-
346 Глава 6. Системы координат и преобразования хождение на поверхности устройства. Соотношение между размерами определяет масштаб вывода. -4 f Рис. 6.3. Проектирование садового участка в логической системе координат Выражаясь точнее, окно определяется четырьмя переменными в страничных координатах: WOrgx Базовая точка окна, координата х WOrgy Базовая точка окна, координата у WExtx Горизонтальные габариты окна WExty Вертикальные габариты окна Область просмотра определяется четырьмя переменными в координатах устройства: VOrgx Базовая точка области просмотра, координата х VOrgy Базовая точка области просмотра, координата у VExtx Горизонтальные габариты области просмотра VExty Вертикальные габариты области просмотра Точка (х,у) в страничной системе координат отображается на точку (х',г/) в координатах устройства по следующим формулам: х' = (х - WOrgx) * VExtx / WExtx + VOrgx у' = (у - WOrgy) * VExty / WExty + VOrgy В этих формулах мы просто вычисляем разность между точкой (хуу) и координатами базовой точки окна, масштабируем ее в пространстве области просмотра и прибавляем к координатам базовой точки области просмотра. Преобра-
Страничная система координат и режимы отображения 347 зование точки из координат устройства в страничные координаты выполняется аналогично: х = (х1 - VOrgx) * WExtx / VExtx + WOrgx у = (у1 - VOrgy) * WExty / VExty + WOrgy Возможны следующие варианты отображений между этими системами координат. О Тождественное отображение. Окно и область просмотра задаются квартетами (0, 0, 1, 1); в этом случае х' = х, у' = у, а страничные координаты идентичны координатам устройства. О Смещение. Окно определяется квартетом (0, 0, 1, 1), а область просмотра — (dx, dy, 1, 1); в этом случае х' = х + dx, а у' = у + dy. Каждая точка страничного пространства при отображении в систему координат устройства смещается на величину (dx,dy). О Масштабирование. Окно определяется квартетом (0, 0, 1, 1), а область просмотра — (0, 0, тх, ту); в этом случае х' = х * тх, а у' = у * ту. Каждая точка страничного пространства при отображении в систему координат устройства масштабируется с коэффициентами (тх, ту). Масштаб может быть произвольным числом — целым или дробным, как большим, так и меньшим 1. Масштабирование по осям хиу выполняется независимо. О Отражение. Окно определяется квартетом (0, 0, ширина, высота), а область просмотра — (ширина,высота,-ширина,-высота)', в этом случае х' = ширина - х, а у' = высота - у. При отображении из страничной системы координат в координаты устройства рисунок может подвергаться зеркальному отражению относительно как горизонтальной, так и вертикальной осей. Отражение позволяет использовать в страничной системе координат направления осей, отличные от фиксированных направлений системы координат устройства. О Комбинированные операции. Любая комбинация перечисленных выше операций. В Win32 API поддерживаются следующие функции для настройки страничной системы координат посредством определения параметров окна и области просмотра: BOOL SetWindowOrgEx (HDC hDC. int X. int Y, LPPOINT pPoint); BOOL SetWindowExtEx (HDC hDC. int X, int Y, LPSIZE pSize); BOOL SetViewportOrgEx (HDC hDC. int X. int Y. LPPOINT pPoint); BOOL SetViewportExtEx (HDC hDC. int X. int Y. LPSIZE pSize): В последнем параметре этих четырех функций передается указатель на структуру POINT или SIZE, заполняемую данными исходного состояния контекста. Для каждой из перечисленных функций существует парная функция, возвращающая информацию об окне и области просмотра. См. описание функций GetWindowOrgEx, GetWindowExtEx, GetViewportOrgEx и GetViewportExtEx в документации Win32. На первый взгляд выглядит довольно запутанно, не правда ли? Для упрощения работы программиста в Win32 API поддерживается несколько заготовок страничных систем координат, называемых режимами отображения (mapping modes). В большинстве режимов отображения устанавливаются заранее выбранные габариты окна и области просмотра, определяющие размер единицы изме-
348 Глава 6. Системы координат и преобразования рения в страничной системе координат и коэффициент масштабирования при переходе к системе координат устройства. Впрочем, приложение может изменять положение базовой точки окна и области просмотра, что позволяет выводить разные фрагменты геометрической модели в разных частях экрана. Режим отображения контекста устройства выбирается следующей функцией: int SetMapMode(HDC hDC. int fnMapMode); Режим отображения ММ_ТЕХТ Простейший режим отображения MMJTEXT устанавливается вызовом SetMapModeChDC, ММ_ТЕХТ). Этот режим выбирается по умолчанию во вновь созданных контекстах устройств. В режиме отображения MMJTEXT используются фиксированные габариты окна и области просмотра (1,1), а базовые точки окна и области просмотра по умолчанию имеют координаты (0,0). Таким образом, по умолчанию страничные координаты в контексте устройства совпадают с координатами устройства. В режиме MMJTEXT приложение может изменять положение базовых точек окна и области просмотра, поэтому общие формулы для преобразования страничных координат в координаты устройства выглядят так: х' = х - WOrgx + VOrgx у' = у - WOrgy + VOrgy Настройка осей в режиме MMJTEXT хорошо подходит для вывода текста в стандартном направлении (слева направо, сверху вниз). Вероятно, именно этим и объясняется выбор названия режима. Простые графические приложения тоже часто используют его при работе с экраном. Если вы захотите выполнить печать в режиме MMJTEXT, вам придется самостоятельно пересчитывать координаты и масштабировать изображение, чтобы результат имел одинаковые размеры на принтерах с разным разрешением. Режим MMJTEXT не позволяет изменять относительный масштаб осей, поскольку пропорции размеров окна и области просмотра в нем жестко фиксируются. Режимы отображения MM_LOENGLISH и MMJHIENGLISH За основу физических единиц, используемых в режимах отображения MML0ENGLISH и MMHIENGLISH, взят дюйм — традиционная английская единица измерения. В режиме MML0ENGLISH одна единица в страничной системе координат соответствует 1/100 дюйма, тогда как в режиме MMHIENGLISH она соответствует 1/1000 дюйма. В режиме MML0ENGLISH один дюйм равен 100 единицам, полдюйма — 50 единицам, четверть дюйма — 25 единицам, а одну восьмую дюйма невозможно представить без потери точности. В режиме MM HIENGLISH один дюйм соответствует 1000 единиц, полдюйма — 500 единицам, четверть дюйма — 250 единицам, а одна восьмая дюйма — 125 единицам. Направление оси у в этих двух режимах совпадает с ее направлением в традиционной декартовой системе координат, то есть ось у направлена снизу вверх (см. рис. 6.3). В этом отношении режимы MML0ENGLISH и MMHIENGLISH отличаются от режима MMJTEXT, системы координат устройства и физической системы координат.
Страничная система координат и режимы отображения 349 Выбрать режим отображения MMLOENGLISH или MMHIENGLISH в приложении несложно — для этого достаточно вызвать функцию SetMapMode(hDC. MMLOENGLISH) или SetMapMode(hDC, MMHIENGLISH). GDI автоматически изменяет габариты окна и области просмотра. Ниже приведена примерная реализация этих двух режимов в функции SetMapMode: BOOL SetMapMode(HDC hDC. int fnMapMode) { // Установить в контексте режим отображения fnMapMode int mill; int div; switch ( fnMapMode) { case MMJIENGLISH: mul - 10000; div « 254: break; case MM_LOENGLISH: mul = 1000; div - 254; break; default: return FALSE; } SetWindowExtExChDC, GetDeviceCaps(hDC. HORSIZE) * mul / div. GetDeviceCaps(hDC. VERTSIZE) * mul /div. NULL); SetViewportExtEx(hDC. GetDeviceCaps(hDC. HORZRES). - GetDeviceCaps(hDC, VERTRES). NULL); return TRUE; } При установке этих двух режимов в контексте устройства используются данные о размерах устройства в физических единицах и пикселах. Скажем, при задании константы HORZRES функция GetDeviceCaps возвращает ширину поверхности физического устройства в пикселах. В режимах MMLOENGLISH и MMHIENGLISH ширина области просмотра совпадает с шириной поверхности устройства, а высота области просмотра равна высоте поверхности устройства с обратным знаком. Таким образом, ось х сохраняет то же направление, что и в системе координат устройства, а ось у направлена в обратную сторону. Ширина окна вычисляется умножением физического размера поверхности устройства на количество единиц в 1/10 дюйма и делением результата на 254. Обратите внимание: физические размеры устройства задаются в миллиметрах (один дюйм равен примерно 25,4 мм). Для экрана размером 1152 х 864 пиксела GetDeviceCaps возвращает физические размеры 320 х 240 мм. Следовательно, SetMapMode (hDC, MMHIENGLISH) устанавливает габариты окна (12 598,9449) и габариты области просмотра (1152,-864). Возможно, вас интересует, почему вместо этих величин не используется логическое разрешение, возвращаемое вызовами GetDeviceCaps(hDC.LOGPIXELSX) и GetDeviceCaps(hDC. LOGPIXELSY)? Использование логического разрешения изменило бы настройку страничной системы координат. Например, для того же экрана размером 1152 х 864 пиксела драйвер устройства возвращает логическое разре-
350 Глава 6. Системы координат и преобразования шение (96,96). Если считать, что физические размеры поверхности устройства составляют 12 х 9 дюймов, габариты окна в режиме MMHIENGLISH должны быть равны (12 000,9600). Термин «логический» в данном случае означает, что значения не являются абсолютно точными. Драйвер экрана обычно поддерживает разные размеры кадровых буферов, и видеоадаптер может подключаться к мониторам разных размеров. Пользователи обычно предпочитают, чтобы документ четко отображался на экране, а размеры изображения хорошо подходили для чтения и редактирования содержимого документа. Не так уж важно, отображается ли страница формата Letter с шириной ровно 8,5 дюйма. В нормальном разрешении драйвер экрана сообщает, что логическое разрешение равно 96 dpi, тогда как в высоком разрешении оно равно 120 dpi. А вот печатный вывод должен быть действительно точным — скажем, выходные данные бухгалтерской программы должны точно уместиться в полях готового бланка. Для устройств создания жестких копий величина логического разрешения существенна. В Windows NT/2000 интерфейс GDI при настройке режима отображения в большей степени полагается на размеры графического устройства, полученные от драйвера экрана. Драйвер экрана получает информацию о физических размерах экрана у драйвера видеопорта. Теоретически драйвер экрана мог бы сообщить точные размеры, если бы он получал информацию от монитора. Однако автору еще не приходилось видеть ни одного драйвера экрана, который бы сообщал что-то кроме 320 х 240 мм — стандартных размеров 17-дюймового монитора. Если вы работаете с режимом отображения, зависящим от физических размеров устройства, не используйте величину логического разрешения в приложении, чтобы избежать возможных несоответствий. Также следует помнить о том, что вызов SetMapMode не изменяет базовой точки окна и области просмотра. Исходные значения сохраняются, и приложение может изменять их по своему усмотрению. Если говорить о разрешении, режим MMHIENGLISH соответствует 1000 dpi, а режим MM_L0ENGLISH - 100 dpi. Режимы отображения MM_LOMETRIC и MM_HIMETRIC Метрические режимы отображения MML0METRIC и MMHIMETRIC похожи на английские режимы, рассмотренные в предыдущем разделе. В режиме MML0METRIC базовая единица равна 0,1 мм, а в режиме MM_HIMETRIC она равна 0,01 мм. Как и в двух предыдущих режимах, ось х направлена слева направо, а ось у направлена снизу вверх. Чтобы включить поддержку метрических режимов отображения, в приведенную выше псевдореализацию SetMapMode следует добавить фрагмент: case MMJIMETRIC: mul = 100; div = 1; break; case MM_L0METRIC; mul = 10; div = 1; break; Разрешение в режиме MMHIMETRIC соответствует 2540 dpi, а в режиме MM_L0METRIC - 254 dpi.
Страничная система координат и режимы отображения 351 Режим отображения MM__TWIPS Метрические режимы отображения предназначены для стран, использующих метрическую систему. Английские режимы используются в странах, продолжающих работать в классических единицах, однако для печати нужны другие единицы измерения. Традиционной единицей в типографском деле является пункт (point), равный примерно 1/72,228 (или 0,013835) дюйма. В современных системах компьютерной верстки 1 пункт нормализуется ровно до 1/72 (0,13889) дюйма, что превышает исходный размер на 0,4 %. В приложениях Windows пункты используются для измерения размера шрифта. Например, размер (кегль) стандартного типографского текста равен 10 пунктам, а межстрочные интервалы составляют 12 пунктов. В языке PostScript пункты являются основной единицей измерения, а все координаты и размеры выражаются в пунктах, заданных посредством вещественных чисел. Английские и метрические режимы не обеспечивают необходимой точности при форматировании текста, поэтому в Win32 API для подобных задач предусмотрен дополнительный режим отображения MMTWIPS. Логическая единица в MMJTWIPS равна 1/20 пункта, то есть 1/1440 дюйма; эти единицы называются тейпами (twips). Если не считать специфических единиц измерения, режим отображения MMTWIPS аналогичен другим режимам, основанным на физических единицах. Чтобы включить поддержку режима MMTWIPS, в приведенную выше псевдореализацию SetMapMode следует добавить фрагмент: case MMJTWIPS: mul = 14400; div = 254; break; He стоит и говорить о том, что режим MMTWIPS соответствует разрешению 1440 dpi. Этого разрешения обычно бывает достаточно для точного распределения интервалов при выравнивании, сжатии и расширении текста. Режимы отображения MM__ISOTROPIC В физике термин «изотропный» означает «обладающий одинаковыми свойствами по всем направлениям». В Win32 GDI режим отображения MM_IS0TR0PIC соответствует произвольному режиму отображения с одинаковым отношением габаритов окна/области просмотра по обеим осям без учета направления. В формальной записи это выглядит так: abs(WExtx / VExtx) = abs(WExty / VExty) или abs(WExtx / VExty) = abs(WExty / VExtx) Чтобы использовать изотропный режим отображения, сначала следует вызвать функцию SetMapModeC hDC, MMIS0TR0PIC). Как показали эксперименты, GDI заимствует параметры окна и области просмотра из режима отображения MML0METRIC. После этого сначала вызывается функция SetWindowExtEx, а затем — функция SetViewportExtEx; GDI следит за тем, чтобы по обеим осям выдерживался одинаковый масштаб. Реализация изотропного режима отображения в Windows NT/2000 не безупречна. Как показали наши эксперименты, в режиме MM_IS0TR0PIC функции SetWindowExtEx
352 Глава 6. Системы координат и преобразования и SetViewportExtEx сначала вызываются вполне обычным образом, после чего GDI выполняет нормализацию в соответствии с требованием изотропности. Для этого GDI выбирает среди WExtx, VExtx, WEtxy и VExty переменную с наибольшим абсолютным значением и вычисляет ее новое значение по трем оставшимся переменным. Некоторые результаты тестов приведены в табл. 6.1. Таблица 6.1. Настройка режима MMJSOTROPIC Вызов функции API (сокращенно) Габариты окна Габариты области просмотра Комментарии SetMapMode(MMJSOTROPIC) (3200,2400 (1152,-864) ) SetWindowExtEx(3,5) (3,5) SetViewportExtEx(5,3) (3,5) (518,-864) (2,3) По каким-то соображениям используются габариты MM_L0METRIC Наибольшее число 1152 заменяется величиной 864 х 3/5; погрешность составляет 0,07 % Наибольшее число 5 заменяется величиной 3x3/5 В этом примере последовательно вызываются функции SetWindowExtEx и SetViewportExtEx. Из таблицы ясно видно, что GDI пытается обеспечить изотропию, изменяя лишь число с наибольшим абсолютным значением — подобный упрощенный подход приводит к большой погрешности. Итоговые значения далеки от изотропии. В данном примере одно из возможных решений заключалось в том, чтобы присвоить окну габариты (9,15), а области просмотра — габариты (15,25); в этом случае отображение будет действительно изотропным. В GDI габариты окна и области просмотра представляются целыми числами. Для получения изотропных отображений иногда приходится выполнять аппроксимацию, которая, как показано в табл. 6.1, может привести к некоторым нарушениям изотропности. Автор рекомендует забыть о мелких удобствах, предоставляемых режимом MM_IS0TR0PIC, и работать непосредственно в режиме ММ_ ANISOTROPIC. Режим отображения MM_ANISOTROPIC Термин «анизотропный» означает «обладающий разными свойствами по разным направлениям». Впрочем, режим отображения MM_ANISOTROPIC на самом деле позволяет использовать любые габариты окна и области просмотра, изотропные и анизотропные. Все режимы отображения, упоминавшиеся до настоящего момента, в той или иной степени ограничивали настройку габаритов окна и области просмотра. В режимах MMJTEXT, MM_L0ENGLISH, MMJUENGLISH, MMJ.0METRIC, MMHIMETRIC и MMTWIPS используются фиксированные габариты окна и области просмотра, которые не могут изменяться приложением. Режим MMIS0TR0PIC позволяет менять габари-
Страничная система координат и режимы отображения 353 ты, но GDI автоматически следит за тем, чтобы масштабы по обеим осям совпадали или были достаточно близки. MM_ANISOTROPIC — единственный режим отображения, в котором приложение может произвольно менять габариты окна и области просмотра. При вызове SetMapMode(hDC,MM_ANISOTROPIC) в контексте устройства устанавливается анизотропный режим отображения без изменения других атрибутов. После этого приложение завершает настройку режима отображения, в произвольном порядке вызывая функции SetWindowExtEx и SetViewportExtEx. Режим MM_ANISOTROPIC позволяет имитировать все остальные режимы отображения, а также создавать ваши собственные режимы. Приложения Windows часто позволяют выбирать масштаб просмотра документа — 500 %, 200 % и т. д. до 25 % и 10 %. При масштабе 100 % один пиксел документа соответствует одному пикселу экрана, а один дюйм документа — одному дюйму экрана; при масштабе 500 % выполняется увеличение в пропорции 5:1, а при масштабе 25 % происходит уменьшение 1:4. Например, в графическом редакторе, где логической единицей является пиксел, изотропный режим отображения в масштабе т:п устанавливается следующим фрагментом: SetMapMode(hDC. MM_ANISOTROPIC); SetExtentsChDC. n. п. m. m); Ниже приведена функция SetExtents, которая исключает общие множители из параметров и устанавливает габариты: int gcdCint x, int у) // Наибольшее общее кратное { while (х!=у) if ( х > у ) х -= у; else у-= х: return x; } B00L SetExtents (HDC hDC. int wx. int wy, int vx. int vy) { int gx = gcd(abs(wx), abs(vx)); int gy = gcd(abs(wy). abs(vy)); SetWindowExtEx (hDC. wx/gx, wy/gy. NULL): return SetViewportExtEx (hDC. vx/gx. vy/gy. NULL): } Если в программе компьютерной верстки в качестве логической единицы выбран твип (1/20 пункта, или 1/1440 дюйма), масштаб т:п устанавливается следующим фрагментом: SetMapMode(hDC. MM_ANISOTROPIC): SetExtents(hDC. n * 1440. n * 1440. m * GetDeviceCaps(hDC. L0GPIXELSX). m * GetDeviceCaps(hDC. L0GPIXELSY)): В этом фрагменте используется логическое разрешение, возвращаемое драйвером устройства. Если вы предпочитаете вычислять разрешение по физическим размерам устройства, это делается так: SetMapMode(hDC. MM_ANIS0TR0PIC): SetExtents(hDC. n * 1440. n * 1440.
354 Глава 6. Системы координат и преобразования m * GetDeviceCapsChDC. HORZRES) * 254 / GetDeviceCapsChDC. HORZSIZE) / 10. m * GetDeviceCapsChDC. VERTRES) * 254 / GetDeviceCapsChDC, VERTSIZE) / 10); В этом фрагменте габариты задаются положительными числами, поэтому направление осей в страничной системе координат совпадает с направлением в системе координат устройства. Чтобы изменить направление осей, достаточно изменить знак габаритов области просмотра. Помимо фиксированных масштабов, в приложениях Windows часто поддерживается оперативное вычисление масштаба, позволяющего разместить определенную часть документа на всех поверхностях устройства. По размерам поверхности устройства могут масштабироваться ширина страницы, вся страница полностью или две соседние страницы. Обобщенная функция для решения подобных задач приведена в листинге 6.1. Листинг 6.1. Размещение в окне col x row страниц BOOL FitPagesCHDC hDC. int pagewidth. int pageheight. int dcwidth, int dcheight. int col, int row, int margin, int gap) { // Вычислить итоговые размеры col x row страниц int width = margin*2 + pagewidth *col + gap*(col-l); int height = margin*2 + pageheight*row + gap*(row-l); if (width <= 0) width = 1; // Избегаем нулевых значений if (height <= 0) height = 1; // Избегаем нулевых значений if ( dcheight * width > dcwidth * dcheight ) { dcheight = dcwidth; height = width; } else { dcwidth = dcheight; width = height; } return SetExtents (hDC, width, height, dcwidth. dcheight); } Эта функция решает общую задачу размещения col x row страниц, каждая из которых имеет размеры pagewidth x pageheight, в координатном пространстве размерами dcwidth x dcheight. На всех четырех краях документа присутствуют поля размером margin, а страницы разделяются интервалами gap. Программа сначала вычисляет итоговые размеры блока из col x row страниц с учетом всех факторов. Затем сравниваются отношения габаритов «область просмотра/окно» по двум осям, и меньшее отношение используется для регулировки габаритов области просмотра. Чтобы вы лучше поняли, как работает эта функция, мы рассмотрим пару примеров. Предположим, размеры каждой страницы документа равны 850 х 1100 еди-
Страничная система координат и режимы отображения 355 ниц, поверхность устройства состоит из 1024 х 768 пикселов, а размеры полей и межстраничных интервалов равны 0. Масштабирование по ширине страницы можно рассматривать как задачу размещения 1x0 страниц, то есть FitPagesChDC,850,1100,1024,768,1.0.0,0). Блок 1x0 страниц имеет размеры 850 х 1 единиц (с округлением, предотвращающим деление на 0), поэтому отношение по оси у (768:1) больше отношения по оси х (1024:850). Программа выбирает высоту области просмотра, равную 1024, и высоту окна, равную 850. Окончательные габариты окна равны (850,850), а габариты области просмотра — (1024,1024). Ширина страницы соответствует ширине пространства координат устройства. Перейдем к размещению двух страниц на поверхности устройства. Блок 2x1 имеет размеры 1700 х 1100, поэтому отношение габаритов области просмотра и окна по оси у (768:1700) оказывается меньше отношения по оси х (1024:1100). Программа выбирает ширину области просмотра, равную 768, и высоту окна, равную 1100. Теперь мы отображаем (1100,1100) единиц на (768,768) пикселов, что упрощается функцией SetExtents до (275,275) на (192,192). Настройка габаритов окна и области просмотра определяет, сколько единиц в страничной системе координат соответствует тому или иному количеству единиц в системе координат устройства. В Windows GDI окно и область просмотра описывают отображение страничной системы координат на систему координат устройства, а отсечение выполняется независимо в координатах устройства. Здесь важны лишь отношения габаритов, а не их конкретные значения. Например, в режиме MMJTEXT и окну, и области просмотра назначаются габариты (1,1) и базовые точки (0,0), но это вовсе не означает, что в системе координат отображается всего один пиксел. Базовые точки окна и области просмотра После настройки габаритов окна и области просмотра приложение задает положение базовых точек окна и области просмотра. После завершения настройки GDI отображает базовую точку окна в страничной системе координат на базовую точку области просмотра в системе координат устройства; остальные точки отображаются в соответствии с заданными параметрами. По умолчанию базовые точки окна и области просмотра имеют координаты (0,0); выбор режимов отображения и настройка габаритов не приводит к их изменению. Чтобы понять, нужно ли изменять координаты базовых точек окна и области просмотра, программист должен знать направления осей х и г/, использованные при настройке окна и области просмотра. В режимах MMJTEXT и в режиме MM_ANISOTROPIC (по умолчанию) ось х направлена слева направо, а ось у — сверху вниз. Следовательно, в страничной системе координат на координатное пространство устройства отображается только первый квадрант, определяемый положительными значениями по осям х и г/, а остальные квадранты не видны. Если вы хотите, чтобы базовая точка страничной системы координат отображалась в центр системы координат устройства, воспользуйтесь следующим фрагментом: SetWindow0rgEx(hDC. 0. 0. NULL); SetViewportOrgExChDC. dcHeight/2, dcWidth/2. NULL);
356 Глава 6. Системы координат и преобразования Изменения продемонстрированы на рис. 6.4. Ту- X- Х+ У+ т Рис. 6.4. Изменение базовой точки в режиме отображения ММ_ТЕХТ Следует помнить, что такое отображение может быть реализовано и иначе. Базовой точке области просмотра можно присвоить координаты (0,0), а базовой точке окна — координаты (-dcHeight*WExtx/VExtx/2,-dcWidth*WExty/VExty/2). С точки зрения математики эти два способы эквивалентны, но второй способ приводит к несколько большим погрешностям округления. В остальных режимах отображения направление оси у по умолчанию отличается от ее направления в режиме ММТЕХТ. В них используется традиционное для декартовой системы координат направление — снизу вверх. При этом в пространство координат устройства отображается только второй квадрант, с положительными значениями х и отрицательными значениями у. Для отображения базовой точки окна в центр пространства координат устройства по-прежнему можно воспользоваться приведенным выше фрагментом. Но если вы просто хотите отображать в пространство координат устройства первый квадрант, это делается так: SetWindowOrgEx(hDC. 0. 0. NULL); SetViewportOrgExChDC. dcHeight, 0, NULL); Аналогичная ориентация оси у используется и в языке PostScript. Отличия показаны на рис. 6.5. Рис. 6.5. Изменение базовой точки в других режимах отображения В режимах MMIS0TR0PIC и MM_AN ISOTROPIC приложение произвольно определяет направления осей. Впрочем, действовать нужно внимательно, поскольку GDI
Мировая система координат 357 учитывает настройки при реализации графических примитивов за одним исключением: GDI всегда выводит текстовые строки в одном направлении, если только контекст устройства не находится в расширенном графическом режиме. Даже если ось х направлена справа налево, текстовые строки все равно выводятся слева направо, без зеркального отражения глифов. Это может причинить немало хлопот, если приложение использует режимы отображения для зеркального отражения документов относительно оси у. Проблема решается либо имитацией отражения текста в совместимом контексте, либо мировыми преобразованиями в мировой системе координат. Другие функции окна и области просмотра В Win32 API предусмотрено несколько вспомогательных функций, упрощающих управление текущими настройками окна и области просмотра. Функция GetMapMode возвращает информацию о текущем режиме отображения. Функции OffsetWin- dowOrgEx и OffsetViewportOrgEx изменяют положение базовой точки окна или области просмотра. Они особенно удобны для пошагового смещения окна и области просмотра (например, при обработке сообщений прокрутки). Функции Scale- WindowExtEx и ScaleViewportExtEx масштабируют габариты окна и области просмотра с дробным коэффициентом, что может пригодиться при обработке запросов на масштабирование. Мировая система координат С точки зрения профессионального графического программирования отображение «окно/область просмотра» и различные режимы отображения являются компромиссом, глубоко укоренившимся в исходной архитектуре Win 16 GDI API, когда процессоры работали на тактовой частоте 8 Мгц, а память стоила дорого. Вследствие этого в архитектуре и реализации GDI приходилось искать как можно более простые и эффективные решения. Ниже перечислены некоторые недостатки архитектуры страничного координатного пространства. О Дробные коэффициенты при отображении окна на область просмотра. И окно, и область просмотра описываются целыми числами, поэтому при отображении окна на область просмотра используются дробные коэффициенты. Значения дробей легко вычисляются посредством целочисленного умножения, сопровождаемого делением. Однако итоговые целые числа выбираются из ограниченного набора, что может привести к потере точности. Например, как было показано выше, в режиме MMIS0TR0PIC может возникать расхождение масштабных коэффициентов по осям. О Неполная реализация. Вывод текста не соответствует семантике отражения относительно оси у. Другими словами, если приложение направляет ось х справа налево, текстовые строки все равно выводятся слева направо. О Ограниченный набор преобразований. Отображение окна на область просмотра позволяет выполнять преобразования смещения, масштабирования и зер-
358 Глава 6. Системы координат и преобразования кального отражения. Вращение и перекос не поддерживаются, а без прямой поддержки со стороны GDI реализовать их очень трудно. В Windows NT/2000 для решения этих проблем была создана новая логическая система координат — мировые координаты. В мировом координатном пространстве координаты по-прежнему задаются в виде 32-разрядных целых чисел, в отличие от других графических систем (например, PostScript), использующих вещественные числа. При отображении точек в мировой системе координат на страничное координатное пространство появляется возможность использования более общих преобразований. Аффинные преобразования Возможны разные виды преобразований из одной системы координат в другую. Скажем, преобразование перспективы отображает трехмерные объекты на двумерную поверхность, а преобразование «рыбьего глаза» имитирует искажение объектов, рассматриваемых через особую линзу. Преобразования, поддерживаемые в Windows NT/2000, относятся к классу двумерных аффинных преобразований. Аффинное преобразование отображает параллельные линии в параллельные линии, а конечные точки — в конечные точки. Двумерное аффинное преобразование определяется шестью числами, образующими матрицу 2 х 3. В Win32 API такие матрицы определяются структурой XF0RM. typedef struct _XF0RM { FLOAT eMll; FLOAT eM12: FLOAT eM21; FLOAT eM22; FLOAT eDx; FLOAT eDy; } XFORM; Аффинное преобразование, определяемое этими числами, преобразует точку (х,у) в (х\у% где х' - еМИ * х + еМ21 * у + eDx; у' - еМ12 * х + еМ22 * у + eDy; На первый взгляд похоже на формулы отображения окна на область просмотра, но в действительности аффинное преобразование обладает более широкими возможностями. Отображение окна на область просмотра, используемое в страничной системе координат, можно рассматривать как особый класс аффинных преобразований, в котором еМ21 и еМ12 равны нулю. Аффинные преобразования позволяют выполнять следующие операции. О Тождественное отображение. Определяется матрицей {1,0,0,1,0,0}; х = г, у' = у. О Смещение. Определяется матрицей {l,0,0,l,<ir,dz/}, х' = х + dx\ z/' = у + dy. О Масштабирование. Определяется матрицей {/тглт,0,0,/72г/,0,0}, х' = тх*х\ у' = = ту*у. О Зеркальное отражение. Определяется матрицей {-1,0,0,-1,0,0}, х' = -х\ у' - - -#•
Мировая система координат 359 О Поворот. Определяется матрицей {cos(6), sin(0), -sin(0), cos(0), 0, 0}. х' = = cos(0) xx - sin(0) x?/;z/'= sin(0) xi+ cos(0) xу. Точка (х,у) поворачивается на угол и относительно базовой точки в направлении против часовой стрелки. О Сдвиг. Определяется матрицей {1,5,0,1,0,0}. х' = .г + s x z/, у' = у. Координаты х смещаются на величину, пропорциональную у. О Комбинированные операции. Матрицы нескольких аффинных преобразований объединяются операцией матричного умножения и образуют новое аффинное преобразование. Шесть основных преобразований иллюстрирует рис. 6.6. Тождество TL OL Смещение Масштабирование Отражение Поворот Сдвиг Рис. 6.6. Простые двумерные аффинные преобразования Тождественное преобразование, смещение, масштабирование и отражение уже были продемонстрированы при отображении страничных координат в координаты устройства, хотя на этот раз мы свободно используем вещественные числа. Поворот — новая и очень полезная операция. Поворот системы координат вокруг базовой точки осуществляется по следующим формулам: х' = cos(theta) * х - sin(theta) * у у" = sin(theta) * х + cos(theta) * у Общая задача поворота вокруг произвольной точки (х0,у0) решается в три этапа. Сначала выполняется смещение (-х0,-у0), затем поворот и, наконец, обратное смещение (хО,уО). Итоговое преобразование выглядит следующим образом: х' - cos(theta) * (х-хО) - sin(theta) * (у-уО) + хО у* = sin(theta) * (х-хО) + cos(theta) * (у-уО) + уО При сдвиге (поперечной деформации) к одной координате прибавляется величина, пропорциональная значению другой координаты. Сдвиг также может осуществляться по обеим координатам, поэтому общие формулы сдвига выглядят так: х* = х + h * у у' - g * х + у Аффинные преобразования обладают рядом интересных свойств, из-за которых они получили широкое распространение в компьютерной графике. Понима-
360 Глава 6. Системы координат и преобразования ние этих свойств поможет вам лучше понять, как определяемая приложением геометрическая модель трансформируется при использовании этих преобразований. Кроме того, это дает некоторое представление о внутренней реализации преобразований графическим механизмом. О Аффинные преобразования сохраняют прямые линии. Прямая линия отображается в прямую линию, треугольник отображается в треугольник, а многоугольник отображается в многоугольник. Чтобы реализовать аффинные преобразования для линий и многоугольников, графическому механизму достаточно вычислить отображения их вершин, а затем провести линии или соединительные отрезки. О Аффинные преобразования сохраняют параллельность (параллельные линии отображаются в параллельные линии). Таким образом, параллелограмм отображается в параллелограмм, хотя прямоугольники не всегда отображаются в прямоугольники, а квадраты не обязательно отображаются в квадраты. О Аффинные преобразования сохраняют эллипсы. Эллипс в результате аффинного преобразования всегда отображается в эллипс, хотя круг не всегда отображается в круг. Средствами GDI API определяются только ортогональные круги и эллипсы, оси которых параллельны осям х и г/, но при помощи аффинных преобразований можно нарисовать на поверхности устройства произвольный эллипс. О Аффинные преобразования сохраняют кривые Безье. Как и в случае с линиями и многоугольниками, GDI остается лишь применить аффинные преобразования к вершинам, определяющим кривую Безье, а затем объединить их в преобразованную кривую Безье в координатах устройства. О Аффинное преобразование однозначно определяется тремя вершинами не- коллинеарных векторов р, q, г в исходной системе координат и тремя вершинами неколлинеарных векторов р\ q\ r' в результирующей системе координат. Другими словами, существует только одно аффинное преобразование, которое отображает р, q, r соответственно в р\ q\ r\ Обратите внимание: все отображения из страничной системы координат в систему координат устройства, поддерживаемые GDI, однозначно определяются двумя точками в каждой из систем координат. Перечисленные свойства аффинных преобразований отражены в реализации графических примитивов GDI, примененной в Windows NT/2000. Как было сказано выше, круг или эллипс в результате аффинного преобразования отображается в круг или эллипс, который может и не быть ортогонален осям. В компьютерной графике эффективно вывести произвольный эллипс очень трудно. Как графический механизм подходит к решению это задачи? Каждый эллипс разбивается на четыре кривые Безье, которые легко отображаются и воспроизводятся в системе координат устройства как кривые Безье. Поскольку аффинные преобразования определяются вещественными числами вместо дробей, используемых при отображении окна в область просмотра, во внутренних операциях графического механизма Windows NT/2000 применяются числа с фиксированной точкой, что делает вычисления более точными. С математической точки зрения аффинное преобразование представляет собой функцию t: R х R->R x R в форме t(x) = Ах + ft, где А — инвертируемая
Мировая система координат 361 матрица 2 х 2, а Ь — точка в R x R. Совокупность всех аффинных преобразований R х R обозначается Л(2). Можно доказать, что множество аффинных преобразований А(2) образует группу по отношению к множеству композиционных операций. Термин «группа» в данном случае является понятием абстрактной алгебры и определяется как произвольный набор операций над некоторым полем, обладающий свойствами замкнутости, существования тождественной (единичной) и обратной операции и ассоциативности. Для аффинных преобразований эти свойства определяются следующим образом. О Замкнутость. Композиция любых двух аффинных преобразований также является аффинным преобразованием. Если tl(x) = А\ х х + Ы и t2(x) = = Л2 х х + Ь2, то (tl х t2)(x) = (А1 х А2) х х + (Л1 х Ъ2 + М). О Наличие тождественного преобразования. Существует тождественное (единичное) аффинное преобразование i(x), при котором для любого аффинного преобразования t(x) справедливы утверждения (i x t)(x) = t(x) и (t x г)(лг) = = t(x). В действительности i(x) = I х х + О, где / — тождественная матрица 2x2. О Наличие обратного преобразования. Преобразование, обратное к аффинному, также является аффинным. Обратным к t(x) = А х х + Ъ является преобразование (1/Л) хх- (1/Л) х Ь. О Ассоциативность. Композиция аффинных преобразований ассоциативна; иначе говоря, для любых аффинных преобразований t1, t2 и t3 выполняется условие (t1 х t2) xt3 = t1 x (t2 x t3). На первый взгляд эти математические концепции кажутся абстракцией, но они приносят чрезвычайно большую пользу в компьютерной графике. Нередко приложению приходится определять несколько преобразований, объединяемых в одно итоговое преобразование. В частности, эта возможность используется во внутренних операциях графического механизма для объединения двух преобразований (из мировых координат в страничные, а из страничных — в координаты устройства). Помните, что отображение страничного координатного пространства в пространство координат устройства также является частным случаем аффинных преобразований. Обратные преобразования упрощают обратное отображение из страничных координат или координат устройства в мировые. В частности, это свойство используется для отображения координат событий мыши в мировое координатное пространство. Графический механизм задействует тот же программный код, но только для обратного преобразования. Функции мировых преобразований в Win32 API Довольно отвлеченных рассуждений об аффинных преобразованиях. Мы переходим к рассмотрению поддержки мировой системы координат и мировых преобразований в Win32 API. По умолчанию контекст устройства работает в так называемом совместимом графическом режиме (имеется в виду совместимость с 16-разрядной семантикой GDI). В совместимом режиме мировые координаты не поддерживаются, а единственной логической системой координат является страничная система. Если приложение захочет разрешить использование мировых координат, оно должно
362 Глава 6. Системы координат и преобразования переключить контекст устройства в графический режим вызовом функции Set- GraphicsMode(hDC, GM_ADVANCED), реализованной только в Windows NT/2000. В результате вызова обеспечивается поддержка контекстом устройства двух уровней логического координатного пространства — мировых и страничных координат, а также матрицы преобразования. Информацию о текущем графическом режиме возвращает функция GetGraphicsMode(hDC). Чтобы вернуться к совместимому режиму, заполните матрицу данными тождественного преобразования и вызовите SetGraphicsModeChDC, GMCOMPATIBLE). Кроме того, можно воспользоваться функциями SaveDC и RestoreDC. Переключение контекста устройства в режим GMADVANCED не ограничивается включением поддержки мировых координат. Оно также оказывает значительное влияние на реализацию графических примитивов, используемую в GDI. Различия между двумя режимами перечислены в табл. 6.2. Таблица 6.2. Различия между графическими режимами GDI Область GM COMPATIBLE GM ADVANCED Логические системы координат Платформа Направление вывода текста Масштабирование текста Прямоугольники Направление дуг Отображение дуг Страничные координаты Windows 95/98, Windows NT/2000 Текст всегда выводится слева направо сверху вниз, даже если направление осей было изменено путем смены режима отображения. Изменить направление текста можно только изменением угла наклона и ориентации логического шрифта или же совместимого контекста устройства Масштабируются только шрифты TrueType. Правая и нижняя граница исключаются из прямоугольника Дуги выводятся в направлении, заданном функцией SetArcDirection При выводе дуг отображения не учитываются Мировые и страничные координаты Windows NT/2000 Текстовые строки выводятся в соответствии с действующими преобразованиями и отображениями Масштабируются шрифты TrueType и векторные шрифты. GDI пытается обеспечить оптимальное качество вывода растровых шрифтов, но результат не гарантирован Правая и нижняя граница входят в прямоугольник В логических координатах дуги всегда выводятся против часовой стрелки Дуги выводятся в соответствии с преобразованиями и отображениями
Мировая система координат 363 В контексте устройства преобразование из мировых координат в страничные по умолчанию инициализируется тождественной матрицей. Для изменения текущего преобразования используются следующие функции: BOOL SetWorldTransformCHDC hDC. CONST XFORM * lpXform); BOOL ModifyWorldTransformCHDC hDC. CONST XFORM * IpXformm DWORD iMode); Функция SetWorldTransform просто заменяет атрибут преобразования в контексте устройства новым преобразованием, заданным структурой XFORM. Обратите внимание: не каждая шестерка чисел типа FLOAT определяет правильное аффинное преобразование. Формальное определение аффинных преобразований требует, чтобы матрица 2x2, образованная числами еМН, еМ12, еМ21 и еМ22, была обратимой; то есть должно выполняться условие еМП х еМ22 != еМ12 х еМ21. Например, следующая попытка завершится неудачей: XFORM xm = {1, 2. 1, 2, 3. 4}; SetGraphicsMode(hDC, GM_ADVANCED); BOOL result - SetWorldTransform(hDC. & xm); DWORD error = GetLastErrorO; Матрица xm определяет преобразование x' = x + z/ + 3hz/' = 2x + 2z/ + 4, поэтому у' - 2x' + 1. Получается, что все точки мировой системы координат отображаются в одну линию у = 2х + 1 страничной системы координат. Такое преобразование не является обратимым и не обеспечивает соответствия 1:1, поэтому оно относится к числу недопустимых. В данном примере SetWorldTransform, как и следовало ожидать, вернет FALSE, но как ни странно, GetLastError возвращает О (ERROR_SUCCESS) даже в Windows NT/2000. Впрочем, GDI вообще плохо справляется с объяснением причин ошибок. Вызов SetWorldTransform также завершается неудачей, когда контекст устройства находится в графическом режиме GM_SETCOMPATIBLE или программа работает в Windows 95/98. При вызове ModifyWorldTransform параметр iMode принимает одно из трех значений. Когда параметр iMode равен MTW_IDENTITY, преобразование преобразуется к тождественной форме. Если параметр iMode равен MTW_LEFTMULTIPLY, текущее преобразование умножается на *lpXform слева. Наконец, если параметр iMode равен MTW_RIGHTMULTIPLY, текущее преобразование умножается на *lpXform справа. В данном случае под умножением понимается последовательное выполнение двух преобразований, образующее новое преобразование. GDI различает левостороннее и правостороннее умножение, поскольку умножение преобразований не всегда коммутативно. В отличие от целочисленной математики, в области преобразований а х Ь не всегда равно b x а. Для получения информации о текущем преобразовании используется функция GetWorldTransform. Собственно, этим исчерпывается вся поддержка удивительного мира преобразований в Win32 API. В остальном вы можете полагаться на свой старый испытанный учебник по аналитической геометрии, книгу по теории компьютерной графики — или просто читать дальше. Использование мировых преобразований Мировые координаты й мировые преобразования благодаря двумерным аффинным преобразованиям приносят огромную пользу в компьютерной графике.
364 Глава 6. Системы координат и преобразования К сожалению, их поддержка в Win32 API очень ограничена. Для решения даже простых практических задач приходится изрядно поломать голову. Например, как повернуть объект относительно произвольной точки (х,у) или рассчитать преобразования, отображающие прямоугольное изображение на грани трехмерного куба? В этом разделе мы создадим класс C++ KAffine, содержащий немало полезных методов. Объявление класса KAffine приведено в листинге 6.2. Листинг 6.2. Объявление класса аффинных преобразований KAffine class KAffine { public: XFORM m_xm; KAffineО { ResetO: } void ResetO; BOOL SetTransform(const XFORM & xm); BOOL Combine(const XFORM & b); BOOL Invert(void); BOOL TranslateCFLOAT dx. FLOAT dy); BOOL Scale(FLOAT sx, FLOAT dy); BOOL Rotate(FLOAT angle. FLOAT x0=0, FLOAT y0=0); BOOL MapTri(FLOAT pxO. FLOAT pyO. FLOAT qxO. FLOAT qyO. FLOAT rxQ. FLOAT ryO); BOOL MapTri(FLOAT pxO. FLOAT pyO. FLOAT qxO. FLOAT qyO. FLOAT rxO. FLOAT ryO, FLOAT pxl, FLOAT pyl. FLOAT qxl, FLOAT qyl, FLOAT rxl. FLOAT ryl): }: Класс предназначен для работы с аффинным преобразованием, представленным структурой Win32 XFORM, которая хранится в единственной переменной класса m_xm. Первые три метода повторяют функциональность Win32 API: Reset сбрасывает матрицу в тождественное состояние, SetTransform копирует данные из структуры XFORM, a Combine объединяет последовательные преобразования. Метод KAffine:: Invert заменяет текущее преобразование обратным. Три метода, следующих после него, на самом деле являются операторами PostScript. Метод Translate дополняет текущее преобразование операцией смещения, метод Scale изменяет масштаб по двум осям, а метод Rotate добавляет к текущему преобразованию поворот вокруг произвольной точки (хО,уО). Можно считать, что каждый из этих трех методов выполняет простое преобразование (смещение, масштабирование или поворот) и объединяет его с текущим преобразованием. Два метода MapTri предназначены для решения общей задачи — поиска аффинного преобразования, которое отображает три вершины "неколлинеарных векторов рО, qO, rO в другой неколлинеарный триплет р1, q1 и г1. Делается это в два
Мировая система координат 365 этапа. Сначала необходимо найти два преобразования, отображающих точки (0,0), (1,0) и (0,1) в два триплета. Эта задача гораздо проще исходной, и она решается первым методом MapTri. Затем первое преобразование обращается и объединяется со вторым; результат представляет собой итоговое преобразование. Происходящее можно представить себе как отображение рО, qOnrOB (0,0), (1,0) и (0,1) с последующим отображением в р1} q1} r1. Реализация класса KAffine с проверкой ошибок приведена в листинге 6.3. Листинг 6.3. Реализация класса аффинных преобразований KAffine #define STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <math.h> #include "Affine.h" // Переход к тождественному преобразованию void KAffine: .-ResetО { m_xm.eMll = 1; m_xm.eM12 = 0; m_xm.eM21 = 0; m_xm.eM22 = 1; m_xm.eDx = 0; m_xm.eDy = 0; } // Если преобразование соответствует критерию проверки. // скопировать его B00L KAffine::SetTransform(const XF0RM & xm) { if ( xm.eMll * xm.eM22 «- xm.eM12 * xm.eM21 ) return FALSE; m_xm = xm: return TRUE: } // преобразование = преобразование * b BOOL KAffine::Combine(const XF0RM & b) { if ( b.eMll * b.eM22 — b.eM12 * b.eM21 ) return FALSE: XF0RM a - m_xm: // 11 12 11 12 // 21 22 21 22 m_xm.eMll - a.eMll * b.eMll + a.eM12 * b.eM21; m xm.eM12 = a.eMll * b.eM12 + a.eM12 * b.eM22:
366 Глава 6. Системы координат и преобразования Листинг 6.3. Продолжение m xm.eM21 - а.еМ21 * b.eMll + a.eM22 * Ь.еМ21: nfxm.eM22 - а.еМ21 * Ь.еМ12 + а.еМ22 * Ь.еМ22: mjcm.eDx - a.eDx * b.eMll + a.eDy * b.eM21 + b.eDx; nfxm.eDy - a.eDx * b.eM12 + a.eDy * b.eM22 + b.eDy; return TRUE; // преобразование - 1 / преобразование // M = A * x + В // Inv(M) - Inv(A) * x - Inv(A) * В BOOL KAffine::Invert(void) { FLOAT det = m_xm.eMll * m_xm.eM22 - m_xm.eM21 * m_xm.eM12; if ( det==0 ) return FALSE; XFORM old - m_xm; m_xm.eMll = old.eM22 / det m_xm.eM12 = - old.eM12 / det m_xm.eM21 = - old.eM21 / det m_xm.eM22 - old.eMll / det m_xm.eDx = - ( m_xm.eMll * old.eDx + m_xm.eM21 * old.eDy ): m_xm.eDy = - ( m_xm.eM12 * old.eDx + m_xm.eM22 * old.eDy ): return TRUE; BOOL KAffine::Translate(FLOAT dx. FLOAT dy) { m_xm.eDx +- dx; m__xm.eDy += dy: return TRUE; BOOL KAffine::Seale(FLOAT sx. FLOAT sy) { if ( (sx«-0) || (sy—0) ) return FALSE: m_xm.eMll *= sx; m_xm.eM12 *= sx; m_xm.eM21 *= sy; m_xm.eM22 *= sy; m_xm.eDx *= sx; m__xm.eDy *= sy; return TRUE;
Мировая система координат 367 BOOL KAffine -Rotate(FLOAT angle. FLOAT xO. FLOAT yO) { XFORM xm; Translate(-xO. -yO): // Перенести начало координат в (хО.уО) double rad - angle * (3.14159265359 / 180); xm.eMll - (FLOAT) cos(rad); xm.eM12 - (FLOAT) sin(rad); xm.eM21 - - xm.eM12; xm.eM22 - xm.eMll; xm.eDx =0; xm.eDy - 0; Combine(xm); // Повернуть Translate(x0. yO): // Вернуть начало координат на место return TRUE; } // Найти преобразование, которое отображает (0.0) (1.0) (0.1) // соответственно в p. q, г BOOL KAffine::MapTri(FLOAT pxO. FLOAT pyO. FLOAT qxO. FLOAT qyO. FLOAT rxO. FLOAT ryO) { // pxO - dx. qxO - mil + dx. rxO - m21 + dx // pyO = dy. qyO - ml2 + dy. ryO = m22 + dy m_xm.eMll - qxO - pxO; m_xm.eM12 e qyO - pyO; m_xm.eM21 - rxO - pxO; m_xm.eM22 - ryO - pyO; m_xm.eDx - pxO: m_xm.eDy = pyO; return m_xm.eMll * m_xm.eM22 != m_xm.eM12 * m_xm.eM21; } // Найти преобразование, которое отображает рО. qO. rO II соответственно в pi. ql. rl BOOL KAffine;:MapTri(FLOAT pxO. FLOAT pyO. FLOAT qxO. FLOAT qyO. FLOAT rxO. FLOAT ryO. FLOAT pxl. FLOAT pyl. FLOAT qxl. FLOAT qyl. FLOAT rxl. FLOAT ryl) { if ( ! MapTri(pxO. pyO. qxO. qyO. rxO. ryO) ) return FALSE; InvertO: // Преобразование рО. qO. гО в (0.0).(1.0).(0.1) KAffine mapl; if (! mapl.MapTri(pxl. pyl. qxl. qyl. rxl. ryl) ) return FALSE; return Combine(mapl.m_xm); // Затем в to pl.rl.ql }
368 Глава 6. Системы координат и преобразования Аффинные преобразования широко используются и в других графических системах (например, в PostScript). Базовый класс KAffine существенно упрощает преобразование эффектных примеров PostScript на язык GDI. Простой пример иллюстрирует рис. 6.7 — при выводе пунктирных линий с постоянной модификацией преобразования на экране возникает интересный рисунок. void Transform_DottedLine(HDC hDC. int width, int height) { KAffine af; // Декартова система координат с началом в центре SetMapMode(hDC. MM_ANISOTROPIC): SetViewportExtExChDC. 1. -1. NULL); SetViewportOrgExChDC. width/2, height/2. NULL); SetGraphicsMode(hDC. GM_ADVANCED); for (int i=0; i<=72*5; i++) { // Пунктирная линия (50.0) -> (248.0) for (int x=0; x<=200: x+=3) SetPixeKhDC. x+50. 0. 0): af.Translated, 5); af.ScaleC(FLOAT) 0.98. (FLOAT) 0.98); af.Rotate(5); SetWorldTransform(hDC. & af.m xm); // Смещение // Уменьшение // Поворот на 5 градусов '<^ШК^^ zm \ У, Щ W Рис. 6.7. Пунктирная линия в мировых преобразованиях Не правда ли, вызовы Translate, Scale и Rotate напоминают запись PostScript «5 pixel 5 pixel translate 0.98 0.98 scale 5 rotate»?
Мировая система координат 369 Рассмотрим более интересный пример — вывод трех прямоугольников, состоящих из пикселов разного цвета, и их отображение на три грани куба с использованием параллельной проекции (рис. 6.8). Решение приведено ниже. Рис. 6.8. Построение цветного куба путем преобразований void FaceCHDC hDC. COLORREF color) { for (int x=0; x<size; x++) for (int y=0; y<size; y++) SetPixeKhDC. x. y. color); void Draw_Cube(HDC hDC. int width, int height, int degree) { KAffine af; SetMapMode(hDC. MM_ANIS0TR0PIC); SetViewportExtExChDC. 1. -1. NULL); SetViewportOrgEx(hDC. width/2, height/2. NULL); SetGraphicsMode(hDC. GM_ADVANCED); const FLOAT dx = 100. dy = 30. h - 120; af.MapTri(0. 0. 100. 0. 0. 100. 0. 0. dx. dy. 0. h); SetWorldTransform(hDC. & af.m_xm); FaceChDC. RGB(0xFF. 0. 0)); af.MapTri(0. 0. 100. 0. 0. 100. 0. 0. -dx. dy. 0. h): SetWorldTransform(hDC. & af.m_xm); Face(hDC. RGB(0. OxFF. 0)); af.MapTri(0. 0. 100. 0. 0. 100. 0. h. dx. h + dy. -dx. h + dy); SetWorldTransformChDC. & af.m_xm); FaceChDC. RGB(0. 0. OxFF));
370 Глава 6. Системы координат и преобразования Функция Face выводит прямоугольный массив пикселов в мировой системе координат, не беспокоясь о том, где в итоге окажется выводимое изображение. Основная функция DrawCube при помощи метода KAffine: :MapTri отображает прямоугольную область, выведенную функцией Face, на три поверхности куба в страничной системе координат. В страничной системе координат, как и в декартовой системе, ось у направлена вверх. Благодаря мировым преобразованиям один и тот же фрагмент кода можно использовать для вывода в разных местах и с разными пропорциями — остается лишь рассчитать правильное преобразование. Кстати, при рисовании на гранях куба ваши возможности отнюдь не ограничиваются выводом пикселов. Здесь приведен лишь простейший вариант, но вы можете самостоятельно реализовать вывод линий, эллипсов, растров и даже текста. Использование систем координат Мы довольно подробно рассмотрели четыре системы координат, поддерживаемых графической системой Windows, — мировую, страничную, систему координат устройства и физическую. Мировое преобразование отображает мировое координатное пространство в физическое. Отображение окна в область просмотра выполняется из страничных координат в координаты устройства. Пространство координат устройства представляет собой обычный прямоугольный регион в физическом координатном пространстве, поэтому отображение между ними сводится к простому смещению. В терминологии Win32 мировая и страничная системы координат называются «логическими». Отображения между четырьмя системами координат независимы друг от друга, то есть при изменении одного отображения остальные не изменяются. Однако в совокупности они обеспечивают отображение геометрической модели приложения в участки поверхности физического устройства. При помощи функций GDI приложение определяет мировое преобразование и параметры отображения окна в область просмотра. Отображение из системы координат устройства и физической системы координат находится под управлением операционной системы. По умолчанию мировые преобразования не используются, поскольку контекст работает в режиме совместимости с Win 16 GDI. Чтобы включить поддержку мировых преобразований, приложение должно переключиться в расширенный графический режим GM_ADVANCED. По умолчанию для отображения мировых координат в страничные используется тождественное преобразование. Впрочем, отображение страничных координат в координаты устройства по умолчанию также является тождественным — выбирается режим ММТЕХТ с базовыми точками окна и области просмотра (0,0). Приложение получает информацию об отображениях между системами координат при помощи специальных функций. Функция GetWorldTransformation возвращает описание отображения из мировых координат в страничные. Функции GetWindowOrgEx, GetWindowExtEx, GetViewportOrgEx и GetViewportExtEx возвращают сведения об отображении из страничных координат в координаты устройства. Функ-
Использование систем координат 371 ция GetDCOrgEx возвращает описание смещения между координатами устройства и физическими координатами. Как правило, отображение из логической системы координат верхнего уровня в физические координаты выполняется графической системой. Однако приложение время от времени может получать данные из разных координатных систем, и тогда преобразования приходится выполнять вручную. В Win32 существует несколько функций для решения этой задачи: BOOL LPtoDPCHDC hDC. LPPOINT lpPoints. int nCount): BOOL DPtoLP(HDC hDC. LPPOINT lpPoints, int nCount); BOOL ClientToScreen(HWND hWnd, LPPOINT IpPoint); BOOL ScreenToCllent(HWND hWnd. LPPOINT IpPoint); Функция LPtoDP отображает массив структур POINT из мировых координат в координаты устройства; функция DPtoLP осуществляет обратное преобразование. Отображение определяется параметрами контекста устройства: графическим режимом, мировым преобразованием, режимом отображения и настройкой отображения окна в область просмотра. Функции работают не с отдельными точками, а с целым массивом структур POINT. Следовательно, при вызове им можно передать структуру RECT или даже массив структур RECT, поскольку раскладка структуры RECT в памяти в точности совпадает с раскладкой двух структур POINT (координаты левого верхнего и нижнего правого угла). Код приведенного ниже примера отображает два угла прямоугольника клиентской области в мировое пространство: RECT rect; GetClientRect(WindowFromDCChDC). & rect); DPtoLP(hDC, (POINT *) & rect. sizeof(RECT)/sizeof(POINT)); В некоторых ситуациях приложение берет на себя управление преобразованием логических координат в координаты устройства вместо того, чтобы использовать функции LPtoDP и DPtoLP. Причины могут быть разными — например, приложение хочет выполнить отображение без контекста устройства, считает затраты на использование функций GDI неприемлемыми или желает прибегнуть к аппаратной поддержке вещественных вычислений с ее повышенным быстродействием. Средства GDI не позволяют легко получить объединенное преобразование из мировых координат в координаты устройства. Ниже приведен новый метод класса KAffine для расчета объединенного преобразования. // Расчет объединенного преобразования из мировых координат // в координаты устройства BOOL KAffine::GetDPtoLP(HDC hDC) { if ( ! GetWorldTransform(hDC. & m_xm) ) return FALSE; POINT origin; GetWindowOrgEx(hDC. & origin); Translate( - (FLOAT) origin.x. - (FLOAT) origin.y); SIZE sizew. sizev; GetWindowExtEx (hDC. & sizew); GetViewportExtEx(hDC, & sizev);
372 Глава 6. Системы координат и преобразования Scale( (FLOAT) sizew.cx/sizev.cx, (FLOAT) sizew.cy/sizev.cy); GetViewportOrgEx(hDC. & origin); Translate( (FLOAT) origin.x, (FLOAT) origin.y); return TRUE; } Как видно из приведенного фрагмента, отображение из логического пространства координат в пространство устройства представляет собой мировое преобразование, за которым следует смещение к базовой точке окна, масштабирование в соответствии с изменениями габаритов и еще одно смещение к базовой точке области просмотра. Отображение из пространства координат устройства в пространство мировых координат представляет собой преобразование, обратное по отношению к предыдущему. Обычно контекст устройства соответствует клиентской области окна. В этом случае для преобразования из координат устройства (клиентской области) в физические (экранные) координаты можно воспользоваться функцией ClientToScreen, тогда как обратное преобразование выполняется функцией ScreenToClient. Впрочем, вы также можете самостоятельно выполнить сложение или вычитание, используя данные базовой точки контекста. Реализация преобразований в GDI Инженер всегда следит за тем, чтобы система делала то, что требуется, с приемлемым быстродействием. Хорошее понимание реализации помогает обрести уверенность, а в сомнительных случаях — разработать альтернативное решение. В этом разделе мы поговорим о том, что нам известно о реализации мировых преобразований и отображения «окно/область просмотра» в Windows NT/2000. Графический механизм Windows NT/2000 работает в адресном пространстве ядра. На старых процессорах операции с плавающей точкой считались ненадежными или слишком медленными, поэтому графический механизм имитирует вещественные вычисления. Каждое вещественное число преобразуется в структуру FL0AT0BJ, состоящую из 32-разрядной экспоненты и 32-разрядной знаковой мантиссы. Структура XF0RM представляется структурой MATRIX, состоящей из шести полей типа FL0AT0BJ для eMU, eM12, eM21, eM22, eDx и eDy, двух целочисленных полей для целых частей eDx и eDy, а также флага. В контексте устройства хранятся три структуры MATRIX. Первая структура определяет преобразование мировых координат в страничные; конечно, именно с этой структурой работают функции API SetWorldTransform и GetWorldTransform. Две другие структуры определяют отображение мировых координат в координаты устройства, и наоборот. Вы можете не сомневаться в том, что функции LPtoDP и DPtoLP проходят только через одну матрицу преобразования. В контексте устройства хранятся флаги следующего вида: WORLD_TO_PAGE_IDENTITY PAGE_TO_DEVICE_IDENTITY PAGE_TO_DEVICE_SCALE_IDENTITY XFORMJJNITY XFORM NO TRANSLATION
Пример программы: прокрутка и масштабирование 373 Имена этих флагов позволяют предположить, что в простых случаях преобразование полностью или частично пропускается. Для отображения окна в область просмотра базовая точка и габариты окна/ области просмотра хранятся в контексте устройства в исходной целочисленной форме. В расширенном графическом режиме они наверняка включаются в преобразования из мировых координат в координаты устройства, и наоборот. Матрицы мирового преобразования и отображения окна в область просмотра хранятся в пользовательском адресном пространстве, что ускоряет обращения к ним. Простые случаи отображения (в частности, LPtoDP и DPtoLP) обрабатываются в gdJ32.dll в пользовательском режиме, а в более сложных случаях генерируются вызовы системных функций, обрабатываемые графическим механизмом в режиме ядра. Также следует помнить о том, что графический механизм Windows NT/2000 для повышения точности использует для хранения координат устройства и физических координат числа с фиксированной точкой в формате 28.4. За дополнительной информацией о внутренней структуре данных контекста устройства обращайтесь к главе 3. Пример программы: прокрутка и масштабирование Все программы, которые мы создавали до настоящего момента, выводили данные во всей клиентской области окна, причем клиентской областью весь вывод и ограничивался. Такой подход не годится даже для простых задач типа редактирования текста, не говоря уже о компьютерной верстке или систем автоматизированного проектирования. Все эти программы обычно выводят данные на воображаемом «холсте», размеры которого значительно превышают размеры клиентской области окна. В любой момент времени в окне отображается лишь часть «холста». При помощи полос прокрутки пользователь выбирает относительную позицию текущего экрана в «холсте». Помимо прокрутки, в профессиональных приложениях предусматривается возможность масштабирования, чтобы пользователь мог оценить общий вид всего «холста» или рассмотреть мельчайшие подробности. В этом разделе мы создадим класс KScroll Canvas, производный от класса KCanvas из главы 5. Класс KScroll Canvas позволяет определять размер «холста» на уровне приложения, полосы прокрутки и масштабирование. В основе реализации класса лежит преобразование страничных координат в координаты устройства. Кроме того, мы рассмотрим простой пример программы, созданной на базе нового класса. Объявление класса KScroll Canvas приведено в листинге 6.4. Листинг 6.4. Объявление класса KScrollCanvas (поддержка прокрутки и масштабирования) class KScrollCanvas : public KCanvas { virtual LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam):
374 Глава 6. Системы координат и преобразования Листинг 6.4. Продолжение public: int m_width. mjieight: int mjinedx. mjinedy; i nt m_zoommul. m_zoomdi v; virtual void OnZoom(int x, int y. int mul. int div); virtual void OnTimer(WPARAM wParam. LPARAM IParam); virtual void OnMouseMove(WPARAM wParam. LPARAM IParam); virtual void OnCreate(void); virtual void OnDestroy(void); KScrollCanvas(void) { m_width - 0; m_height = 0; mjinedx - 0: mjinedy = 0; m_zoommul = 1; m_zoomdiv = 1; } void SetSize(int width, int height, int linedx. int linedy) { m_width = width; m_height - height; mjinedx = linedx; mjinedy - linedy; } void SetScrollBar(int side, int maxsize, int pagesize); void OnScrolKint nBar. int nScrollCode. int nPos); }: Класс KScrollCanvas объявлен производным от KCanvas. Он переопределяет функцию WndProc, чтобы обрабатывать дополнительные сообщения — об этом наглядно свидетельствует появление новых виртуальных функций, обрабатывающих сообщения создания окна, уничтожения окна, перемещения мыши, прокрутки и таймера. В новых переменных класса m_width и m_height хранятся размеры холста; переменные mjinedx и mjinedy управляют величиной прокрутки, а переменные m_zoomdiv и m_zoommul определяют дробный масштабный коэффициент. Реализация класса частично приведена в листинге 6.5. Метод OnZoom реализует простой, но эффективный способ масштабирования при помощи мыши. Если щелкнуть левой кнопкой мыши в некоторой точке окна, изображение увеличивается, а точка по возможности перемещается в центр экрана. Щелчок правой кнопкой уменьшает изображение. Функция масштабирования написана по возможности более обобщенной. При вызове ей передаются четыре параметра — новые координаты центра, а также числитель и знаменатель дроби, определяющей коэффициент масштабирования. Функция обновляет масштаб, прибавляет текущую позицию полос прокрутки к соответствующим координатам, масштабирует и вычисляет новую позицию. Затем происходит обновление размера холста и полос прокрутки. Программа вычисляет новую позицию полос прокрутки и перемещает точку щелчка в центр, если это возможно. В конце своей работы функция выдает запрос на полную перерисовку.
Пример программы: прокрутка и масштабирование 375 Листинг 6.5. Реализация KScrollCanvas: масштабирование и обработка сообщений void KScrollCanvas::0nZoom(int x. int y. int mul. int div) { m_zoommul *= mul; m__zoomdiv *= div; int factor - gcd(m_zoommul. m_zoomdiv); m_zoommul /= factor; m_zoomdiv /= factor: // Прибавить смещение полос прокрутки и вычислить новую позицию // после масштабирования х = ( х + GetScrolIPos(m_hWnd. SBJiORZ) ) * mul / div: у - ( у + GetScrol 1 Pos(mJiWnd, SBJ/ERT) ) * mul / div: // Обновить параметры холста m_width = m_width * mul /div: mjieight - mjieight * mul /div: RECT rect; GetClientRectCmJiWnd. & rect): // Изменить состояние полос прокрутки SetScrol1Bar(SB_HORZ, m_width, rect.right): SetScrol1 Bar(SBJ/ERT, mjieight, rect.bottom); // Постараться расположить х в центре окна х -= rect.right/2; if ( x<0 ) x - 0; if ( x > m_width - rect.right ) x = m_width - rect.right; SetSc roll Pos (mJiWnd. SBJORZ. x. FALSE); у -= rect.bottom/2; if ( y<0) у - 0; if ( у > mjieight - rect.bottom ) у = mjieight - rect.bottom; SetScrol 1 Pos(m_hWnd. SBJ/ERT. y. FALSE); // Перерисовать InvalidateRect(m_hWnd. NULL. TRUE); ::UpdateWindow(m_hWnd); } LRESULT KScrollCanvas::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam) { switch( uMsg ) {
376 Глава 6. Системы координат и преобразования Листинг 6.5. Продолжение case WM_CREATE: m_hWnd = hWnd; OnCreateO: return 0; case WM_SIZE: SetScro11Bar(SB_H0RZ. m_width. LOWORD(lParam)); SetScrollBar(SB_VERT. m_height. HIWORD(lParam)); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = BeginPaint(m_hWnd, &ps); SetWindowOrgEx(hDC. 0. 0. NULL): SetViewportOrgEx(hDC- GetScrollPos(hWnd. SB_H0RZ). - GetScrollPos(hWnd, SBJERT). NULL); OnDraw(hDC. & ps.rcPaint); EndPaint(m_hWnd. &ps); } return 0; case WM_RBUTT0ND0WN: OnZoom( LOWORD(lParam). HIWORD(lParam). 1. 2); return 0; case WM_LBUTTONDOWN: OnZoom( LOWORD(lParam). HIWORD(lParam). 2. 1); return 0; case WM_HSCROLL: OnScroll(SB_HORZ. LOWORD(wParam). HIWORD(wParam)); return 0; case WMJSCROLL: OnScroll (SBJERT. LOWORD(wParam). HIWORD(wParam)); return 0; case WMJIMER: OnTimer(wParam. IParam); return 0; case WM_MOUSEMOVE: OnMouseMove(wParam. IParam); return 0; case WM_DESTROY: OnDestroyO; return 0;
Пример программы: прокрутка и масштабирование 377 default: return KCanvas::WndProc(hWnd. uMsg. wParam, IParam); } } Функция KScrollWindow::WndProc управляет обработкой сообщений окном. Для некоторых сообщений обработка сводится к вызову виртуальной функции, что позволяет производным классам обработать эти сообщения без переопределения функции WndProc. Для сообщения WMSIZE полосы прокрутки обновляются в соответствии с изменениями в размерах окна. Для сообщения WMPAINT программа читает позиции полос прокрутки и использует полученные значения для перемещения базовой точки области просмотра в позицию за левым верхним углом экрана. Обратите внимание: когда позиции полос прокрутки отличны от нуля, часть виртуального холста находится за пределами экрана слева и сверху. Исходя из этого, мы перемещаем базовую точку области просмотра во внеэкранную позицию, заданную координатами (-позиция вертикальной полосы прокрутки, -позиция горизонтальной полосы прокрутки). После правильного выбора методу OnDraw уже не придется беспокоиться о прокрутке. При обработке сообщения WM_LBUTT0ND0WN изображение в окне увеличивается в точке щелчка, а при обработке WMRBUTT0ND0WN изображение уменьшается. Полоса прокрутки генерирует сообщения WM_VSCROLL и WMHSCROLL, обрабатываемые одной функцией OnScroll. Эта функция (отсутствующая в приведенном листинге) вычисляет новую позицию прокрутки в соответствии с кодом, переданным в LOWORD(wParam), нормализует ее в допустимом интервале, обновляет позицию бегунка и затем прокручивает окно функцией Scroll Window. Функция Win32 Scroll Window прокручивает содержимое окна сдвигом экранного буфера. Открывшаяся непрорисованная часть добавляется в обновляемый регион окна и перерисовывается при последующей обработке WMPAINT. Игра го в классе KScrollCanvas Использование класса KScrollCanvas будет продемонстрировано на примере простой программы, которая рисует доску для игры го1. Кстати, го — японское название интереснейшей и очень древней китайской игры «вэйчи» (в буквальном переводе — «игра в окружение»). В листинге 6.6 приведено объявление класса KWeiQi Board. Производный класс реализует виртуальные методы OnDraw и OnCommand, обеспечивающие специализированный вывод и обработку команд меню. Листинг 6.6. Объявление класса KWeiQiBoard — вариант использования класса KScrollCanvas class KWeiQiBoard : public KScrollCanvas { virtual void OnDraw(HDC hDC, const RECT * rcPaint); virtual BOOL OnCommand(WPARAM wParam. LPARAM IParam); int m_grids ; 1 Cm. http://www.go.sp.ru. — Примеч. перев.
378 Глава 6. Системы координат и преобразования Листинг 6.6. Продолжение int mjjnitsize ; char * m_stones; int margin(void) const; // Поля с четырех краев холста int pos(int n) const; // Позиция n-ro пересечения public; KWeiQiBoardO { m_grids =19; // Доска 19 x 19 mjjnitsize = 20; // 20 пикселов между пересечениями m_stones = "B20212223241404W3031323334251505"; // Пример SetSize(pos(m_grids-l)+margin(), pos(m_grids-l)+margin(). mjjnitsize. mjjnitsize); } }: Результат показан на рис. 6.9. Изображение было увеличено и прокручено к правому нижнему углу доски. fite $utfd, Рис. 6.9. Доска для игры го, выведенная с использованием класса KScrollCanvas Итоги Эта глава посвящена четырем системам координат, поддерживаемым в Win32 API, — мировой, страничной, системе координат устройства и физической системе координат. Мы рассмотрели мировые преобразования, отображающие ко-
Итоги 379 ординаты из мировой системы координат в страничную; различные режимы отображения, управляющие преобразованием координат из страничной системы в систему координат устройства; кроме того, было описано отношение между системой координат устройства и физической системой координат. Глава завершается подробным описанием аффинных преобразований, их свойств и некоторых операций, включая композицию и обращение. Изложенный материал иллюстрирует класс KScrollView, поддерживающий масштабирование и прокрутку. Глава завершается простым примером использования этого класса. Примеры программ В этой главе рассматриваются всего два примера программ (табл. 6.3). Таблица 6.3. Программы главы б Каталог проекта Описание Samples\ChaptJ)6\CoordinateSpace Исследование систем координат и аффинных преобразований Samples\ChaptJ)6\WeiQi Тестовая программа для класса KScrollView с поддержкой масштабирования и прокрутки
Глава 7 Пикселы Из всех операций с контекстами устройств в Win32 GDI самая трудная — это вывод отдельного пиксела. Конечно, речь идет не о простом изменении цвета пиксела, а скорее о полном понимании всех факторов, влияющих на процесс вывода. При условии такого понимания вывод линий, кривых, фигур, растров и текста уже не вызовет особых затруднений. Глава 6 была посвящена системам координат, режимам отображения и преобразованиям; в этой главе рассматриваются объекты GDI, манипуляторы, отсечение, цвет и, наконец — вывод отдельного пиксела. Объекты GDI, манипуляторы и таблица объектов В Win32 API используются десятки всевозможных объектов (файловые объекты, объекты синхронизации, объекты GDI и т. д.), хотя интерфейс API не основан ни на одном из современных объектно-ориентированных языков программирования. Windows 1.0 API был выпущен в 1985 году, когда на C++ писали всего 500 программистов — это было задолго до стремительного роста популярности объектно-ориентированных языков в 1990-х годах. Объектом в объектно- ориентированном языке называется экземпляр класса, члены которого (переменные и функции класса) определяют данные объекта и его поведение. Новые объекты инициализируются специальными функциями класса, которые называются конструкторами. Для уничтожения объектов используются другие специальные функции, называемые деструкторами. Таким образом, с объектом в объектно-ориентированном языке связывается некоторая совокупность данных и функций, выполняющих некоторые операции. В определении класса указано, какие из переменных и функций являются открытыми, защищенными или закрытыми. Тем самым достигается одна из важных целей — маскировка реализации.
Объекты GDI, манипуляторы и таблица объектов 381 Хотя Win32 API и не является объектно-ориентированным, этот интерфейс по-своему пытается решить те же проблемы, для решения которых создавались объектно-ориентированные языки — а именно маскировку реализации и абстрактные типы данных. Win32 API маскирует свои объекты в большей степени, чем объектно-ориентированные языки. Работая с объектами Win32 API, прикладная программа не знает их размера или местонахождения в памяти. В Win32 API поддерживаются специальные функции для создания объектов разных типов, отличающиеся от конструкторов языка C++. Объекты C++ создаются за два этапа: сначала для объекта выделяется память, а затем объект инициализируется конструктором. Следовательно, приложение, использующее объект, должно точно знать, сколько памяти занимает объект и где он находится в памяти. Функции Win32 для создания объектов Win32 скрывают и размер, и адрес объекта. Более того, приложение даже не получает указателя на объект; ему лишь предоставляется манипулятор объекта. Манипуляторы Win32 API представляют собой числа, однозначно идентифицирующие oбъeктыWin32, причем способ идентификации известен только операционной системе. Концепция абстрактных типов данных представлена в C++ абстрактными классами, конкретными классами и виртуальными функциями. Абстрактный класс определяет общее поведение класса при помощи виртуальных функций. Один и тот же абстрактный класс может быть по-разному реализован несколькими конкретными классами, каждый из которых предоставляет свою реализацию виртуальных функций. Объекты конкретных классов могут интерпретироваться как объекты абстрактного класса, у которых вызовы функций осуществляются через таблицу виртуальных функций. При внимательном рассмотрении оказывается, что в Win32 используется немало абстрактных типов объектов. Например, тип файлового объекта является абстрактным типом, на базе которого создаются различные типы конкретных объектов. Функция CreateFile может использоваться для создания файлов, каналов, почтовых слотов, коммуникационных портов, консолей, каталогов и устройств, причем во всех случаях возвращается один и тот же тип манипулятора. Трассировка функции WriteFile показывает, что завершающая операция выполняется разными фрагментами, относящимися к разным частям операционной системы и даже к драйверам устройств, что очень похоже на использование виртуальных функций в C++. В области GDI контекст устройства можно рассматривать как абстрактный тип объекта. При создании или получении контекста устройства для принтера или экрана, совместимого или метафайлового контекста вы всегда получаете один и тот же тип манипулятора контекста. Конечно, общие графические вызовы по конкретному манипулятору обрабатываются разными функциями, предоставленными GDI или драйверами графических устройств через таблицу указателей на функции в структуре физического устройства. Короче, существуют достаточно веские причины для того, чтобы называть Win32 API «объектно-базированным». А теперь давайте посмотрим, как в Windows NT/2000 организовано хранение объектов GDI и как устанавливается соответствие между манипуляторами и объектами. Заодно вы познакомитесь с конкретным примером реализации Win32 GDI — конечно же, далеко не единственной из возможных.
382 Глава 7. Пикселы Хранение объектов GDI Объекты C++ обычно хранятся в стеке, в куче или в другом месте, определяемом перегруженным оператором new. Как правило, этим местом является адресное пространство пользовательского режима, если только речь не идет о драйвере устройства режима ядра. Графическая система Windows NT/2000 делится на три части: клиентскую библиотеку DLL пользовательского режима (gdi32.dll), графический механизм режима ядра (win32k.sys) и различные драйверы устройств, также обычно работающие в режиме ядра. Разделение реализации на два режима несколько усложняет архитектуру хранения объектов GDI. Объект GDI, целиком хранящийся в адресном пространстве пользовательского режима, доступен лишь в то время, когда активен создавший его процесс, поскольку каждый процесс обладает собственным адресным пространством пользовательского режима. После переключения процесса адрес, по которому хранится объект, становится недействительным. При таком подходе операции GDI в графическом механизме режима ядра могли бы выполняться лишь в контексте процесса, из которого поступил вызов. Конечно, это затруднило бы процесс быстрого переключения задач, необходимый для современных многозадачных систем. С другой стороны, объект GDI, полностью хранящийся в адресном пространстве режима ядра, доступен только из пространства режима ядра. При выполнении простейших операций (например, присваивании значений атрибутам контекстов устройств) системе пришлось бы вызывать системную функцию для переключения процессора в режим ядра, выполнять несколько строк ассемблерного кода, а затем переключаться обратно в пользовательский режим. В Windows NT/2000 объект GDI обычно хранится в виде двух частей — объекта пользовательского режима и объекта режима ядра. Объект пользовательского режима обеспечивает быстрое выполнение операций, а в объекте режима ядра хранится информация, не зависящая от процесса. Некоторые объекты режима ядра (например, объект контекста устройства) содержат полные копии своих объектов пользовательского режима, синхронизируемые при помощи какого-либо механизма. Для большинства объектов GDI хранит в памяти некоторую структуру данных фиксированного размера. В этом случае использование памяти не вызывает особых проблем. Однако для объектов регионов и аппаратно-зависимых растров объем данных, хранимых GDI, может увеличиться до значительных размеров. Структуры данных GDI режима ядра хранятся в так называемом выгружаемом пуле. В однопользовательской системе архитектуры Windows NT/2000 объем выгружаемого пула ядра не превышает 192 Мбайт. Учитывая, что внутренняя память GDI имеет ограниченный размер и совместно используется всеми приложениями системы, приложения должны умеренно пользоваться ресурсами системы и избегать создания больших регионов и аппаратно-зависимых растров.
Объекты GDI, манипуляторы и таблица объектов 383 Таблица объектов GDI Информация об объектах GDI хранится в системной таблице фиксированного размера, которая называется «таблицей объектов GDI». Здесь необходимо обратить внимание на ряд моментов. Во-первых, таблица объектов GDI имеет фиксированный размер и не расширяется динамически, как можно было бы предположить. Такое решение отличается простотой и эффективностью, но ему присущи некоторые ограничения. В настоящее время для Windows NT/2000 таблица объектов GDI рассчитана на 16 384 манипулятора. Во-вторых, таблица объектов GDI совместно используется всеми процессами и системными DLL. Ваш рабочий стол, браузер, текстовый редактор и игры DirectX — все они соревнуются за общий набор манипуляторов GDI. В Windows 2000 интерфейс DirectX использует отдельную таблицу манипуляторов. Таблица объектов GDI хранится в адресном пространстве ядра, что упрощает доступ к ней со стороны графического механизма. В адресных пространствах всех процессов, использующих GDI, создается копия этой таблицы, доступная только для чтения. ПРИМЕЧАНИЕ На терминальных серверах каждый сеанс обладает собственной копией графического механизма Windows и собственной копией диспетчера окон (win.32k.sys), поэтому в системе может присутствовать несколько таблиц объектов GDI. Элементы таблицы манипуляторов GDI представляют собой 16-байтовые структуры: typedef struct { void * pKernel; unsigned short nPid; unsigned short nCount; unsigned short nUnique; unsigned short nType; void * pUser; } GdiTableEntry; Для каждого объекта GDI в таблице хранится указатель на объект режима ядра, указатель на объект пользовательского режима, идентификатор процесса, некий счетчик, уникальный код и идентификатор типа. Какое изящное, элегантное решение! В поле nPid хранится идентификатор процесса, создавшего объект (значение, возвращаемое при вызове GetCurrentProcessIdO). Каждый объект GDI, созданный пользовательским процессом, помечается идентификатором процесса, что предотвращает использование манипуляторов GDI другими процессами. Получая манипулятор объекта, GDI может легко проверить, был ли этот объект создан текущим процессом. Попытки использования объектов GDI, созданных другими процессами, завершаются неудачей. Исключение составляют стандартные объекты GDI вроде черного пера, белой кисти и системного шрифта, по умолчанию выбираемых в новых контекстах устройств. Было бы слишком расточительно создавать копии стандартных объектов в каждом процессе. Вместо этого
384 Глава 7. Пикселы стандартные объекты создаются в системе всего один раз, и им присваивается нулевой идентификатор процесса. Диспетчер задач сообщает, что нулевой идентификатор соответствует процессу пассивного ожидания (idle). Объекты GDI с нулевыми идентификаторами доступны всем процессам. Поле nCount содержит счетчик выбора (или счетчик ссылок), который предотвращает удаление задействованных объектов или гарантирует, что некоторые объекты могут выбираться в контекстах лишь один раз. К сожалению, использование поля nCount в значительной степени ограничено, и в настоящее время пользовательские приложения должны сами следить за тем, когда объекты можно удалять. Возможно, наибольший интерес представляет поле nUnique. Как говорилось выше, объекты GDI хранятся в одной таблице. После создания, использования и удаления объекта соответствующий элемент таблицы освобождается и позднее выделяется для другого объекта. Допустим, программа сохранила манипулятор первого объекта и решила воспользоваться им; что при этом произойдет? В Windows NT/2000 такая попытка почти наверняка завершится неудачей благодаря полю nUnique. Это поле делится на 8-разрядный счетчик многократного использования и 8-разрядный код типа. Счетчик инициализируется нулем и увеличивается каждый раз, когда в данном элементе таблицы создается новый объект GDI. Существует специальный механизм, обеспечивающий равномерное использование элементов таблицы. Следовательно, манипулятор пройдет проверку уникальности лишь при условии, что количество элементов, созданных в этом элементе таблицы, кратно 256 и типы объектов совпадают. В поле пТуре хранится внутренний признак типа, который преобразуется в тип объекта GDI, возвращаемый функцией GetObjectType. Манипулятор объекта GDI В MSDN манипулятор определяется как переменная, однозначно определяющая объект, или косвенная ссылка на ресурс операционной системы. В наши дни считается, что программист должен довольствоваться подобным абстрактным определением и писать отличные программы. Но при диагностике проблем, связанных с совместимостью 16- и 32-разрядного кода в Windows NT/2000, а также при написании низкоуровневых утилит желательно точно представлять себе смысл каждого бита манипулятора Win32 API. Манипулятор объекта GDI в Windows NT/2000 состоит из двух 16-разрядных компонентов — уникального кода и индекса в таблице объектов GDI. Чтобы получить адрес объекта в таблице, достаточно замаскировать младшие 16 бит манипулятора, умножить их на 16 и прибавить к начальному адресу таблицы объектов GDI. Начальный адрес таблицы объектов GDI хранится в глобальных переменных библиотек win32k.sys и gdi32.dll. Выше уже упоминался уникальный код, обеспечивающий дополнительную защиту. Уникальный код также включается в манипуляторы объектов GDI. Чтобы GDI разрешил обращение к манипулятору, уникальный код манипулятора GDI должен совпадать с кодом, хранящимся в таблице объектов GDI. После получения индекса в таблице объектов GDI выполняется дополнительная про-
Объекты GDI, манипуляторы и таблица объектов 385 верка идентификатора процесса. Это позволяет легко отвергнуть недействительные, устаревшие и принадлежащие внешним процессам манипуляторы. В текущей реализации максимальное количество манипуляторов объектов GDI уровня системы (или уровня сеанса на терминальном сервере Windows) равно 16 384. Это значение легко увеличивается до 65 536, поскольку в манипуляторах GDI из 16 бит, выделенных для индексной части, используется всего 12. Пользовательские приложения не должны полагаться па знание внутреннего формата манипуляторов GDI или структуры таблицы объектов GDI, поскольку они изменялись раньше и могут измениться в будущих версиях. С другой стороны, в Windows NT/2000 для манипуляторов GDI устанавливаются дополнительные ограничения и меры защиты, помогающие в отладке кода. Следите за тем, чтобы манипуляторы GDI не передавались за пределы процесса, и тестируйте свои приложения во всех современных версиях Windows. За дополнительной информацией о внутренних структурах данных GDI обращайтесь к главе 3. API объектов GDI Объекты GDI делятся на несколько категорий. Наиболее распространенными типами объектов GDI являются логические кисти, логические перья, логические шрифты, логические палитры, регионы, аппаратно-зависимые растры, DIB-сек- ции, расширенные метафайлы и контексты устройств. Для каждого типа объектов создаются специальные функции, предназначенные для создания объектов этого типа. При создании объекта интерфейс GDI возвращает приложению манипулятор. При вызове многих функций GDI передается HGDI0BJ — обобщенный тип манипулятора объекта GDI. HGDIOBJ SelectObjectCHDC hDC. HGDIOBJ hgdiobj); BOOL DeleteObjectCHGDIOBJ hObject); DWORD GetObjectType(HGDIOBJ h); int GetObject(HGDIOBJ hgdiobj. int cdBuffer. LPVOID lpvObject); Некоторые типы объектов GDI (кисти, перья, шрифты, палитры, аппаратно-зависимые растры и DIB-секции) могут использоваться в качестве атрибутов контекста устройства. Такие объекты связываются с контекстом устройства при помощи функции SelectObject. При вызове эта функция возвращает манипулятор предыдущего объекта GDI, относящегося к тому же типу. Чтобы восстановить в контексте устройства старый объект, достаточно снова вызвать Select- Object для значения, полученного при предыдущем вызове. Логические палитры выбираются в контекстах устройств специальной функцией SelectPalette. Когда объект GDI становится ненужным, его следует удалить функцией DeleteObject. Перед удалением объекта приложение должно проследить за тем, чтобы он не был выбран ни в одном контексте устройства. Подходы к удалению объектов GDI в Windows 95/98/Ме и Windows NT/2000 несколько различаются. В Windows 95/98/Ме функция DeleteObject не удаляет объекты, выбранные в контекстах устройств, что может привести к утечке объектов. В Windows NT/2000 объект GDI удаляется даже в том случае, если он остается выбранным. Дальнейшие попытки вывода с использованием удаленного объекта GDI приводятся к возникновению ошибок, что упрощает диагностику проблемы программистом.
386 Глава 7. Пикселы Если программа разрабатывалась и нормально работала на компьютере с Windows NT/2000, но отказывается работать в Windows 95/98/Ме, одно из возможных объяснений кроется в некорректном удалении объектов. Приведенный ниже класс KGDIObject представляет собой простую оболочку для выбора, исключения из контекста и удаления объектов GDI. class KGDIObject { HGDIOBJ m_h01d; HDC m_hDC: public: HGDIOBJ m_hObj; KGDIObject(HDC hDC. HGDIOBJ hObj) { m_hDC = hDC: m_hObj - hObj: m_h01d - SelectObject(hDC. hObj): assert(m_hDC): assert(m_hObj); assert(m_h01d): } -KGDIObjectО { HGDIOBJ h = SelectObject(m_hDC. m_h01d): assert(h-=j): DeleteObject(mJiObj): // assert(GetObjectType(m_hObj)==0): } }: Объект KGDIObj содержит три переменные, в которых хранятся манипулятор выбираемого объекта GDI, манипулятор контекста устройства и манипулятор исходного объекта в этом контексте. Конструктор выбирает объект GDI в контексте устройства. Три директивы assert проверяют правильность параметров и успешность выполнения операции. Деструктор класса исключает объект GDI из контекста и удаляет его. Первая директива assert убеждается в том, что исключение прошло нормально. Вторая директива проверяет результат удаления объекта. В приведенном коде она закомментирована, поскольку GDI иногда кэши- рует объекты для ускорения создания объектов того же типа. ПРИМЕЧАНИЕ В MFC оболочка для вызова SelectObject возвращает указатель на экземпляр CGdiObject — класс объекта GDI в MFC. Но если MFC не удается установить соответствие между манипулятором объекта GDI и указателем на объект MFC, то возвращается указатель на временный объект. Если приложение воспользуется временным указателем для исключения неиспользуемого объекта из контекста, может оказаться, что контекст уже был изменен другим вызовом SelectObject, что приведет к утечке объектов GDI.
Объекты GDI, манипуляторы и таблица объектов 387 Простой пример использования класса KGDIObject: void OnDraw(HDC hDC) { KGDIObject blue(hDC. CreateSolidBrush(RGB(O.O.OxFF)); Rectangle(hDC. 0. 0. 100. 100); } При завершении процесса все созданные им объекты GDI автоматически удаляются из таблицы (вероятно, по идентификатору процесса, связанному с каждым объектом GDI). И все же приложение должно само обеспечивать правильное удаление объектов, не рассчитывая на то, что весь мусор после завершения процесса уберет система. Постоянная утечка ресурсов GDI быстро нарушает нормальную работу всей системы. Функция GetObjectType получает манипулятор объекта GDI и возвращает целое число, обозначающее тип объекта GDI. Например, для контекста устройства возвращается константа 0BJ_DC, для объекта логической кисти — константа OBJJJRUSH, для аппаратно-зависимого растра или DIB-секции — константа OBJ BITMAP и т. д. Если функция возвращает 0, значит, переданный ей манипулятор объекта GDI либо недействителен, либо принадлежит другому процессу. Функция GetObject может использоваться для получения информации об исходной структуре, передаваемой при создании некоторых типов объектов GDI. Для аппаратно-зависимого растра она заполняет структуру BITMAP, для DIB-секции — структуру DIBSECTION, для расширенного пера — структуру EXTLOGPEN, для пера — структуру L0GPEN, для кисти — структуру L0GBRUSH, для шрифта — структуру L0GF0NT, а для палитры — структуру WORD. При вызове Gdi Object передается манипулятор объекта GDI, предполагаемый размер структуры и указатель на блок данных, размер которого достаточен для хранения структуры. Если вызывающая сторона не уверена в том, сколько байт потребуется для хранения структуры, она может получить количество байт, вызывая функцию GetObject с указателем NULL. Функция GetObject позволяет отличить аппаратно-зависимый растр от DIB- секции (функция GetObjectType их не различает). Для этого достаточно вызвать GetObject со структурой DIBSECTION; если вызов завершится успешно, значит, объект GDI является DIB-секцией. Все типы объектов GDI будут подробно рассматриваться по мере необходимости. Обнаружение утечки объектов GDI При проявлении симптомов утечки объектов GDI — например, при сбоях во время вывода или нарушениях внешнего вида системных интерфейсных элементов — очень трудно определить, какое приложение вызывает проблему, какие именно ресурсы теряются и где это происходит. Вероятно, существует всего одна достойная программа для обнаружения утечки памяти и ресурсов — BoundsChecker компании Numega. В процессе работы BoundsChecker перехватывает практически все функции Win32 API для последующего сохранения, проверки и анализа параметров и возвращаемых значений. Например, при регистрации всех вызовов функций API, создающих и удаляю-
388 Глава 7. Пикселы щих объекты, программа типа BoundsChecker сравнивает созданные объекты с удаляемыми перед завершением программы и находит все утечки объектов GDI с точной информацией о вызывающей стороне. В главе 4 описаны некоторые утилиты для отслеживания вызовов функций GDI API, системных функций и функций DDL Эти утилиты нетрудно усовершенствовать для поиска утечек ресурсов GDI. Зная внутренние структуры данных GDI, можно без особого труда написать программу, которая следит за использованием объектов GDI всеми процессами Windows NT/2000. Такая программа выведет сведения обо всех ресурсах GDI в системе, хотя она и не располагает информацией, необходимой для поиска утечек. На рис. 7.1 изображено окно программы GDIObj, написанной специально для этой главы. Программа регулярно просматривает содержимое таблицы объектов GDI, сортирует записи по идентификатору процесса и типу, после чего отображает в табличном представлении. fM^fK^A 4S*SJ \т 912 18 212 164 184 1948 2336 224 392 440 480 536 852 944 908 928 lixL Х.?ш$$$ >. unknown 0SA.EXE unknown services.exe unknown winlogon.exe MSDEV.EXE IEXPLORE.EXE lsass.exe svchost.exe spoolsv.exe svchost.exe MSTask.exe Explorer.exe internat.exe tgsched.exe zm32nt.exe JIafot 707 25 5 4 22 14 466 256 4 4 8 4 4 126 15 4 5 Ш 10 3 5 2 3 2 48 25 2 2 2 2 2 20 6 2 2 ,Jl..^e^, 38 0 0 0 4 1 5 8 0 0 1 0 0 23 1 0 0 JjBiJmaf 357 5 0 1 1 4 269 160 1 1 1 1 1 33 5 1 1 ffietfe 175 0 0 0 0 1 5 3 0 0 0 0 0 5 0 0 0 T*w 16 0 0 0 12 5 73 28 0 0 0 0 0 20 0 0 1 J 8iSi 45 17 0 1 2 1 66 32 1 1 4 1 1 25 3 1 1 |ЩЩ ""б™ | 0 0 о • 0 0 о • 0 ' 0 0 0 0 0 J 0 0 0 0 J J"jT Рис. 7.1. Программа наблюдения за объектами GDI Как видно из рисунка, MS Developer Studio использует 466 объектов GDI, 2,8 % от максимального количества. Если число манипуляторов GDI, используемых приложением, продолжает расти, это является верным признаком утечки объектов. Для отслеживаемого процесса GDIObj выводит общее количество объектов GDI и отдельные данные по нескольким распространенным категориям. При возникновении утечки вы будете знать, какие объекты GDI теряются. На рисунке показано, что первый процесс (PID = 0) использует 66 объектов, не относящихся ни к однорИхз категорий. В основном это объекты физических шрифтов, задействованные GDI. Кроме того, из рисунка видно, что процессы
Объекты GDI, манипуляторы и таблица объектов 389 Win32 с графическим интерфейсом используют минимум четыре объекта GDI: два контекста устройства, один растр и одну кисть. Эти объекты могут создаваться в процессе инициализации user32.dll или gdi32.dll. Следовательно, в системе может одновременно работать не более 4096 процессов, поскольку таблица рассчитана всего на 16 384 манипулятора. В Windows 2000 появилась новая функция, при помощи которой приложения получают информацию об использовании ресурсов GDI и USER. DWORD GetGuiResources(HANDLE hProcess. DWORD uiflags): Функция GetGui Resources сообщает, сколько объектов GDI или USER в настоящее время используется приложением. В первом параметре GetGui Resources передается манипулятор интересующего вас процесса. Второй параметр принимает значения GRGDI0BJECTS и GRUSEROBJECTS. Функция возвращает текущее количество используемых объектов. Функция GetResources является «законным» средством для поиска утечки ресурсов. Например, ее можно вызвать до и после выполнения некоторого фрагмента, сравнить возвращаемые значения и узнать, создает ли этот фрагмент новые объекты. Хотя различающиеся значения не всегда свидетельствуют об утечке ресурсов из-за возможного кэширования объектов приложением и GDI, устойчивый рост числа объектов является верным признаком их утечки. Ниже приведен простой класс-оболочка KGUIResource и пример его использования при обработке сообщения WMPAINT: class KGUIResource { int m_gdi. mjjser; public: KGUIResourceO { m_gdi - GetGuiResources(GetCurrentProcess(). GR_GDIOBJECTS); m_user = GetGuiResources(GetCurrentProcess(). GRJJSEROBJECTS): } -KGUIResourceO { int gdi = GetGuiResources(GetCurrentProcess(). GR_GDIOBJECTS); int user = GetGuiResources(GetCurrentProcess(). GRJJSEROBJECTS); if ( (m_gdi==gdi) && (m_user==user) ) return; char temp[64]; wsprintf(temp. "ResourceDifference: gdi(%d->%d) userUd->$d)\n". m_gdi. gdi. m_user. user); OutputDebugString(temp); } }: case WM_PAINT: { KGUIResource res;
390 Глава 7. Пикселы PAINTSTRUCT ps; HDC hDC - BeginPaint(m_hWnd. &ps); OnDraw(hDC. &ps.rcPaint): EndPaint(m_hWnd. &ps); } Отсечение В компьютерной графике отсечением (clipping) называется часть алгоритма графического вывода, обеспечивающая избирательное исключение некоторых частей изображения из процесса вывода. Например, если вы читаете документ в текстовом редакторе при большом увеличении, в окне отображается лишь малая часть страницы, а все остальное отсекается. А если вы в графическом редакторе заключаете рисунок в овальную рамку, отсекается все, что находится за пределами овала. Традиционно отсечение определяется в логическом адресном пространстве. Скажем, при выводе в окне отображаются только те графические примитивы, которые попадают в это окно; все остальное отсекается. Существуют специальные алгоритмы, вычисляющие пересечение графических примитивов с окном, чтобы в вывод включались только неотсеченные участки. Отсечение принадлежит к числу базовых возможностей Windows GDI. Например, приложение может провести линию от начала координат в псевдобесконечную точку 32-разрядного координатного пространства (maxint,maxint). При достижении границы клиентской области окна или контекста устройства вывод немедленно прекращается. Такой подход существенно упрощает проектирование графических алгоритмов. Конвейер отсечения С концептуальной точки зрения каждый графический вызов проходит через несколько уровней отсечения, образующих конвейер отсечения (clipping pipeline). Документация Microsoft на эту тему весьма туманна, а иногда и недостоверна — как, впрочем, и немногочисленные книги, написанные на эту тему. Приведу конкретный пример: «При получении манипулятора контекста устройства функцией BeginPaint DC содержит заранее определенный прямоугольный регион отсечения, который соответствует недействительному прямоугольнику, нуждающемуся в перерисовке». Это описание содержит целых три ошибки. Ниже перечислены известные нам уровни отсечения, поддерживаемые в Microsoft Windows. О Прямоугольник окна. У каждого окна имеется прямоугольная область, которая изначально определяется при вызове CreateWindow/CreateWindowEx и в дальнейшем может изменяться. Все, что находится за пределами прямоугольника окна, отсекается.
Отсечение 391 О Регион окна. Приложение может изменить регион окна и придать окну произвольную форму при помощи функции SetWindowRgn. Все, что находится за пределами региона окна, отсекается. О Видимость. Окна могут перекрывать друг друга; у окна могут быть дочерние или соседние окна. Любая часть окна, закрытая другим окном, отсекается. Области, закрытые дочерними и соседними окнами, также могут отсекаться при указании флагов WS_CLIPCHILDREN и WS_CLIPSIBLINGS. О Клиентская область. Если контекст устройства соответствует клиентской области окна, все, что находится за пределами клиентской области, отсекается. О Обновляемый регион. Win32 поддерживает API для определения обновляемого региона (update region) окна — региона, нуждающегося в обновлении. Описание обновляемого региона можно получить при помощи функции GetUpdate- Region. Для контекста устройства, возвращаемого функцией BeginPaint, все, что находится за пределами обновляемого региона, отсекается. О Системный регион. Комбинированный регион, построенный с учетом перечисленных выше факторов, называется системным регионом (system region). Информацию о системном регионе контекста устройства можно получить при помощи функции GetRandomRgn(hDC, hRgn, SYSRGN). О Метарегиои. Метарегион (meta region) образует первый уровень отсечения в контексте устройства, находящийся под управлением приложения. Все, что находится за пределами метарегиона, отсекается. О Регион отсечения. Регион отсечения (clip region) образует второй уровень отсечения в контексте устройства, находящийся под управлением приложения. Все, что находится за пределами этого региона, отсекается. А теперь давайте перефразируем приведенное выше описание региона отсечения, в котором упоминается функция BeginPaint. При получении манипулятора контекста устройства функцией BeginPaint DC содержит заранее определенный системный регион, который может и не быть прямоугольным; он генерируется на основании обновляемого региона окна с учетом других факторов. Метарегион и регион отсечения всегда начинаются с состояния NULL-регионов; они позволяют приложению управлять процессом отсечения. Системный регион находится под управлением операционной системы и автоматически обновляется при изменении размеров или перемещении окна. Метарегион и регион отсечения находятся под управлением приложения. Пиксел выводится лишь в том случае, если он принадлежит пересечению системного региона, метарегиона и региона отсечения. В каком-то смысле термин «отсечение» в названии «регион отсечения» сбивает с толку, поскольку этот регион определяет остающиеся, а не отсекаемые точки. Отсекается лишь то, что находится за пределами этого региона. Простые регионы Регион представляет собой множество точек в координатном пространстве. Это множество может быть пустым либо состоять из точек, образующих прямоугольник, круг или фигуру произвольной формы или же занимающих всю коорди-
392 Глава 7. Пикселы натную поверхность. В Win32 предусмотрен богатый набор функций API для работы с регионами как с одним из типов объектов GDI. В этом разделе рассматриваются некоторые простые возможности, которые позволят нам двигаться дальше, а более сложный материал будет изложен в дальнейших главах. В Win32 регион представляет собой такой же объект, находящийся под управлением GDI, как контекст устройства, логическое перо, логическая кисть и т. д. Когда приложение вызывает функцию создания региона, система создает объект региона, инициализирует его нужными данными и возвращает манипулятор региона приложению. Манипулятор региона (тип HRGN) позднее передается GDI при выполнении операций с регионом. Простейший способ создания региона: HRGN CreateRectRgn( int hLeftRect, int nTopRect. int nRightRect. int nBottomRect); Функция создает прямоугольный регион, содержащий все точки прямоугольника, определяемого вершинами (nLeftRect, nTopRect) и (nRightRect, nBottomRect), за исключением нижнего и правого края. Учтите, что относительное расположение вершин не фиксируется; GDI автоматически упорядочивает точки так, чтобы они образовывали прямоугольник. Рассмотрим несколько примеров: HRGN hRgnl = OeateRectRgn(0. 0. 0. 0): HRGN hRgnl = CreateRectRgnCO. 0. 1. 1); HRGN hRgnl = CreateRectRgn(-0x7FFFFFF, -0x7FFFFFF. -0x7FFFFFF. -0x7FFFFFF); HRGN hRgnl = CreateRectRgnCl. 1. 0, 0); Первый регион пуст, а второй содержит всего одну точку (0,0). Вероятно, третий вызов создает самый большой регион, поддерживаемый 32-разрядной реализацией GDI в Windows NT/2000. Последний регион идентичен второму. Исключение правого и нижнего края нередко приводит к недоразумениям. При создании прямоугольного региона {0, 0, 1, 1} GDI сохраняет объект региона с прямоугольником {0, 0, 1, 1} вместо {0, 0, 0, 0}. Правый и нижний край исключаются только на завершающей стадии вывода или обработки некоторых запросов на получение информации. Такой подход упрощает выполнение операций с регионами. Например, при увеличении региона {0, 0, 1, 1}вп раз он превращается в регион {0, 0, п, п}, содержащий п х п точек. А как масштабировать регион, представленный квартетом {0, 0, 0, 0}? Во что он должен превращаться — в {О, О, О, 0} или в {0, 0, п-1, п-1}? Когда объект региона становится ненужным, освободите связанные с ним ресурсы функцией DeleteObject. Регион отсечения Одним из атрибутов контекста устройства является регион отсечения. В этом атрибуте хранится объект региона, определяемый приложением и описывающий границы области, в которой осуществляется вывод. Для контекстов устройств, возвращаемых функциями BeginPaint, GetDC или CreateDC, регион отсечения пуст (атрибут равен NULL). В этом случае говорят, что у приложения нет региона отсечения. NULL-регион отсечения не следует путать с пустым регионом в смысле теории множеств; наоборот, это его полная противоположность. При пустом регио-
Отсечение 393 не отсечения (например, CreateRectRgnCO, 0, 0, 0)) не выводится ничего, то есть все изображение отсекается. NULL-регион отсечения означает, что выводится все, что находится в системном регионе. Таким образом, пустой регион отсечения представляет собой пустое множество точек, а NULL-регион можно рассматривать как полный набор точек на поверхности устройства (в этом случае также говорят об отсутствии региона отсечения в контексте устройства). Ниже перечислены базовые функции для работы с регионами отсечения. int GetClipRgnCHDC hDC, HRGN hrgn); int SelectClipRgn(HDC hDC. HRGN hrgn); int ExtSelectClipRgn(HDC hDC. HRGN hrgn. int fnMode); int OffsetClipRgnCHDC hDC. int nXOffset. int nYOffset): int ExcludeC1ipRect(HDC hDC. int nLeftRect. int nTopRect. int nRightRect. int nBottomRect); int IntersectClipRect(HDC hDC, int nLeftRect. int nTopRect. int nRightRect. int nBottomRect); int GetClipBox(HDC hDC. LPRECT lprc): Многие функции регионов отсечения возвращают целочисленный код сложности региона. Другие функции (например, GetClipRgn) возвращают код завершения (0, 1 или -1). Ниже перечислены некоторые из допустимых кодов сложности. NULLREGION Пустой регион (не содержащий ни одной точки) SIMPLEREGION Регион состоит из одного прямоугольника C0MPLEXREGI0N Регион имеет более сложную структуру ERROR Произошла ошибка (предыдущее состояние региона не меняется) Код NULLREGION означает, что регион пуст (например, был получен в результате пересечения двух несмежных регионов). Как было сказано выше, пустой регион не следует путать с NULL-регионом. Код SIMPLEREGION означает, что множество точек региона образует прямоугольник. Кодом C0MPLEXREGI0N описывается любой действительный регион, который не может быть представлен простым прямоугольником. Код ERROR обычно означает, что при вызове функции GDI были переданы недопустимые или равные NULL значения. Если вы знаете, как организовано взаимодействие с контекстами устройств других объектов GDI (например, перьев, кистей или шрифтов), API регионов отсечения может показаться странным и непонятным. Функция GetCurrentObject возвращает информацию о текущем пере, кисти или шрифте, выбранном в контексте устройства, а функция SelectObject заменяет его новым объектом GDI. Объект, выбранный в контексте устройства, считается используемым и может быть удален лишь после исключения его из контекста. Взаимодействие объекта региона с контекстом устройства в большей степени основано на данных, определяющих регион, нежели на манипуляторе региона. Выражаясь точнее, для контекста устройства нельзя получить манипулятор текущего региона отсечения, а после выбора региона отсечения в контексте устройства его манипулятор уже не считается используемым в этом контексте. Чтобы получить текущий регион отсечения для контекста устройства, приложение должно создать действительный объект региона и передать его манипулятор функции GetClipRgn в параметре hrgn. Таким образом, hrgn ссылается на вполне действительный, хотя и реально не используемый объект региона. Если
394 Глава 7. Пикселы контекст устройства содержит регион отсечения, GetClipRgn освобождает объект региона, на который ссылается hrgn, создает копию региона отсечения из контекста устройства и помещает ссылку на новый регион отсечения в hrgn. Результат, возвращаемый GetClipRgn, представляет собой код сложности региона (пустой, прямоугольный или сложный регион). Если регион отсечения в контексте устройства не задан, возвращается значение О (ERROR), а параметр hrgn не изменяется. При вызове SelectClipRgn в параметре hrgn может передаваться манипулятор действительного объекта региона. В этом случае GDI создает копию данных региона и связывает ее с контекстом устройства. Обратите внимание: переданный манипулятор не закрепляется за контекстом устройства; приложение может удалить его после вызова. Параметр hrgn также может быть равен NULL; в этом случае объект региона отсечения, хранящийся в контексте устройства, освобождается, а в контексте устройства устанавливается NULL-регион отсечения. Такие атрибуты контекста, как логическое перо, в определенном смысле задаются своими манипуляторами. После того как манипулятор объекта будет выбран в контексте устройства, он считается используемым в этом контексте и не может быть удален до тех пор, пока в контексте не будет выбран манипулятор другого объекта того же типа. С другой стороны, регион отсечения задается своими данными. При передаче манипулятора региона функции SelectClipRgn создается копия данных региона, а не манипулятора. Когда приложение захочет получить текущий регион отсечения, оно предоставляет действительный манипулятор региона, и данные этого региона заменяются данными региона отсечения. Если в контексте устройства отсутствует регион отсечения, функция GetClipRgn возвращает 0, в этом случае параметр-регион не изменяется. Вообще говоря, в только что созданных контекстах устройств, возвращаемых функцией BeginPaint или CreateDC, регион отсечения отсутствует, поэтому GetClipRgn всегда возвращает 0. Еще раз напоминаем — отсутствие региона отсечения означает, что выводится все содержимое системного региона. Приложение всегда должно проверять результат, полученный при вызове GetClipRgn, и действовать по ситуации, не полагаясь на один лишь манипулятор объекта региона. Например, следующий фрагмент проверяет, вернула ли функция GetClipRgn значение 0; в этом случае она удаляет объект региона и присваивает его манипулятору значение NULL. По этому манипулятору, равному NULL, приложение может узнать, присутствует ли в контексте устройства регион отсечения. HRGN hRgn = CreateRectRgrUO. 0. 1, 1); if ( GetClipRgnChDC. hRgn)==0) { DeleteObjectChRgn); hRgn - NULL; } Как упоминалось выше, регион представляет собой множество точек. Следовательно, операции с регионами легко моделируются на основе операций с множествами. В табл. 7.1 приведена сводка таких операций, поддерживаемых в GDI.
Отсечение 395 Таблица 7.1. Бинарные операции с регионами Режим Результат RGN_AND Регион, соответствующей области перекрытия регионов 1 и 2 RGNC0PY Копия региона 1 RGNDIFF Регион, точки которого принадлежат региону 1, но не принадлежат региону 2 RGN0R Регион, точки которого принадлежат хотя бы одному из регионов 1 и 2 RGN_X0R Регион, точки которого принадлежат либо региону 1, либо региону 2, но не одновременно Когда объект региона выбирается в качестве региона отсечения или метаре- гиона в контексте устройства, предполагается, что он определен в системе координат устройства этого контекста. В этом отношении функции регионов отличаются от функций GDI, которым обычно передаются логические координаты контекста устройства. Для преобразования координат из логической системы в систему координат устройства можно воспользоваться функцией GDI LPtoDP. Кроме того, вы должны хорошо разбираться в пустых и абсолютных регионах. Пустой регион не содержит ни одной точки, поэтому функция CreateRectRgnCO, О, О, 0) создает пустой регион. Абсолютный регион содержит все точки координатного пространства. Впрочем, попытка передать функции CreateRectRgn минимальные и максимальные 32-разрядные числа завершается неудачей. Похоже, регион максимального размера создается при следующих параметрах: {-0x7FFFFFF, -0x7FFFFFF. 0x7FFFFFF, 0x7FFFFFF} Несомненно, это связано с тем, что графический механизм использует в системе координат устройства числа с фиксированной точкой в формате 28.4, содержащие 28-разрядную целую часть со знаком. Впрочем, для практических целей можно сгенерировать «абсолютный» регион по размерам поверхности физического устройства в пикселах. Функция ExtSelectClipRegion позволяет лучше управлять процессом объединения региона hrgn с существующим регионом отсечения в контексте устройства для получения нового региона отсечения с использованием операций, перечисленных в табл. 7.1. В этом случае первый операнд является текущим регионом отсечения контекста устройства, а второй операнд задается параметром hrgn. Для функции ExtSelectClipRegion режим RGN_C0PY назначает текущим регионом отсечения копию hrgn, как и для функции SelectClipRegion. Выше уже упоминалось о том, что в только что созданных контекстах устройств, возвращаемых функциями BeginPaint и GetDC, регион отсечения отсутствует; как же в этом случае работает функция ExtSelectClipRegion? При вызове ExtSelectClipRegion для контекстов устройств без региона отсечения RGN_AND интерпретируется как RGN_C0PY. Для RGNJDIFF, RGNJJR или RGNX0R используется размер физического устройства. Рассмотрим следующий фрагмент: PAINTSTRUCT ps; HDC hDC = BeginPaint(hWnd. & ps);
396 Глава 7. Пикселы HRGN hRgn = CreateRectRgnCO. 0. 200. 200); int rslt = ExtSelectClip(hDC. hRgn. RGN_DIFF); rslt = GetClipRgnChDC. hRgn); Этот фрагмент создает прямоугольный регион 200 х 200 и пытается изменить текущий регион отсечения с помощью операции RGNDIFF. В контексте устройства отсутствует регион отсечения, поэтому в экранном режиме 1152 х 864 графический механизм использует прямоугольный регион {0,0,1152,864}, а функция GetClipRgn возвращает регион, состоящий из двух прямоугольников {200,0,1152,200} и {0,200,1152,864}. Следующие три функции для работы с регионами отсечения просты. Для контекста устройства, обладающего регионом отсечения, функция OffsetClipRgn смещает все точки объекта на величину (nXOffset, nYOffset). Функция Exclude- ClipRgn удаляет прямоугольник из текущего региона отсечения по аналогии с режимом RGNDIFF функции ExtSelectClip. Функция IntersectClipRect вычисляет пересечение текущего региона отсечения с прямоугольником по аналогии с режимом RGN_AND функции ExtSelectClip. По сравнению с ExtSelectClip функции ExcludedipRect и IntersectClipRect предоставляют более простой способ модификации региона отсечения. Функция GetClipBox возвращает наименьший ограничивающий прямоугольник для пересечения текущего системного региона и региона отсечения. Будьте внимательны: речь идет именно о пересечении этих двух регионов! По умолчанию в контексте устройства нет региона отсечения, поэтому GetClipBox возвращает ограничивающий прямоугольник системного региона, совпадающий с прямоугольником rcPaint в структуре PAINTSTRUCT, заполняемой функцией BeginPaint. По мере построения региона отсечения вызовами SelectClipRgn и ExtSelectClipRgn механизм GDI отслеживает его пересечение с системным регионом и его ограничивающий прямоугольник, который и возвращается функцией GetClipBox. Если в процессе обработки сообщения WMPAINT приложение изменяет регион отсечения, вызов GetClipBox дает более точную информацию, чем прямоугольник rcPaint. Метарегион Метарегионы относятся к числу практически не документированных возможностей GDI. Они не упоминаются среди атрибутов контекста устройства, для них не указывается значение по умолчанию и не объясняется связь с системным регионом и регионом отсечения. В основе дальнейшего материала лежат самостоятельные исследования автора. Метарегион представляет собой «регион отсечения первого уровня», управляющий отсечением на другом уровне. Вспомним принцип отсечения в GDI: в контексте устройства вывод ограничивается областью пересечения системного региона, метарегиона и региона отсечения. Все, что лежит за пределами этой области, отсекается. Если выясняется, что работать с одним уровнем отсечения на уровне приложения слишком неудобно, в вашем распоряжении оказывается второй уровень (по аналогии с двумя уровнями логических систем координат GDI). Если вам хватает одного уровня отсечения, определяемого приложением, вы можете забыть о метарегионах.
Отсечение 397 В только что созданном контексте устройства метарегион отсутствует. Даже после выбора региона отсечения метарегион все равно не существует до тех пор, пока не будет вызвана функция SetMetaRgn. Ниже перечислены функции API для работы с метарегионами. int SetMetaRgn(HDC hDC): int GetMetaRgn(HDC hDC, HRGN hRgn); В отличие от других функций с префиксом Set, функция SetMetaRgn получает всего один параметр — манипулятор контекста устройства. Другим, неявным параметром является текущий регион отсечения. Функция SetMetaRgn заменяет текущий метарегион и его пересечение с текущим регионом отсечения, а затем сбрасывает контекст устройства в состояние, при котором у него отсутствует регион отсечения. Поскольку метарегион отсекает графические примитивы так же, как и регион отсечения, непосредственно после вызова SetMetaRgn общее отсечение в контексте устройства не изменяется. Но теперь, когда приложение переместило старый регион отсечения на уровень метарегиона, оно может построить новый регион отсечения, который обеспечит дополнительные ограничения при выводе. Функция GetMetaRgn работает вполне нормально; она (по аналогии с GetClipRgn) возвращает данные текущего метарегиона через манипулятор существующего региона. В только что созданном контексте устройства нет региона отсечения, поэтому GetMetaRgn в этом случае возвращает 0, не обновляя hRgn. Интересно заметить, что при многократном вызове SetMetaRgn метарегион контекста только уменьшается и никогда не увеличивается в размерах, причем после определения метарегион уже невозможно сбросить в состояние NULL- региона. Одним из обходных путей является использование функций SaveDC и RestoreDC. Рассмотрим пример использования метарегиона. Следующий фрагмент создает метарегион и регион отсечения с размерами 100 х 100, но со смещением 50 х 50, поэтому весь вывод ограничивается областью 50 х 50. HRGN hRgn - CreateRectRgn(0, 0, 100. 100); SelectClipRgn(hDC. hRgn): // Мета: нет. регион отсечения: 100x100 SetMetaRgn(hDC); // Мета: 100x100. регион отсечения: нет SelectClipRgn(hDC. hRgn): // Мета: 100x100. регион отсечения: совпадает OffsetClipRgn(hDC. 50. 50); // Мета: 100x100. регион отсечения: 100x100 Rectangle(0. 0. 150, 150); // Рисует прямоугольник 50x50 DeleteObject(hRgn); Вероятно, вы уже поняли, что метарегионы не относятся к числу обязательных возможностей. Если приложение управляет всем отсечением на одном уровне, оно может легко имитировать метарегионы и даже что-нибудь посложнее. Но если в приложении используется многоуровневая схема вывода, то метарегион позволяет управлять отсечением в двух местах. Допустим, приложение осуществляет вывод при помощи библиотеки DLL, разработанной независимой фирмой, и в коде вывода используется регион отсечения GDI. Приложение не сможет изменить чужой исходный текст, чтобы ограничить вывод определенной областью, но оно может установить метарегион и добиться того же эффекта. Существование малоизвестных возможностей Win32 API, к числу которых принадлежат и метарегионы, всегда объясняется практическими причинами. Метарегионы используются GDI при воспроизведении метафайлов.
398 Глава 7. Пикселы Пять регионов контекста устройства Итак, у нас имеется системный регион, метарегион и регион отсечения в контексте устройства. Их пересечение фактически и определяет ту область, в которой происходит вывод. Системный регион находится под управлением диспетчера окон; метарегион и регион отсечения контролируются пользовательским приложением. Если бы для каждого графического вызова GDI приходилось заново вычислять пересечение и передавать его драйверу устройства, работа графического механизма была бы крайне неэффективной. Как нетрудно предположить, в контексте устройства содержатся еще два региона, повышающих быстродействие вывода: регион API и регион Рао. Регион API представляет собой пересечение метарегиона с регионом отсечения. Конечно, название связано с тем, что этот регион находится под управлением GDI API. Регион Рао является пересечением региона API с системным регионом. Он был назван по имени программиста Microsoft, который предложил хранить этот регион в контексте устройства. При изменении метарегиона или региона отсечения система пересчитывает заново регион API и регион Рао. Регионы хранятся в контексте устройства в виде указателей на объекты ядра, а не в виде манипуляторов. Становится понятно, почему функция SetClipRgn создает копию региона вместо того, чтобы использовать манипулятор GDI, а функции GetClipRgn в качестве параметра должен передаваться действительный манипулятор региона. Из этих пяти регионов данные системного региона, метарегиона, региона отсечения и региона API могут быть получены вызовом одной функции API GetRandomRgn. Вы когда-нибудь задумывались, почему эта функция называется GetRandomRgn1? Какую пользу способен принести случайный регаон? Функция GetRandomRgn предоставляет произвольный доступ к четырем регионам, хранящимся в контексте устройства. В последнем параметре GetRandomRgn передается целочисленный индекс, для которого документировано всего одно значение SYSRGN (4). Другие приемлемые значения: #define CLIPRGN 1 // GetClipRgn #define METARGN 2 // GetMetaRgn #define APIRGN 3 #define SYSRGN 4 При помощи функции GetRandomRgn приложение может получить данные региона отсечения, метарегиона, региона API и системного региона. Регион Рао нетрудно вычислить по данным региона API и системного региона. Наглядное представление регионов в контексте устройства В разделе «Пример программы: графический вывод в контексте устройства» главы 5 мы создали программу для наглядного представления сообщений прорисовки окна и системного региона. Если вам кажется, что подобные програм- 1 То есть «Получить произвольный регион», однако возможен и другой перевод — «получить случайный регион». — Примеч. перев.
Отсечение 399 мы написаны «для чайников» и профессионалам не годятся, в этом разделе мы напишем новую программу ClipRegion, обеспечивающую наглядное представление региона отсечения, метарегиона и региона API вместе с системным регионом. Прежде всего нам понадобится функция для получения всех четырех регионов и вывода информации о них в текстовом окне. Функция DumpRegions приведена ниже. void KMyCanvas::DumpRegions(HDC hDC) { for (int i-1: i<-4: i++) { m_bValid[i] « false; int rslt - GetRandomRgn(hDC, m_hRandomRgn[i]. i); switch ( rslt ) { case 1: m_bValid[i] - true; mJjDg.DumpRegion("RandomRgn(fcd)\ m_hRandomRgn[i]. false, i); break; case -1: m_Log.Log("RandomRgn(*d) Error\r\n". i); break; case 0: m_Log.Log("RandomRgn($d) no region\r\n". i); break; default: m_Log.Log("Unexpected\r\n"); } } } Функция вызывает GetRandomRgn для получения информации о регионе отсечения, метарегионе, регионе API и системном регионе контекста устройства и проверяет возвращаемое значение. Данные каждого региона выводятся в текстовом окне при помощи класса KLogWindow. Манипуляторы регионов сохраняются в переменных класса для последующего использования функцией DrawRegions. void KMyCanvas::DrawRegions(HDC hDC) { HBRUSH hBrush; SetBkMode(hDC. TRANSPARENT); if ( m_bValid[l] ) // Регион отсечения { hBrush - CreateHatchBrush(HS_VERTICAL. RGB(0xFF. 0. 0)); FillRgnChDC, m_hRandomRgn[l]. hBrush); DeleteObject(hBrush); }
400 Глава 7. Пикселы if ( m_bValid[2] ) // Метарегион { hBrush « CreateHatchBrush(HS_HORIZONTAL. RGB(0, OxFF. 0)); FillRgnChDC, m_hRandomRgn[2]. hBrush); DeleteObject(hBrush); } } } Функция DrawRegions вызывается после возврата из EndPaint. Она использует новый контекст устройства, возвращаемый функцией GetDC, и поэтому может рисовать во всей клиентской области, а не только в системном регионе. При выводе региона отсечения и метарегиона используются разные штриховые кисти. Регион API должен представлять собой пересечение этих двух регионов. Мы знаем, что в только что созданном контексте региона отсечения, метарегиона и региона API быть не должно, поэтому для получения осмысленных результатов необходимо подготовить эксперимент при помощи функции TestClipMeta, приведенной ниже. В программе ClipRegion определяются четыре режима, выбираемые в главном меню. Первый режим не устанавливает региона отсечения и метарегиона, поэтому вы можете увидеть ситуацию по умолчанию. Во втором режиме устанавливается регион отсечения, в третьем — метарегион, а в четвертом — регион отсечения вместе с метарегионом. В качестве региона отсечения используется эллиптический регион, находящийся в левых трех четвертях клиентской области. Метарегион также имеет форму эллипса и находится в верхних трех четвертях клиентской области. void KMyCanvas::TestC1ipMeta(HDC hDC. const RECT & rect) { HRGN hRgn; switch ( m_test ) { case IDM_TEST_DEFAULT: break; case IDM_TEST_SETCLIP: hRgn = CreateEllipticRgnCO. 0. rect.right*3/4. rect.bottom); SelectClipRgn(hDC. hRgn); DeleteObject(hRgn); break; case IDM_TEST_SETMETA: hRgn = CreateEl1ipticRgn(0. 0, rect.right. rect.bottom*3/4); SelectClipRgn(hDC. hRgn): SetMetaRgn(hDC); break; case IDM_TEST_SETMETACLIP; hRgn = CreateEllipticRgnCO, 0, rect.right. rect.bottom*3/4); SelectC1ipRgn(hDC. hRgn);
Отсечение 401 SetMetaRgn(hDC); DeleteObject(hRgn); hRgn = CreateEllipticRgnCO, 0. rect.right*3/4. rect.bottom): SelectClipRgn(hDC. hRgn); break; } Del eteObject(hRgn); // При установке метарегиона и региона отсечения // вывод происходит только в пересечении системного региона // с метарегионом и регионом отсечения HBRUSH hBrush « CreateSolidBrush(RGB(0. 0, OxFF)); FillRectChDC. & rect. hBrush); DeleteObject(hBrush); DumpRegions(hDC); } Функция TestClipMeta вызывается главной функцией вывода KMyCanvas: :0nDraw после вывода системного региона. Таким образом, после установки региона отсечения и метарегиона при выводе учитываются все три региона. Программа пытается закрасить всю клиентскую область однородной синей кистью. Если наши выкладки верны, закрашено будет только пересечение системного региона, региона отсечения и метарегиона, а все остальное отсекается. На рис. 7.2 показано, как выглядит окно программы при одновременной установке региона отсечения и метарегиона. Рис. 7.2. Регионы в контексте устройства
402 Глава 7. Пикселы Прямоугольник рамки окна изображает системный регион. Вертикальными линиями закрашивается регион отсечения, а горизонтальными — метарегион. Регион API закрашен сеткой из линий, а область сплошной закраски обозначает пересечение регионов (то есть область вывода). Если провести все четыре опыта и просмотреть содержимое окна выходных данных, можно получить довольно интересные результаты. Пример: // IDMJESTJDEFAULT RandomRgn(l) no region RandomRgn(2) no region RandomRgnO) no region RandomRgn(4) SIMPLEREGION RgnBox«[464, 247, 922. 590) 1 rects // IDMJEST_SETCLIP RandomRgn(l) C0MPLEXREGI0N RgnBox=[0. 0. 342. 342) 201 rects RandomRgn(2) no region RandomRgn(3) C0MPLEXREGI0N RgnBox-[0. 0. 342. 342) 201 rects RandomRgn(4) SIMPLEREGION RgnBox«[464. 247. 922. 590) 1 rects // IDMJESTMETA RandomRgn(l) no region RandomRgn(2) COMPLEXREGION RgnBox=[0, 0. 457. 256) 189 rects RandomRgn(3) COMPLEXREGION RgnBox=[0. 0. 457. 256) 189 rects RandomRgn(4) SIMPLEREGION RgnBox=[464. 247. 922. 590) 1 rects // IDMJESTMETACLIP RandomRgn(l) COMPLEXREGION RgnBox=[0. 0. 342. 342) 201 rects RandomRgn(2) COMPLEXREGION RgnBox=[0. 0. 457. 256) 189 rects RandomRgn(3) COMPLEXREGION RgnBox=[2. 2. 342. 256) 191 rects RandomRgn(4) SIMPLEREGION RgnBox=[464. 247. 922. 590) 1 rects В протоколе приведены значения по умолчанию для трех регионов. Системный регион (RandomRgn[4]) обеспечивает независимое отсечение; пересечение с ним региона отсечения и метарегиона образует регион API (RandomRgn[3]). Чтобы сгенерировать более интересный системный регион, расположите поверх главного окна программы ClipRegion какое-нибудь маленькое окно, а затем отодвиньте его в сторону. При этом генерируется сообщение WM_PAINT для перерисовки вновь открывшейся области, в результате чего может возникнуть сложный системный регион. Цвет Цвет возникает в наших глазах как восприятие света, отраженного объектами. Чтобы использовать цвета при программировании компьютерной графики, мы должны уметь описывать их в числовой форме, легко реализуемой на обычном компьютерном оборудовании. Кроме того, эти описания должны быть как-то связаны с нормальными представлениями о цветах, доступными для человека. Цвет обычно описывается несколькими атрибутами, принимающими значения из определенных интервалов. Эти атрибуты можно рассматривать как координаты некоторого пространства, где каждый цвет представлен отдельной точ-
Цвет 403 кой. Такое координатное пространство для описания цветов называется цветовым пространством (color space). В компьютерной графике используются десятки всевозможных цветовых пространств. Экраны мониторов обычно используют цветовое пространство RGB с тремя основными цветами — красным, зеленым и синим. На цветных принтерах чаще используется цветовое пространство CMYK, в котором каждый цвет является комбинацией голубой, малиновой, желтой и черной составляющей. Художники предпочитают описывать цвета в терминах оттенка, насыщенности и яркости. Наиболее распространенные цветовые пространства рассматриваются в следующих подразделах. Цветовое пространство RGB В графической системе Windows для описания цветов обычно используется цветовое пространство RGB. Система координат в этом пространстве состоит из трех осей: красной, зеленой и синей составляющей. Каждый цвет является точкой этого трехмерного пространства и описывается триплетом (красный, зеленый, синий). В литературе и в описании алгоритмов компьютерной графики для упрощения математических операций обычно используются нормализованные компоненты, представленные вещественными числами в интервале от 0 до 1. Однако интерфейсы графического программирования (такие, как GDI) должны быть практичными и эффективными, поэтому цвета в них представляются дискретными величинами, удобными для компьютерного хранения и обработки. В Windows API каждая цветовая составляющая представляется одним байтом, что позволяет использовать до 256 разных уровней интенсивности (0-255). Таким образом, цвет в RGB-пространстве Windows представляется тремя байтами (24 битами); получается 224 комбинаций, или 16,7 миллиона возможных цветов. Цветовое пространство RGB обладает свойством аддитивности. Начало координат (0,0,0) соответствует черному цвету. Прибавление к нему полной красной составляющей дает красный цвет (255,0,0), прибавление полной зеленой составляющей превращает его в желтый (255,255,0) и, наконец, прибавление полной синей составляющей дает белый цвет (255,255,255). В GDI существует несколько макросов для объединения трех составляющих RGB в одно 32-разрядное значение типа C0L0RREF и для разделения данных C0L0RREF на составляющие RGB. Эти макросы можно представить в виде следующих подставляемых (inline) функций: C0L0RREF RGBCBYTE ByRed, BYTE byGreen, BYTE byBlue); BYTE GetRValue(COLORREF rgb); BYTE GetGValue(C0L0RREF rgb); BYTE GetBValue(C0L0RREF rgb): Ниже перечислены некоторые удобные определения для часто используемых цветов: const C0L0RREF black = RGB( 0. 0. 0); const C0L0RREF darkred = RGB(0x80. 0, 0); const C0L0RREF darkgreen - RGB( 0.0x80. 0); const C0L0RREF darkyellow - RGB(0x80.0x80. 0); const COLORREF darkblue - RGB( 0. 0,0x80); const COLORREF darkmagenta - RGB(0x80. 0.0x80):
404 Глава 7. Пикселы const COLORREF darkcyan const COLORREF darkgray const COLORREF moneygreen const COLORREF skyblue const COLORREF cream const COLORREF mediumgray const COLORREF lightgray const COLORREF red const COLORREF green const COLORREF yellow const COLORREF blue const COLORREF magenta const COLORREF cyan const COLORREF white - RGB( 0.0x80.0x80); - RGB(0x80.0x80.0x80); = RGB(OxCO.OxDC.OxCO) - RGB(0xA6.0xCA.0xF0) - RGB(0xFF.0xFB.0xF0) - RGB(0xA0.0xA0.0xA4) - RGB(0xC0.0xC0.0xC0) - RGBCOxFF. 0. 0) = RGB( O.OxFF. 0) - RGBCOxFF.OxFF. 0) - RGB( 0. O.OxFF) - RGBCOxFF. O.OxFF) - RGB( O.OxFF.OxFF) = RGBCOxFF,OxFF.OxFF) В GDI API организовано аппаратно-независимое использование значений в формате RGB. Обычно приложение не занимается преобразованием цветов перед их сохранением в памяти видеоадаптера — эта задача решается драйвером экрана. В некоторых режимах пользовательское приложение управляет содержимым системной палитры для улучшения качества изображения. Системная палитра рассматривается вместе с растрами в одной из последующих глав книги. Для экспериментов с изменением цвета проще всего воспользоваться функцией SetPixel, изменяющей цвет одного пиксела. Функция SetPixel определяется следующим образом: COLORREF SetPixelChDC HDC. int x. int y. COLORREF crColor); Функция SetPixel выводит на поверхности один пиксел цвета crColor в точке с координатами (х, у), с учетом настройки логического координатного пространства и отсечения. Впрочем, один пиксел — это слишком тривиально, поэтому в следующем примере мы нарисуем цветовой куб RGB (то есть куб, образованный 256 уровнями красной, зеленой и синей составляющих в трехмерном пространстве). Код программы RGBCube приведен в листинге 7.1. Листинг 7.1. Вывод трехмерного куба RGB без использования мировых преобразований void RGBCube(HDC hDC) { int r, g. b; // Нарисовать и пометить оси // Красный - OxFF for (g-0: g<256; g++) for (b-0: b<256: b++) SetPixeHhDC. g. b. RGB(0xFF. g, b)); // Синий = OxFF. верхняя грань, со сдвигом for (g-0: g<256; g++) for (r-0: r<256; r+=2) SetPixel(hDC. g+128-r/2, 255+128-Г/2. RGB(r. g. OxFF));
Цвет 405 // Зеленый = 255. правая грань, for (b-0; b<256; b++) for (r=0; r<256; r+=2) SetPixeTV(hDC. 255+128-Г/2. со сдвигом b+128-r/2. RGB(r. OxFF. b)); Программа рисует три грани объемного куба, у которых красная, синяя и зеленая составляющие равны 255. Первая грань имеет прямоугольную форму, а две других выводятся со сдвигом для имитации объема. В принципе сдвиг можно было бы выполнить путем мировых преобразований, но эта задача легко решается вручную простым пропусканием половины строк развертки и небольшим смещением выводимых пикселов. В нарисованном цветном кубе видны все вершины, кроме черного начала координат. Небольшой объем вспомогательного кода обеспечивает отображение области просмотра и вывод осей. Окончательный результат показан на рис. 7.3. Синий COLORREF (0,0,255) Зеленый COLORREF (0,255,0) Красный COLORREF (255,0,0) Рис. 7.3. Программа наблюдения за объектами GDI К сожалению, на рисунке наш красивый цветной куб окрашен в оттенки серого цвета. Возникает другой вопрос — как цвет, заданный в формате RGB, преобразуется в оттенки серого? Ниже приведены простые формулы. Grayscale = (Red*30 + Green*59 + B1ue*ll + 50) / 100 Grayscale = (Red*77 + Green*151 + Blue*28 + 50) / 256 Цветовые составляющие вносят разный вклад в уровень серого цвета, и этот факт отражен в весовых коэффициентах. Чистый синий цвет выглядит достаточно темным, поэтому ему присвоен наименьший вес, за которым по возрастанию следуют веса красного и зеленого цвета. Прибавление 50 или 128 обеспечивает округление в верхнюю сторону. Если ваш компилятор или процессор плохо справляется с делением, воспользуйтесь второй формулой, чтобы компилятору хватило операции сдвига. Современные компиляторы творят настоящие чудеса в области оптимизации. Не удивляйтесь, если из вашего двоичного файла пропадают операции умножения или деления на константу — компилятор
406 Глава 7. Пикселы может заменить их более быстрыми инструкциями. Из тех же соображений при написании оптимизирующего ассемблерного кода не следует использовать инструкции умножения на такие константы, как 3 (например, при вычислении 24- разрядных адресов). Компилятор лучше справится с этой задачей, заменяя умножение фиксированным числом сложений. Цветовое пространство HLS Хотя цветовое пространство RGB хорошо подходит для хранения и обработки данных при программировании компьютерной графики, оно плохо соответствует нашему восприятию цветов. В цветовом пространстве HLS цвета описываются оттенком (hue, H), насыщенностью (saturation, S) и яркостью (lightness, L). Эти характеристики гораздо лучше соответствуют нашим представлениям о цветах. Цветовое пространство HLS можно рассматривать как результат поворота цветового куба RGB. Давайте развернем цветовой куб RGB в трехмерном пространстве так, чтобы белый угол находился в верхней точке, а черный угол — в нижней точке. Если смотреть вдоль линии, проходящей от белого угла (255,255,255) к черному углу (0,0,0), вы увидите шесть углов: красный, желтый, зеленый, голубой, синий и малиновый; все остальные цвета расположены между ними. Компонент оттенка в пространстве HLS измеряется в угловых величинах от 0 до 360 градусов; 0 соответствует красному цвету, 60 — желтому, 120 — зеленому, 180 — голубому, 240 — синему, 300 — малиновому, а 360 — снова красному. Оттенок определяет угловое смещение цвета относительно красного угла куба RGB при взгляде вдоль диагонали от белого угла к черному. Яркость определяет относительную высоту точки в развернутом кубе; 1 соответствует белому цвету, а 0 — черному. Насыщенность определяет расстояние цветовой точки от диагонали, проведенной от белого угла к черному. Функция, приведенная в листинге 7.1, рисовала трехмерный куб RGB без обращения к мировым преобразованиям GDI. В листинге 7.2 показано, как использовать мировые преобразования для отображения каждой из трех граней в соответствующую треть шестиугольника HLS. Помимо всего прочего, этот фрагмент призван проиллюстрировать технику мировых преобразований. Листинг 7.2. Вывод развернутого куба RGB в виде сверху void ColorRectCHDC hDC. C0L0RREF cO. C0L0RREF dx, C0L0RREF dy) { for (int x=0; x<256; x++) for (int y=0: y<256; y++) SetPixeKhDC. x, y. cO + x * dx + у * dy): MoveToEx(hDC. 0. 0. NULL); LineTo(hDC. 0. 255); LineTo(hDC. 255. 255); MoveToExChDC. 0. 0, NULL); LineTo(hDC. 255. 0); LineToChDC. 255. 255); // LineToChDC. 0. 0): } }
Цвет 407 void RGBCube2HLSHexagon(HDC hDC) { KAffine affine; SetGraphicsMode(hDC. GM_ADVANCED); FLOAT г = 254; // Четное число number < 255 FLOAT x = г / 2; // cos(60); FLOAT у - (FLOAT) (r * 1.732/2); // sin(60); // Задать положение центра шестиугольника с небольшими полями SetViewportOrgExChDC. (int)r + 40. (int)y + 40. NULL); // Красный - 255 affine.MapTriCO.O. 255.0. 0.255. г.О. х.у. х.-у); SetWorldTransformChDC. & affine.m_xm); ColorRect(hDC. RGB(0xFF. 0. 0). RGB(0. 1. 0). RGB(0. 0. D); // Зеленый = 255 affine.MapTriCO.O. 255.0. 0.255. -x.y. x.y. -r.O); SetWorldTransform(hDC. & affine.m_xm); ColorRecKhDC. RGB(0. OxFF. 0). RGBd. 0. 0). RGB(0. 0. I)); // Синий = 255 affine.MapTri(0.0. 255.0. 0.255. -x.-y. -r.0. x.-y); SetWorldTransformChDC. & affine.m_xm); ColorRectChDC. RGB(0. 0. OxFF). RGB(0. I. 0). RGBd. 0. 0)): } Программа рисует каждую из трех граней как прямоугольник в мировой системе координат. Прямоугольники отображаются на параллелограммы, образующие шестиугольник. Преобразование из мировых координат в страничные выполняется классом KAffine с привязкой по трем точкам. Цветовое пространство HLS изображено на рис. 7.4. Рис. 7.4. Куб RGB в направлении от белого угла к черному
408 Глава 7. Пикселы Цветовое пространство HLS образует коническую фигуру, которую иногда изображают при помощи двух шестиугольников. В центре фигуры яркость равна 0,5. В вершине фигуры яркость равна 1 (белый цвет), а в нижней точке — 0 (черный цвет). Оттенок определяется угловым расстоянием, причем в углах О, 60, 120, 180, 240 и 300 градусов расположены соответственно красный, желтый, зеленый, голубой, синий и малиновый цвета. Насыщенность определяет глубину цвета (от тусклого к интенсивному); точки с максимальной интенсивностью 1 находятся на краях шестиугольника, а точки с нулевой интенсивностью расположены в центре. Яркость 0 соответствует черному, а яркость 1 — белому цвету. В цветовом пространстве HLS оттенок обычно представляется вещественным числом из интервала [0..360], а яркость и насыщенность лежат в интервале [0..1]. В листинге 7.3 приведен класс C++ для преобразования цветов между моделями RGB и HLS. Листинг 7.3. Класс KColor: преобразование между RGB и HLS class KColor { typedef enum { Red, Green. Blue }; public: unsigned char red, green, blue; double lightness, saturation, hue; void ToHLS(void); void ToRGB(void); void KColor::ToHLS(void) { double mn, mx; int major; if ( red < green ) { mn = red; mx = green; major = Green: } else { mn = green; mx = red; major = Red; } if ( blue < mn ) mn = blue; else if ( blue > mx ) { mx - blue; major = Blue: } if ( mn==mx ) {
lightness = mn/255: saturation = 0: hue = 0; } else { lightness » (mn+mx) / 510; if ( lightness <= 0.5 ) saturation = (mx-mn) / (mn+mx); else saturation = (mx-mn) / (510-mn-mx); switch ( major ) { case Red : hue = (green-blue) * 60 / (mx-mn) + 360; break; case Green; hue = (blue-red) * 60 / (mx-mn) + 120; break; case Blue : hue = (red-green) * 60 / (mx-mn) + 240: } if (hue >= 360) hue = hue - 360: } } unsigned char Value(double ml. double m2. double h) { if (h >= 360) h -= 360; else if (h < 0) h +- 360; if (h < 60) ml = ml + (m2 - ml) * h / 60; else if (h < 180) ml = m2: else if (h < 240) ml = ml + (m2 - ml) * (240 - h) / 60; return (unsigned char)(ml * 255): } void KColor::ToRGB(void) { if (saturation == 0) { red = green = blue = (unsigned char) (lightness*255); } else { double ml. m2; if ( lightness <= 0.5 )
410 Глава 7. Пикселы Листинг 7.3. Продолжение m2 - lightness + lightness * saturation; else m2 - lightness + saturation - lightness * saturation; ml - 2 * lightness - m2; red - ValueCml, m2. hue + 120); green - ValueCml, m2. hue); blue - ValueCml, m2. hue - 120): } } Цветовое пространство HLS часто используется для выбора цвета. Например, в стандартном диалоговом окне выбора цвета в ОС Windows цветовая модель HLS управляет выбором цвета. Плоскость «оттенок/насыщенность» отображается в виде прямоугольника. Пользователь выбирает цвет, перемещая курсор мыши в прямоугольной области, а затем регулирует яркость цвета на отдельной полосе прокрутки. Листинг 7.4 показывает, как имитировать такое диалоговое окно средствами класса KColor. Результат иллюстрирует рис. 7.5. Рис. 7.5. Цветовая палитра модели HLS Листинг 7.4. Вывод цветовой палитры HLS void HLSColorPa1ette(HDC hDC. int scale. KColor & selection) { KColor c; for (int hue=0; hue<360; hue++) for (int sat=0: sat<=scale: sat++)
Цвет 411 { с.hue * hue; с.lightness * 0.5 ; с.saturation - ((double) sat)/scale; c.ToRGBO: SetPixeKhDC. hue. sat, RGB(c.red. c.green, c.blue)); } for (int 1-0; l<»scale; 1++) { с * selection; c.lightness - ((double)l)/scale; c.ToRGBO: for (int x=0; x<64; x++) SetPixeKhDC. scale+20+x. 1. RGB(c.red. c.green. c.blue)); } } Первая часть этой функции демонстрирует преобразование цвета из модели HLS в RGB и вывод на экран. В трехмерной модели имитируется разная освещенность объектов, при этом яркость изменяется в зависимости от расстояния и угла между объектом и источником света. Вторая часть функции показывает, как решить эту задачу в модели HLS изменением яркости при постоянном оттенке и насыщенности. Эта методика очень полезна при создании градиентных заливок, когда простого смешения цветов RGB оказывается недостаточно. Индексируемые цвета и палитры Помимо задания цветов в модели RGB, в Win32 API также поддерживается возможность их задания в виде индексов палитры — цветовой таблицы, содержащей значения RGB. У каждого контекста устройства имеется такой атрибут, как логическая палитра. Логические палитры принадлежат к числу объектов GDI, и для ссылок на них используются манипуляторы типа HPALETTE. Ниже перечислены основные функции для работы с палитрами. HPALETTE CreateHalftonePaletteCHDC hDC); COLORREF GetNearestColor(HDC hDC. COLORREF crColor); UINT GetNearestPalettelndexCHPALETTE hpal. COLORREF crColor); UINT GetPaletteEntries(HPALETTE hPal. UINT iStartlndex. UINT nEntries. LPPALETTEENTRY Ippe): UINT RealizePaletteCHDC hDC); HPALETTE SelectPaletteCHDC hDC. HPALETTE hPal. BOOL bForceBackground): Функция CreateHalftonePalette создает палитру из 256 цветов, равномерно распределенных в кубе RGB. Такая палитра позволяет отображать многоцветную графику полутоновыми методами в видеорежимах, использующих палитру. Функция GetNearestColor ищет в текущей логической палитре контекста устрой-
412 Глава 7. Пикселы ства цвет, ближайший к заданному. Функция GetNearestPalettelndex возвращает индекс в палитре цвета, ближайшего к заданному. Функция GetPaletteEntries загружает из логической палитры определения элементов из заданного интервала. Функция RealizePalette готовит системную палитру к отображению цветов из логической палитры. Функция SelectPalette присоединяет логическую палитру к контексту устройства и возвращает исходную логическую палитру. Манипулятор текущей палитры также можно получить функцией GetCurrentObject(hDC, 0BJ_PAL). Для работы с палитрой существуют следующие макросы: COLORREF PALETTEINDEXCWORD wPalettelndex): COLORREF PALETTERGBCBYTE bRed. BYTE bGreen. BYTE bBlue); Макрос PALETTEINDEX получает индекс и возвращает 32-разрядное описание элемента палитры. Когда такое описание используется в контексте устройства, оно интерпретируется как элемент логической палитры контекста. Макросу PALETTERGB, как и макросу RGB, передаются значения красной, зеленой и синей составляющих. При использовании этих макросов в контексте устройства без системной (аппаратной) палитры их возвращаемые значения интерпретируются одинаково. Но если макрос PALETTERGB используется в контексте с системной палитрой, входящие в него значения составляющих RGB позволяют найти ближайшее совпадение в логической палитре устройства, словно приложение указало индекс в палитре. Реализация PALETTERGB в виде функции может выглядеть так: COLORREF PALETTERGBCHDC hDC. BYTE bRed. BYTE bGreen. BYTE bBlue): { COLORREF rslt = RGB(bRed. bGreen. bBlue): if ( GetDeviceCaps(hDC. RASTERCAPS) & RC_PALETTE) { HPALETTE hPal - GetCurrentObjectChDC. 0BJ_PAL): int indx = GetNearestPalettelndexChPal. rslt): return PALETTEINDEX(indx): } else return rslt: } Выше уже упоминалось о том, что в каждом контексте устройства (даже в режимах High Color и True Color) имеется логическая палитра. Из всех перечисленных функций обязательное присутствие аппаратной палитры необходимо лишь для работы RealizePalette; другие функции могут свободно использоваться и в режимах, не требующих палитру. В листинге 7.5 показано, как с помощью макроса PALETTEINDEX можно отобразить все цвета в логической палитре. Листинг 7.5. Вывод содержимого логической палитры void ShowLogicalPalette(HDC hDC. bool bHalftone) { HPALETTE hPalette = (HPALETTE) GetCurrentObjectChDC. 0BJ_PAL): if ( bHalftone ) hPalette - CreateHalftonePalette(hDC);
Цвет 413 PALETTEENTRY entry[256]; int num = GetPaletteEntries(hPalette. 0. 256. entry); HPALETTE hOld = SelectPalette(hDC. hPalette, FALSE); if ( GetDeviceCapsChDC. RASTERCAPS) & RC_PALETTE ) RealizePalette(hDC); for (int j=0; j<(num+15)/16; j++) for (int i=0; i<16 for (int y=0; y<24 for (int x=0; x<24 У++) x++) SetPixeKhDC. i*25+x. j*25+y. PALETTEINDEX(j*16+i)); SelectPaletteChDC. hOld. FALSE); if ( bHalftone ) DeleteObject(hPalette); } Эта программа работает как для текущей логической палитры, так и для полутоновой палитры. Функция GetPaletteEntries возвращает количество цветов в логической палитре; по ее возвращаемым значениям также можно вывести значения составляющих RGB каждого цвета. Затем программа реализует палитру, если это необходимо, и отображает ее содержимое рядами из 16 цветов при помощи макроса PALETTEINDEX. На рис. 7.6 показано расположение 256 цветов полутоновой палитры. Рис. 7.6. Полутоновая палитра, выведенная с использованием макроса PALETTEINDEX Палитра, создаваемая по умолчанию в контексте устройства, содержит всего 20 цветов — 16 цветов старого видеоадаптера VGA и еще 4 цвета, определяю-
414 Глава 7. Пикселы щих текущую цветовую схему Windows. Конечно, этого недостаточно даже для изображения среднего качества. Полутоновая палитра состоит из 256 цветов, равномерно распределенных в кубе RGB. Но если приложение захочет изобразить закат солнца, вероятно, ему понадобится больше теплых цветов, а холодные цвета окажутся лишними. Win32 содержит функции, позволяющие приложениям определять собственные палитры и управлять их взаимодействием с системной палитрой и другими приложениями системы, конкурирующими за общий ресурс системной палитры. Управление палитрой рассматривается после обсуждения обработки растров в GDI. Вероятно, область применения макросов RGB и PALETTEINDEX вам уже ясна. Макрос RGB лучше всего подходит для режимов High Color и True Color. Макрос PALETTEINDEX предназначен для режимов, требующих палитру; впрочем, он работает в режимах High Color и True Color при условии, что в контексте используется действительная логическая палитра. А что получится, если испытать макрос RGB в режиме с палитрой? Ничего хорошего. Слева на рис. 7.7 наш красивый RGB-куб изображен в 256-цветном режиме, причем результат не зависит от выбора в контексте устройства полутоновой палитры. Рис. 7.7. Цветовой куб RGB, выведенный в 256-цветном режиме с полутоновой палитрой (слева использован макрос RGB, справа — макрос PALETTERGB) Весьма своеобразный трехмерный объект, не правда ли? Однако это совсем не то, что требовалось. Для вывода куба, который на самом деле состоит из 3 х 256 х 256 разных цветов, используется всего 9 цветов! Чтобы улучшить качество изображения, необходимо создать, выбрать и реализовать в контексте устройства полутоновую палитру (см. листинг 7.5). Есть и другой, не менее важный аспект — заменить макрос RGB макросом PALETTERGB. В правой части рис. 7.7 видны волшебные последствия такой замены. Из рисунка видно, что куб, выведенный при помощи макроса PALETTERGB, не обладает идеальной симметрией. Это объясняется неравномерностью распределения цветов полутоновой палитры. Но даже улучшенный вариант по сравнению с версией для режима True Color смотрится весьма уродливо. Дело в том, что при выводе куба каждый пиксел
Вывод пикселов 415 выводится независимо от других, без полутоновой обработки всей поверхности. Чтобы улучшить результат без написания собственного алгоритма полутонирования, следует сохранить пикселы в 24-разрядном растре и воспользоваться командами вывода растров с полутоновой поддержкой. Нетривиальные возможности Даже после чтения всего десятка страниц можно уверенно сказать, что работа с цветом — непростая тема. В Win32 API предусмотрены дополнительные средства создания логических палитр и операции с системной палитрой, которые будут рассматриваться ниже в этой книге. Microsoft также предоставляет в ваше распоряжение специальный интерфейс API управления цветом — ICM 2.0, но эта тема выходит за рамки GDI. Полное описание возможностей ICM 2.0 в книге не приводится, хотя отображение и регулировка цветов будут дополнительно рассматриваться при подробном описании палитр в главе 13. Windows GDI поддерживает две разновидности цветовых пространств: цветовое пространство RGB и пространство индексов палитры, соответствующее базовым возможностям видеоадаптера. Поддерживаемый современными видеоадаптерами альфа-канал не является самостоятельным компонентом цветового пространства GDI. Он поддерживается только в DirectX и в новой функции GDI AlphaBlending. На рис. 7.8 показано, как 32-разрядная величина C0L0RREF представляется в форматах RGB, PALETTERGB и PALETTEINDEX. В GDI эти форматы различаются по первому байту 32-разрядного числа. 0 Red Green Blue RGB(Red, Green, Blue) 1 0 index PALETTEINDEX(index) 2 Red Green Blue PALETTERGB(Red, Green, Blue) | | | I | 32 бита 24 16 8 0 Рис. 7.8. Три способа задания цветов в GDI Вывод пикселов Функции вывода пикселов Win32 неоднократно встречались в этом разделе. После изложения теоретических обоснований пришло время для более точного и формального описания этих функций. В Win32 API предусмотрены следующие функции для работы с пикселами:
416 Глава 7. Пикселы COLORREF GetPixel (HDC hDC. int X. int Y); COLORREF SetPixelVCHDC hDC. int X. int Y. COLORREF crColor); COLORREF SetPixel (HDC hDC. int X. int Y. COLORREF crColor); В параметре hDC передается манипулятор контекста устройства. Прежде всего следует помнить, что не все устройства поддерживают работу с пикселами, а выражаясь точнее — не все драйверы устройств поддерживают непосредственные операции с пикселами. На уровне DDI не существует функции, которая бы обеспечивала вывод отдельных пикселов. Вместо этого GDI преобразует команды вывода пикселов в команды DDI, выполняющие блиттинг растров. Следовательно, в сомнительных случаях приложение должно проверить значение Get- DeviceCapsChDC, RASTERCAPS)&RC_BITBLT и узнать, поддерживается ли устройством блиттинг растров, частным случаем которого является вывод отдельного пиксела. Параметры (X,Y) определяют позицию пиксела в логической системе координат — мировой для расширенного графического режима или страничной для совместимого графического режима. Перед определением окончательной позиции пиксела координаты проходят мировое преобразование, отображение окна в область просмотра и отображение координат устройства в физические координаты. Непосредственный вывод пиксела также зависит от того, входит ли пиксел в фактическую область отсечения контекста устройства, также называемую регионом Рао. Границы региона Рао определяются пересечением системного региона, метарегиона и региона отсечения данного контекста. Если точка находится за пределами фактической области отсечения, возвращается код ошибки (CLRJNVALID, то есть OxFFFFFFFF, или FALSE для SetPixelV): Параметр crColor функций SetPixel и SetPixelV может существовать в трех разных формах. В нем может передаваться результат, возвращаемый макросом RGB для одного из цветов в 24-разрядном пространстве RGB. Если контекст относится к устройству без поддержки палитры (например, экрану в режиме High Color и True Color), значение RGB используется непосредственно или после небольшого усечения по размерам кадрового буфера. В противном случае графический механизм или драйвер устройства находит ближайший соответствующий цвет и связывает с пикселом индекс этого цвета в системной палитре. Если параметр crColor находится в формате PALETTEINDEX, то по логической палитре контекста находится индекс в системной палитре, который используется в качестве значения пиксела в кадровом буфере. Если параметр crColor находится в формате PALETTERGB, то для устройства без палитры результат будет тем же, как если бы параметр хранился в формате RGB; в противном случае в текущей логической палитре ищется ближайший цвет и пикселу присваивается результат поиска. Обратите внимание: в контекстах устройств с палитрой макросы RGB и PALETTERGB могут приводить к разным результатам. В документации Microsoft отсутствует четкое описание того, как цветовое значение в формате RGB преобразуется в индекс палитры, но, похоже, при этом используется палитра из 20 системных статических цветов. Цветовое значение, заданное при помощи макроса PALETTERGB, ищет совпадение в логической палитре контекста устройства, из которой можно выбрать гораздо больше цветов. Функции SetPixel и SetPixelV отличаются типом возвращаемого значения. Функция SetPixelV возвращает логическую величину — признак успешного за-
Вывод пикселов 417 вершения операции, а функция SetPixel возвращает реально использованный цвет. Функция GetPixel возвращает цветовое значение пиксела с заданными координатами. Следует помнить, что функции SetPixel и GetPixel возвращают результаты в формате RGB, а не в формате PALETTE INDEX или PALETTERGB, даже в контекстах устройств с поддержкой палитры. Это может вызвать проблемы в контекстах с палитрой. Например, приведенная ниже функция копирует пикселы в новую позицию. Если применить ее к кубу в правой части рис. 7.7, результат будет напоминать левый куб за исключением того, что в этом случае используется только 20 цветов. void CopyPixeKHDC hDC. int xO. int yO. int xl. int yl) { SetPixel(hDC. xl. yl. GetPixel(hDC. xl. yl)); } Чтобы эта функция нормально работала, последний параметр SetPixel должен иметь вид GetPixel (hDC, xl. yl)|PALETTERGBCO,0,0) или GetPixel(hDC, xl, yl)| 0x02000000. Разобраться в том, как реализованы функции вывода пикселов, особенно интересно, поскольку речь идет о самой простой графической операции. Кроме того, это поможет вам лучше понять, с какими затратами связано выполнение некоторых графических команд в GDI. В Windows NT/2000 обе функции, SetPixel и SetPixelV, обслуживаются одной и той же системной функцией _NtGdiSetPixel@16, вызываемой после проверки параметров по манипулятору контекста устройства. Имя _NtGdiSetPixel@16 означает, что при вызове функции передаются четыре параметра. При этом инициируется программное прерывание, которое обрабатывается одноименной функцией механизма GDI режима ядра (win32k.sys). Функция ядра NtGdiSetPixel блокирует контекст устройства, отображает логические координаты в физические, выполняет простую проверку границ, при необходимости преобразует C0L0RREF в индекс и вызывает функцию драйвера DrvBitBlt (если она поддерживается) или функцию EngBitBlt. При необходимости цветовое значение транслируется в формат RGB. Перед возвращением из функции контекст устройства разблокируется. Функция GetPixel обрабатывается системной функцией _NtGdiGetPixel@12. Эта функция создает временный растр и копирует в него пиксел вызовом DrvCopyBits/ EngCopyBits. Главное, на что необходимо обратить внимание в этой процедуре — это быстродействие. Для каждого вызова GetPixel, SetPixel или SetPixelV графическая система должна инициировать программное прерывание, переключиться в режим ядра и обратно, отобразить координаты, преобразовать индекс палитры, построить временный растр и затем вызвать функцию блиттинга драйвера устройства. Для одного пиксела объем работы получается довольно большим. В главе 1 была описана методика хронометража некоторых операций с использованием специальной инструкции процессора Pentium. Ею можно воспользоваться и для измерения быстродействия функций работы с пикселами. Некоторые результаты представлены в табл. 7.2.
418 Глава 7. Пикселы Таблица 7.2. Быстродействие функций работы с пикселами: Pentium 200 МГц Функция SetPixel(RGBO) SetPixeKPALETTERGBO) SetPixeKPALETTEINDEX) SetPixelV(RGBO) GetPixeK) Такты (256 цветов) 1850 4897 1345 1880 6362 Такты (32-разрядный цвет) 1286 1284 1295 1284 6499 Наивысшая скорость (пикселов/с) 152 881 153 374 153 115 153 115 30 251 Хронометраж показывает довольно интересные результаты. Во-первых, SetPixel и SetPixelV обладают похожим быстродействием, хотя документация Microsoft утверждает, что SetPixelV работает быстрее, поскольку ей не приходится возвращать цветовое значение. Преобразование данных RGB в индекс палитры — очень медленный процесс, на который тратится около 500 тактов, причем преобразование PALETTERGB в индекс полутоновой палитры обходится еще в 3000 тактов. Время обратного преобразования индекса палитры в RGB пренебрежимо мало, поскольку преобразование сводится к простой выборке элемента таблицы. Как ни странно, функция GetPixel работает гораздо медленнее SetPixel. В целом функции пикселов Win32 API работают очень медленно, и это вполне объяснимо, если учесть огромные затраты на обработку одного пиксела. Впрочем, если скорость от 30 до 150 тысяч пикселов в секунду вас устраивает, этими функциями можно пользоваться. Если понадобится более высокое быстродействие, операции с пикселами на растрах легко реализуются в коде C/C++ с использованием аппаратно-независимых растров или DIB-секций. В главах 10, 11 и 12 рассматриваются три разных типа растров, поддерживаемых GDI. Прямой доступ к пикселам позволяет обрабатывать миллионы пикселов в секунду. Пример: множество Мандельброта Множеством Мандельброта называется множество точек плоскости, определяемых простой итеративной формулой. Для точки (х, у) ее позиция на комплексной плоскости СО = х + yi определяет последовательность СО, С1, С2,...} Сп, где Сп+1 = Сп2 + СО. Точка (х}у) принадлежит множеству Мандельброта, если эта последовательность сходится (то есть элементы последовательности приближаются друг к другу и не уходят в бесконечность). Несмотря на простоту определения, не существует простой математической формулы, позволяющей проверить принадлежность точки (х,у) к множеству Мандельброта. Единственный способ проверки — выполнение итераций несколько сотен или даже тысяч раз. Самое интересное заключается в том, что само множество определяет количество итераций, необходимых для выяснения того, принадлежит ли точка этому множеству. Если раскрасить точки по числу необходимых итераций, получается очень затейливая и красивая картинка.
Пример: множество Мандельброта 419 Графическое представление множества Мандельброта невозможно точно описать массивом пикселов фиксированного размера. При увеличении части изображения не существует формулы, по которой можно было бы вычислить цвета открывшихся точек; вам придется снова провести итерации для нового уровня детализации. Следовательно, множество Мандельброта лучше всего представляется динамически сгенерированным массивом пикселов, хотя для ускорения вывода желательно воспользоваться растром. Вычисления занимают очень много времени, поэтому по практическим соображениям число итераций приходится ограничивать фиксированными величинами вроде 128, 1024 или 16 384. После проведения заданного количества итераций т возможно получение нескольких разных результатов. Если после п < m итераций расстояние от точки (х,у) от начала координат (0,0) больше 2, значит, последовательность уходит в бесконечность. Другой возможный вариант — если после р < m итераций выясняется, что последовательность сходится к одной фиксированной точке или перемещается между двумя, тремя, четырьмя и т. д. фиксированными точками в пределах допустимой погрешности. Третий вариант — когда после т итераций мы не можем принять обоснованного решения; элементы последовательности лежат в достаточно малом интервале, но критерий сходимости не выполняется. Мы можем раскрасить точки схождения одной последовательностью цветов в зависимости от значения п, точки расхождения — другой последовательностью цветов в зависимости от значения р, а неопределенные точки раскрасить одним фиксированным цветом. Цветовое пространство HLS хорошо подходит для построения цветовых последовательностей посредством изменения оттенка при фиксированных насыщенности и яркости или изменения яркости при фиксированных оттенке и насыщенности. На рис. 7.9 показано полное изображение множества Мандельброта после 128 итераций. На рис. 7.10 показана крошечная часть изображения после 1024 итераций с увеличением 64:1. Программа Mandelbrot создана на основе класса KScrollView, обеспечивающего базовые средства прокрутки и масштабирования. Прежде чем начинать длинные вычисления, реализация метода OnDraw запрашивает данные системного региона и проверяет, не отсекается ли текущая точка. Функция вычисления возвращает положительные числа для точек схождения, отрицательные числа для точек расхождения и 0 для неопределенных точек. Число итераций преобразуется в C0L0RREF по цветовым таблицам, после чего пиксел выводится функцией SetPixel. При рассмотрении других графических примитивов GDI будет показано, как повысить скорость вывода за счет нетривиальных графических команд. Пример программы Mandelbrot наглядно покажет, насколько хорошо вы разобрались в API режимах отображения, прокрутки, отсечения, цветовых пространств и вывода пикселов.
420 Глава 7. Пикселы Рис. 7.9. Полное множество Мандельброта (128 итераций) Рис. 7.10. Подмножество Мандельброта в увеличении 64:1 (1024 итерации)
Итоги 421 Итоги Главы 5 и 6 были посвящены контекстам устройств и координатным пространствам. В этой главе описаны другие базовые концепции, используемые в самой примитивной операции GDI — выводе отдельных пикселов. В главе 8 мы поднимемся на более высокий уровень и посмотрим, как пикселы соединяются в более интересные линии и кривые. В Windows GDI поддерживается специальный интерфейс API управления цветом — ICM (последняя версия — ICM 2.0). Описание ICM выходит за рамки этой книги. Дополнительную информацию можно найти в документации MSDN. Немало данных о цветовых пространствах и преобразованиях цветов можно найти в Интернете. Проведите поиск по таким ключевым словам, как «color space», «color-space conversion» и «color-space FAQ». Примеры программ К этой главе прилагается пять примеров программ (табл. 7.3). Таблица 7.3. Программы главы 7 Каталог проекта Описание Samples\Chapt_07\GDIObj Samples\Chapt_07\ClipRegion Samples\Chapt_07\ColorSpace Samples\Chapt_07\PixelSpeed Samples\Chapt_07\Mandelbrot Мониторинг таблицы объектов и получение информации об использовании объектов GDI процессом, позволяющей своевременно узнать о возможных утечках ресурсов Наглядное представление системного региона, ме- тарегиона и региона отсечения Демонстрация цветовых пространств RGB и HLS и цветов полутоновой палитры Хронометраж функций вывода пикселов Графическое представление множества Мандельб- рота с использованием функций вывода пикселов
Глава 8 Линии и кривые При соединении отдельных пикселов возникают линии и кривые. Следовательно, если вы умеете рисовать отдельные пикселы, рисование линий и кривых превращается в стандартную задачу из области математики и программирования. Впрочем, на практике линии и кривые могут обладать разными цветами, стилями, шириной, узором и прочими атрибутами, поэтому процедура рисования кривых бывает весьма нетривиальной. В этой главе рассматриваются некоторые концепции и средства GDI, связанные с рисованием линий и кривых, — бинарные растровые операции, режимы заполнения фона, перья, линии, кривые и траектории. Бинарные растровые операции При вызове функции SetPixel для вывода пиксела на поверхности устройства описатель цвета преобразуется в цветовой формат (физический цвет), соответствующий формату кадрового буфера, а затем значение соответствующего пиксела приемника заменяется преобразованным цветом. Если интерпретировать кадровый буфер как массив пикселов D, то операцию SetPixel можно рассматривать как присваивание D[x,y] = Р, где Р — преобразованный цвет (или физический цвет). Обобщая эту операцию, можно определить функцию /, объединяющую исходный цвет пиксела D[x,y] с цветом Р и порождающую новое цветовое значение, которое присваивается соответствующему пикселу приемника. Другими словами, D[x,y] = /(D[x,y],P) или D =/(D,P). Функция / получает два параметра, то есть является бинарной функцией. Теоретически число таких функций бесконечно, однако в GDI поддерживается лишь одна их разновидность: поразрядные логические операции. В этих операциях к битам двух аргументов, находящихся в одинаковых позициях, применяются логические операции. Для бинарных логических операций существует 16 бинарных функций (22х2). В GDI эти функции называются бинарны-
Бинарные растровые операции 423 ми растровыми операциями (binary raster operations), или сокращенно ROP2. В табл. 8.1 приведен перечень бинарных растровых операций, поддерживаемых в GDI. В данном случае буквой Р обозначается цвет пера, поскольку операции ROP2 используются для рисования линий. Таблица 8.1. Бинарные растровые операции ROP2 Формула Описание R2_BLACKPEN D = О Всегда 0, черный цвет в режиме RGB R2_N0TMERGEPEN D - ~(D | P) Инверсия R2_MERGEPEN R2J1ASKN0TPEN D = D&-P Конъюнкция приемника с инвертированным пером R2_N0TC0PYPEN D = ~Р Инверсия цвета пера R2MASKPENN0T D = P&-D Конъюнкция пера с инвертированным приемником R2_N0T D = ~D Инверсия приемника R2__X0RPEN D = DAP Приемник и перо объединяются операцией исключающего «ИЛИ» R2J0TMASKPEN D - ~(D&P) Инверсия R2J1ASKPEN R2MASKPEN D - D&P Конъюнкция приемника с пером R2J0TX0RPEN D - ~(DAP) Инверсия R2J0RPEN R2N0P D - D Приемник не изменяется R2_MERGEN0TPEN D = D|~P Дизъюнкция приемника с инвертированным пером R2_C0PYPEN D - Р Перо R2_MERGEPENN0T D - P|~D Дизъюнкция пера с инвертированным приемником R2_MERGEPEN D - P|D Конъюнкция пера с приемником R2_WHITE D = 1 Всегда 1, белый цвет в режиме RGB Контекст устройства GDI содержит атрибут, определяющий текущую операцию ROP2. Этот атрибут также называется режимом рисования (draw mode). Для получения текущего значения этого атрибута используется функция GetR0P2, а для присваивания ему нового значения — функция SetR0P2. Эти функции определяются следующим образом: int SetR0P2(HDC hDC. int fnDrawMode); int GetR0P2(HDC hDC); Функция SetR0P2 назначает в контексте устройства новую бинарную операцию (при условии передачи корректного значения) и возвращает исходный режим рисования. Функция GetR0P2 просто возвращает текущий режим рисования. По умолчанию в контексте устройства выбирается режим R2C0PYPEN, при котором пикселу приемника просто присваивается цвет пера. Режим рисования
424 Глава 8. Линии и кривые используется всюду, где используются перья, — то есть при выводе линий и кривых, а также при обводке заполненных областей. На рис. 8.1 показан эффект применения всех 16 бинарных растровых операций к 20 разноцветным полосам на экране в режиме True Color. Сначала программа рисует вертикальные полосы с применением цветов, взятых из палитры контекста устройства по умолчанию. Перед выводом каждой горизонтальной полосы происходит переключение бинарной растровой операции. Взгляните на рисунок; в режиме R2_BLACK фон закрашивается черным цветом, в режиме R2_N0T фон инвертируется, в режиме R2N0P фон не изменяется, а в режиме R2WHITE фон закрашивается белым цветом. В 256-цветном режиме цвета могут изменяться непредсказуемо, если только системная палитра не была специально подготовлена для последующего выполнения логических операций. R2_WHITE ■■■■■■■ тжжшт мм Рис. 8.1. Эффект от выполнения бинарных растровых операций (режим True Color) При использовании бинарных растровых операций следует помнить о том, что операции определяются для физических цветов, а не для логических значений C0L0RREF. Таким образом, результат операций является более или менее ап- паратно-зависимым. Для устройств, использующих цветовое пространство RGB, операции применяются к каждой из трех составляющих RGB, поэтому результат вполне предсказуем, но не всегда оправдан с точки зрения логики. В цветовой модели RGB хранятся значения интенсивности основных цветов, поэтому применение поразрядных логических операций не всегда находит соответствие в цветовом восприятии. Для устройств с палитрой растровые операции применяются к цветовым индексам, поэтому результат зависит от упорядочения цветов в палитре. В многозадачных ОС семейства Windows приложения не обладают полным контролем над аппаратной палитрой системы. В GDI существуют
Бинарные растровые операции 425 специальные функции, при помощи которых приложение вносит изменения в системную палитру, причем приоритетным правом обладают окна переднего плана. Обрабатывая специальные сообщения, приложение может реагировать на изменения в системной палитре. Палитры подробно рассматриваются в главе 13. Бинарные растровые операции играют важную роль в компьютерной графике. Режим R2BLACK используется для закраски пикселов черным цветом (0), а режим R2WHITE окрашивает пикселы в белый цвет (1, или OxFFFFFF в 24-разрядном кадровом буфере). Режим R2N0TC0PYPEN меняет цвет пера на противоположный. Режим R2N0P полностью подавляет вывод линий и кривых — это очень удобно, если вы не хотите обводить прямоугольник рамкой. Режим R2MASKPEN обеспечивает избирательное подавление битов на графической поверхности контекста устройства. Например, если режим R2MASKPEN используется для пера RGB(OxFF.O.O) в кадровом буфере RGB, при выводе линий данные синего и зеленого канала маскируются, в результате остаются только данные красного канала. При использовании цвета RGB(0x7F,0x7F,0x7F) подавляются яркие цвета, поскольку после вывода максимальная интенсивность каждого канала будет равна только 127 вместо 255. Режимы R2N0T и R2X0RPEN часто используются в интерактивной компьютерной графике для вывода перекрестий и эластичных контуров. Перекрестие состоит из горизонтальной и вертикальной линий, пересекающих весь экран. Точка пересечения этих линий определяет текущую позицию курсора. Перекрестия часто применяются при выравнивании объектов в графических редакторах. Эластичные контуры изменяются динамически и обозначают некие границы, определяемые пользователем при помощи мыши или клавиатуры. Эластичные прямоугольники и другие фигуры часто используются при построении и выделении геометрических фигур в графических пакетах. В процессе перемещения мыши построение фигуры считается еще не законченным, поэтому фиксировать фигуру нельзя. Вместо этого, когда пользователь перемещает мышь, приложение должно быстро нарисовать контур, стереть его, восстановить исходное содержимое и переместить в новую позицию. Бинарные операции R2_N0T, R2X0RPEN и R2N0TX0RPEN позволяют быстро рисовать временные линии и удалять их, не оставляя следа, поскольку при повторном применении этих операции восстанавливается исходное содержимое — одно из свойств логических операций. В листинге 8.1 показано, как реализовать перекрестие с использованием операции R2N0T. Класс окна хранит последнюю позицию курсора в переменных (m_lastx,m_lasty). Для каждого сообщения WMM0USEM0VE функция вывода перекрестия вызывается дважды — для удаления старых pi для рисования новых линий. Листинг 8.1. Вывод перекрестия с использованием R2_NOT void KMyCanvas::DrawCrossHair(HDC hDC. bool on) { if ( m_lastx<0 ) return; RECT rect; GetClientRect(m_hWnd. & rect); SetR0P2(hDC. R2 NOT); Продолжение х*>
426 Глава 8. Линии и кривые Листинг 8.1. Продолжение MoveToExChDC. rect.left. mjasty. NULL); LineToChDC. rect.right. m_lasty): MoveToExChDC. mjastx. rect.гор. NULL); LineTo(hDC. m_lastx. rect.bottom); } LRESULT KMyCanvas::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam) { switch( uMsg ) { case WM_CREATE: mjastx = mjasty = -1; // Перекрестия еще нет return 0: case WM_M0USEM0VE; { HDC hDC = GetDC(hWnd): DrawCrossHair(hDC); mjastx = L0W0RD(IParam); mjasty = HIWORDCIParam); DrawCrossHairChDC. true): ReleaseDCChWnd. hDC); } }: Хотя каждая из операций R2N0T, R2X0RPEN и R2N0TX0RPEN может использоваться для вывода временных, легко стираемых линий, между ними существуют незначительные отличия. Операция R2N0T инвертирует биты кадрового буфера, заменяет черный цвет белым, а белый цвет — черным. Таким образом, линия всегда видна, однако контролировать ее цвет вы не можете. Операция R2X0RPEN имеет более общий характер, чем R2_N0T. Чтобы добиться того же эффекта, как и при использовании R2_N0T, достаточно определить перо, физический цвет которого содержит 1 во всех битах. Другими словами, R2_X0RPEN с белым пером работает так же, как и R2_N0T. Если вы знаете, что фон клиентской области в основном состоит из одного цвета С, вы можете воспользоваться пером цвета Р и операцией R2X0RPEN, чтобы линия в основном была окрашена в цвет САР. Растровые операции бывают не только бинарными. Для растровых изображений в Win32 GDI определяются операции с тремя и четырьмя операндами, которые называются соответственно тернарными или кватернарными. В Windows 98 и Windows 2000 появилась простейшая поддержка нелогических операций смешения на уровне пикселов посредством альфа-наложения. Все эти возможности будут рассматриваться при знакомстве с растрами. Режим заполнения фона и цвет фона Для некоторых графических примитивов GDI делит выводимые пикселы на два класса: основные (foreground) и фоновые (background). Например, при выводе текста пикселы, образующие глифы символов, считаются основными, а остальные пикселы текстовой области считаются фоновыми. При выводе пунктирных
Перья 427 линий пикселы отрезков считаются основными, а пикселы промежутков — фоновыми. Основные пикселы выводятся всегда, а вывод фоновых пикселов необязателен. В каждом контексте устройства присутствует такой атрибут, как режим заполнения фона, управляющий выводом фоновых пикселов. При выводе фоновых пикселов задействуется цвет, заданный другим атрибутом контекста устройства — цветом фона. Для работы с этими атрибутами в GDI используются следующие функции: int GetBkMode(HDC hDC): int SetBkMode(HDC hDC. iBkMode); COLORREF GetBkColor(HDC hDC); COLORREF SetBkColorCHDC hDC. COLORREF crColor); Допустимыми значениями режима заполнения фона являются константы OPAQUE (пикселы фона выводятся) и TRANSPARENT (пикселы фона игнорируются). По умолчанию в контексте устройства используется режим OPAQUE с белым цветом фона. Режим заполнения и цвет фона требуются при выводе стилевых линий, текста и рисовании штриховой кистью. Цвет фона также используется при преобразовании растров между цветным и черно-белым форматом. Перья На вывод линий влияют многочисленные атрибуты. Некоторые атрибуты, относящиеся именно к линиям, группируются в объект пера GDI, что позволяет хранить и легко ссылаться на группы параметров. Говоря точнее, объект пера в Win32 GDI содержит информацию о толщине линии или кривой, ее стиле, цвете, концах, типе соединений и узоре. Толщина пера определяет толщину нарисованных линий. Линии толщиной в один пиксел хорошо подходят для вывода на экран и являются самыми тонкими линиями, используемыми в инженерной графике. Линии с фиксированной физической толщиной нужны для того, чтобы напечатанные документы одинаково выводились на принтерах с разными разрешениями. Стиль пера определяет тип выводимой линии — однородная, точечная, пунктирная или линия с пользовательским стилем. При определении пера обычно указывается однородный цвет, которым рисуются все пикселы линии, однако Win32 GDI также позволяет выводить узорные линии (узор определяется объектом кисти). Атрибуты завершения и типа соединения описывают внешний вид обоих концов линии и точек соединения отрезков. Объект логического пера GDI позволяет создавать объекты перьев (а точнее, объекты логических перьев). Логическое перо представляет собой описание требований к перу со стороны приложения, которое может в каких-то деталях и не соответствовать тому, как линии будут выводиться на поверхности физического устройства. Драйвер графического устройства можс^т поддерживать собственные структуры данных,
428 Глава 8. Линии и кривые определяющие реализацию логического пера; такие внутренние объекты называются физическими перьями. Структура данных логического пера находится под управлением GDI вместе с остальными логическими объектами — контекстами устройств, логическими кистями, логическими шрифтами и т. д. Манипулятор созданного пера возвращается приложению и требуется для ссылок на перо при его использовании в будущем. Манипуляторы объектов GDI описываются общим типом HGDIOBJ; для манипуляторов логических перьев зарезервирован тип HPEN. При определении макроса STRICT GDI использует файлы, в которых HGDIOBJ определяется как указатель на void, a HPEN и другие специализированные типы указателей — как указатели на абсолютно разные структуры. Таким образом, типом HPEN можно заменить тип HGDIOBJ, но попытка использования HGDIOBJ вместо HPEN требует обязательного преобразования типа. Если макрос STRICT не определен, все типы манипуляторов объявляются как указатели на void, поэтому компилятор не отвечает за неправильное использование типов манипуляторов. Объект логического пера является одним из атрибутов контекста устройства. В отличие от других атрибутов (таких, как режим заполнения фона или бинарная растровая операция), для работы с этим атрибутом используются общие функции работы с объектами. HGDIOBJ GetCurrentObjectCHDC hDC. UINT uObjectType); HGDIOBJ SelectObjectCHDC hDC, HGDIOBJ hgdiobj); int GetObjectCHGDIOBJ hgdiobj. int cbBuffer, LPVOID IpvObject); int EnumObjectsCHDC hDC. int nObjectType. GOBJENUMPROC lpObjectProc. LPARAM IParam); Функция GetCurrentObject возвращает манипулятор текущего объекта GDI в контексте устройства; тип объекта определяется параметром uObjectType. Например, GetCurrentObject (hDC, 0BJPEN) возвращает манипулятор текущего объекта логического пера. Функция SelectObject заменяет манипулятор объекта GDI в контексте манипулятором нового объекта и возвращает старый манипулятор. Таким образом, если при вызове SelectObject был указан манипулятор действительного объекта логического пера, функция SelectObject изменяет значение атрибута логического пера в контексте устройства. Функция GetOb ject возвращает исходное определение объекта GDI. Функция EnumObjects вызывает заданную функцию для каждого объекта GDI, доступного для пользователей, в контексте устройства. Объект логического пера, как и другие объекты GDI, поглощает ресурсы пользовательского пространства и ресурсы ядра, а также занимает место в таблице объектов GDI. Следовательно, когда необходимость в логическом пере отпадает, его следует исключить из контекста устройства и уничтожить функцией Delete- Object. По тем же причинам приложение не должно создавать слишком большого количества объектов GDI и не должно уничтожать их лишь при выходе из приложения. В системах Win32 все процессы в системе используют общую таблицу, рассчитанную на 16 384 манипуляторов GDI. Следовательно, если приложение создает 1024 манипуляторов объектов GDI, в системе одновременно может работать не более 16 таких приложений. Впрочем, при завершении процесса операционная система удаляет все созданные им объекты GDI и освобождает все занимаемые ресурсы.
Перья 429 Стандартные перья В GDI определяются четыре стандартных объекта перьев, которые могут использоваться любыми приложениями. Чтобы получить манипулятор стандартного пера, вызовите функцию GetStockObject с индексом стандартного объекта. Например, GetStockObject (BLACKPEN) возвращает однородное черное перо толщиной в один пиксел, также назначаемое по умолчанию в контексте устройства. Вызов GetStockObject (WHITEPEN) возвращает однородное белое перо толщиной в один пиксел. GetStockObject (NULL_PEN) возвращает пустое перо, которое ничего не рисует и может использоваться для временного запрета вывода линий. Эти стандартные перья (черное, белое и пустое) существуют уже давно. В Windows 98/2000 наконец появилось новое стандартное перо — перо DC, возвращаемое вызовом GetStockObject(DC_PEN). Перо DC, или выражаясь шире — объект GDI контекста устройства, является абсолютно новой концепцией. Обычные объекты GDI «намертво» фиксируются при создании. Их можно использовать, можно удалять, но нельзя изменять. Если вам понадобился слегка отличающийся объект GDI, приходится создавать новый объект и удалять старый. При большом количестве объектов GDI это приводит к снижению быстродействия (например, при реализации градиентных заливок без прямой поддержки со стороны GDI). Перо DC принадлежит к новому типу объектов GDI и является лишь одним из частных случаев. Можно называть эти объекты объектами GDI контекста устройства, поскольку они имеют особый смысл лишь при присоединении к контексту устройства. После выбора в контексте устройства такие объекты можно до определенной степени модифицировать. Если такие изменения поддерживаются GDI, вы избавляетесь от необходимости создавать новые или заменять старые объекты. По умолчанию перо DC является однородным черным пером толщиной в один пиксел. После выбора пера DC в контексте устройства вы можете изменять только его цвет. При работе с цветом пера DC используются следующие функции: C0L0RREF GetDCPenColorCHDC hDC); C0L0RREF SetDCPenColorCHDC hDC, C0L0RREF crColor); Функция GetDCPenColor возвращает текущий цвет пера DC в контексте устройства. Функция SetDCPenColor устанавливает новый цвет пера и возвращает старый цвет пера. Эти функции могут использоваться даже в том случае, если перо DC не выбрано в контексте устройства. Цвет пера DC можно рассматривать как новый атрибут контекста устройства, требуемый для рисования линий только в том случае, если перо DC выбрано в качестве текущего объекта пера. Следующий фрагмент показывает, как нарисовать градиентную заливку всего одним пером: HGDIOBJ hOld = SelectObjectChDC. GetStockObject(DC_PEN)); for (int i=0; i<128; i++) { SetDCPenColor(hDC. RGB(i. 255-i. 128+i)): MoveToEx(hDC, 10. i+10. NULL): LineToChDC. 110. i+10): } SelectObjectChDC. hOld);
430 Глава 8. Линии и кривые После получения и выбора пера DC программа при помощи функции Set- DCPenColor постепенно изменяет цвет пера в интервале от RGB(0,255,128) до RGB( 127,128,255), создавая линейную градиентную заливку. После завершения вывода программа восстанавливает исходное перо. Без пера DC нам пришлось бы при каждой итерации создавать новое перо, выбирать его в контексте устройства и удалять старое перо. Стандартные объекты заранее создаются операционной системой pi совместно используются всеми процессами, работающими в системе. После завершения работы со стандартными объектами их манипуляторы удалять не нужно. Впрочем, вызов DeleteObject для манипулятора стандартного пера абсолютно безопасен — Del eteObject просто возвращает TRUE, не выполняя никаких действий. Простые перья Все стандартные перья имеют однородный цвет и единичную толщину. Чтобы рисовать прерывистые или более толстые линии, приложение создает нестандартные объекты логического пера. Ниже приведены две несложные функции для создания простых перьев: HPEN CreatePendnt fnPenStyle. int nWidth, COLORREF crColor): HPEN CreatePenlndirect(CONST LOGPEN * Iplgpn); Структура LOGPEN содержит три параметра логического пера, а именно его стиль, толщину и ссылку на цвет. Следовательно, эти две функции представляют собой разновидности одной и той же функции. В практической реализации CreatePenlndirect извлекает данные из структуры LOGPEN и вызывает функцию CreatePen. Стиль пера определяет порядок следования пикселов и расположение линии. В табл. 8.2 перечислены различные стили перьев и ограничения, накладываемые на их реализацию. Таблица 8.2. Простые стили перьев Стиль Вид линии Выравнивание Ограничения PS_S0LID PS_DASH PS_D0T PS_DASHDOT PS_DASHD0TD0T PS_NULL PSJNIDEFRAME Сплошная, рисуются все пикселы Пунктирная Точечная Чередование отрезков и точек Отрезок и две точки Линия не рисуется Сплошная, рисуются все пикселы По центру По центру По центру По центру По центру Нет Внутри контура Толщина <1 Толщина < 1 Толщина < 1 Толщина < 1 Толщина > 1, используется при обводке замкнутых областей
Перья 431 Одной из составляющих стиля пера является правило чередования отрезков и промежутков в нарисованной линии. Перья со стилями PSSOLID и PSINSIDEFRAME рисуют сплошные линии, а перо PSNULL вообще ничего не рисует, поэтому остается разобраться с 4 оставшимися стилями. Пунктирное перо PSDASH состоит из отрезков длиной 18 пикселов, разделенных промежутками в 6 пикселов. Точечное перо PSD0T состоит из отрезков длиной 3 пиксела, разделенных промежутками в 3 пиксела. Пунктирно-точечное перо PS_DASHDOT строится по правилу «отрезок 9 пикселов, промежуток 6 пикселов, отрезок 3 пиксела, промежуток 6 пикселов». Для перьев PS_DASHD0TD0T используется цикл «отрезок 9 пикселов, промежуток 3 пиксела, отрезок 3 пиксела, промежуток 3 пиксела, отрезок 3 пиксела, промежуток 3 пиксела». Вероятно, в Microsoft решили, что один пиксел слишком мал, поэтому одна точка на линии представляется тремя пикселами. На рис. 8.2 показаны циклы чередования пикселов в стилях, перечисленных в табл. 8.2. В левом столбце указано название стиля, в среднем — пример линии, нарисованной пером этого стиля, а справа та же линия изображена в увеличении. Линии на рисунке выведены в режиме заполнения OPAQUE темным пером на светлом фоне. Как видно из рисунка, перо PSNULL не выводит ничего, даже пикселов фона. Если толщина линии составляет один пиксел в системе координат устройства, перо PSINSIDEFRAME эквивалентно PSSOLID. В линиях других стилей наглядно прослеживается цикл чередования пикселов. PS_SOLID PS_DASH PS_DOT PS_DASHDOT PS_DASHDOTDOT PSJMULL PSJNSIDEFRAME ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ Рис. 8.2. Стили простых перьев Второй параметр CreatePen определяет толщину линии в логической системе координат. Фактическая толщина линии в физических координатах зависит от мировых преобразований и от отображения окна в область просмотра. Например, в режиме MMLOENGLISH с тождественным мировым преобразованием перо с логической толщиной 10 всегда рисует линию, толщина которой на принтере близка к 0,1 дюйма независимо от разрешения. Одна десятая дюйма соответствует 30 пикселам на принтере с разрешением 300 dpi или 120 пикселам на принтере с разрешением 1200 dpi. Перо толщины 0 интерпретируется особым образом; оно всегда рисует линию толщиной 1 пиксел в физических координатах. Если физическая толщина пера превышает 1 пиксел, то перо, созданное функцией CreatePen, не рисует полноценные стилевые линии — например, пунктирные и точечные линии. Вместо этого рисуются только однородные линии с большей толщиной. Другими словами, логическое перо, созданное функций CreatePen, позволяет рисовать стилевые линии лишь толщиной в один пиксел. Увеличение толщины перьев приводит к усложнению вида нарисованных линий. Каждая линия определяется своей базовой осью. Предположим, гори- ■■■■■■■■■■■■■■■■■■шшШШШшМШВВВ шшшшшшшшшшшшшшшшшшшшшшшшшшшшшш — шшшшшшшшшшшшшшшшшшшшшшшшшшшшшш — шшшшшшшшшшшшшшштттшшштттшшшшшш пппппппппппппппппппппппппппппп
432 Глава 8. Линии и кривые зонтальная линия определяется двумя точками (0,0) и (100,0). Если толщина линии равна одному пикселу, вполне очевидно, что в линию должны входить точки (1,0), (2,0) и т. д. до (99,0). Но если толщина линии составляет 5 пикселов, эту линию можно нарисовать несколькими разными способами. Первый вопрос — как пикселы линии должны располагаться по отношению к базовой оси? Для всех перьев, кроме перьев со стилем PSINSIDEFRAME, нарисованная линия центруется относительно своей базовой оси. Перья со стилем PSINSIDEFRAME используются при обводке некоторых фигур GDI (прямоугольников простых и с закругленными углами, эллипсов и т. д.). В этом случае центр линии смещается внутрь области таким образом, чтобы линия не выходила за границы контура. Чтобы нарисовать линию внутри контура, GDI необходимо точно знать, какая из двух сторон линии является внутренней, а какая — внешней. Следовательно, при выводе обычных линий и даже многоугольников перо со стилем PSINSIDEFRAME рисует обычную сплошную линию, центрованную по базовой оси, поскольку GDI не знает, какая сторона является внутренней. Стиль PSINSIDEFRAME активизируется лишь при рисовании определенных фигур с четко различаемой внутренней и внешней частью. Не каждую линию удается точно отцентрировать по базовой оси. Только в вертикальных и горизонтальных линиях с нечетной толщиной один пиксел находится в центре, а остальные равномерно распределяются по обе стороны от него. При выводе утолщенных линий возникает и другой вопрос — как должны выглядеть концы линий? Для перьев, созданных функцией CreatePen, линии всегда заканчиваются полукруглыми концами. w=0 w=2 w=4 w=6 w=8 w=10 w=1 w=3 w=5 w=7 w=9 w=11 Рис. 8.З. Концы линий разной толщины На рис. 8.3 изображены концы линий со стилем PSD0T, которые были нарисованы перьями разной толщины, созданными функцией CreatePen. Если физическая толщина равна 0 или 1, нарисованная линия действительно состоит из точек. С увеличением толщины линии по обеим сторонам базовой оси (светлые
Перья 433 точки) появляются дополнительные пикселы (темные точки), образующие утолщенные линии и закругленные концы. Обратите внимание: только при нечетной толщине линия симметрично центруется относительно базовой оси. ПРИМЕЧАНИЕ В Windows 95/98 утолщенные линии рисуются не так, как показано на рисунке, полученном в Windows 2000. Пикселы не распределяются равномерно по двум сторонам базовой оси, а завершение не имеет нормальной полукруглой формы. Расширенные перья Если разобраться со всеми ограничениями, становится ясно, что простые перья, созданные функциями CreatePen или CreatePenlndirect, существуют только в двух формах: стилевое перо с толщиной один пиксел и утолщенное перо с круглым завершением. Стиль PSINSIDEFRAME из-за своей реализации в Windows GDI особой пользы не приносит, поскольку он может применяться лишь для некоторых фигур, вписанных в прямоугольник. Однако для этих фигур приложение может легко добиться того же эффекта при помощи обычного пера, слегка уменьшив ограничивающий прямоугольник. Для преодоления этих ограничений в Win32 API появилась новая функция ExtCreatePen, создающая расширенные перья с обогащенным набором атрибутов. HPEN ExtCreatePen(DWORD dwPenStyle. DWORD dwWidth, CONST LOGBRUSH * lplb. DWORD dwStyleCont. CONST DWORD * IpStyle): Функция ExtCreatePen позволяет создавать перья 2 типов, 9 стилей, с 3 видами завершений и 3 видами соединений. Вся эта информация определяется одним параметром dwStyle в виде комбинации флагов из табл. 8.3, объединенных поразрядным оператором ИЛИ (|). Таблица 8.3. Типы, стили, завершения и соединения расширенных перьев Флаг Смысл Ограничения Типы PSC0SMETIC Косметическое перо. Толщина равна одному пикселу PSGE0METRIC Геометрическое перо. Толщина пера задается в логических единицах Стили PSS0LID Непрерывная линия, рисуются все пикселы. Выравнивание по центру PSDASH Пунктирная линия. Выравнива ние по центру В Windows 95/98 не поддерживаются геометрические перья с этим стилем Продолжение #
434 Глава 8. Линии и кривые Таблица 8.3. Продолжение Флаг Смысл Ограничения PSD0T Точечная линия. Выравнивание по центру PSDASHDOT Пунктирно-точечная линия. Выравнивание по центру PSDASHD0TD0T Отрезок и две точки. Выравнивание по центру PS_NULL Линия не рисуется PS_INSIDEFRAME Непрерывная линия, рисуются все пикселы. Выравнивание внутри контура PSUSERSTYLE Чередование отрезков и промежутков задается параметрами dwStyl eCount и IpStyle. Выравнивание по центру PS_ALTERNATE Чередование «пиксел — промежуток» Только геометрические перья, используется лишь при обводке некоторых фигур GDI Поддерживается только в Windows NT/2000 Поддерживается только в Windows NT/2000 и только для косметических перьев Завершение PS_ENDCAP_ROUND PS ENDCAP SQUARE Закругленное завершение (к линии добавляется половина круга) Квадратное завершение (к линии добавляется половина квадрата) PSENDCAPFLAT Плоское завершение Только для геометрических перьев Только для геометрических перьев Только для геометрических перьев Соединение PS_JOIN_BEVEL PS_JOIN_MITER PS JOIN ROUND Усеченное соединение Заостренное соединение Закругленное соединение Только для геометрических перьев Только для геометрических перьев Только для геометрических перьев Расширенные перья делятся на два типа: косметические и геометрические. Параметр dwWidth определяет толщину пера. Для косметических перьев толщина может быть равна только 1. Для геометрических перьев толщина задается в логических координатах, поэтому фактическая толщина линий зависит от мировых преобразований и отображения окна в область просмотра. Расширенные перья используют структуру L0GBRUSH для определения цветов и узоров. В косметических перьях допускаются только однородные цвета и сплошная заливка, а геометрические перья допускают наличие смешанных цветов и различных узор-
Перья 435 ных кистей. Два последних параметра ExtCreatePen определяют пользовательское правило чередования пикселов в перьях стиля PSJJSERSTYLE. Косметические перья Косметические перья всегда рисуют линии толщиной в один пиксел. Хотя в MSDN утверждается, что косметические перья обладают произвольной шириной, задаваемой в системе координат устройства, единственным допустимым значением параметра dwWidth является 1; при любых других значениях вызов функции завершается неудачей. В Windows NT/2000 появились два новых стиля: PSJJSERSTYLE и PS_ALTERNATE. Стиль PS_ALTERNATE может использоваться только в косметических перьях для создания «настоящих» точечных линий. Стиль PSJJSERSTYLE позволяет приложению определять собственные последовательности чередования пикселов (биты стиля). Для создания пера с пользовательским стилем необходимы два дополнительных параметра, dwStyl eCount (тип DWORD) и IpStyle (массив DWORD). Первый элемент массива содержит длину первого отрезка, второй — длину промежутка, третий — длину второго отрезка и т. д. При этом одна единица соответствует трем пикселам вместо одного. Следовательно, пользовательский стиль позволяет имитировать стили PSJ3ASH, PSJDOT, PSJDASHDOT и PSJ3ASHD0TD0T, но не стиль PS_ ALTERNATE. В приведенном ниже фрагменте создается косметическое перо с циклом чередования {4,3,2,1} — то есть отрезок из 12 пикселов, промежуток из 9 пикселов, отрезок из 6 пикселов и промежуток из 3 пикселов. const DWORD cycle[4] ={4.3.2.1}: const LOGBRUSH brush = {BS_S0LID. RGB(O.O.OxFF). 0 }: HPEN hPen - ExtCreatePen(PS_COSMETIC | PSJJSERSTYLE. 1. & brush. sizeof(cycle)/sizeof(cycle[0]). cycle): На первый взгляд кажется, что косметические перья аналогичны простым перьям, созданным функцией CreatePen с шириной 0. Однако у них имеется одно важное недокументированное отличие (а может, дефект реализации): косметические перья всегда рисуют в прозрачном режиме. Другими словами, даже при включении режима заполнения фона OPAQUE фоновые пикселы в промежутках не выводятся. Стили косметических перьев показаны на рис. 8.4. Для линий пользовательского стиля используется цикл чередования {4,3,2,1}. Обратите внимание: PS_INSIDEFRAME является недопустимым стилем косметического пера, который не преобразуется автоматически к стилю сплошной линии. PS_SOLID PS_DASH PS_DOT PS_DASHDOT PS_DASHDOTDOT PS_NULL PSJNSIDEFRAME PSJJSERSTYLE PS ALTERNATE ■DDDDnDI ■□□□■■■□□□■■■□□□■■■□□□■■■□□□ ■■■■■■«^□□□□□■■■□□□□□□■■■■И ■■■■■■■□□□■■■□□□■■■□□□■■■■■И ■■■■■■■■■■■■□□□□□□□□□■■■■■■□□□ ■□■□■□■□■□■□■□■□■□■□■□■□■□■ПИП Рис. 8.4. Стили косметических перьев
436 Глава 8. Линии и кривые Как подсказывает название, косметические перья хорошо подходят для рисования тонких линий, особенно на экране монитора. При выводе в контексте устройства принтера, обладающем повышенным разрешением, стилевые линии выглядят как более светлые сплошные линии, и даже сплошные линии видны лишь при высоком контрасте с цветом фона. Геометрические перья Геометрическое перо рисует линию «наконечником» в виде геометрической фигуры. Говоря точнее, геометрическое перо обладает переменной толщиной, стилем, завершением и соединением. Давайте рассмотрим эти атрибуты более подробно. Толщина геометрического пера задается в логических координатах, но в отличие от перьев, созданных функцией CreatePen, толщина геометрического пера не может быть равна 0. С реальной физической толщиной геометрического пера дело обстоит сложнее. В режиме ММТЕХТ с тождественным преобразованием одна логическая единица устройства преобразуется в одну физическую единицу, поэтому физическая толщина совпадает с логической. Если мировое преобразование и отображение окна в область просмотра используют одинаковый масштаб по обеим осям, толщина логического пера масштабируется в соответствии с заданным масштабным коэффициентом. Но если масштаб различается, вертикальные и горизонтальные линии, нарисованные одним и тем же геометрическим пером, будут иметь разную толщину. Это относится и к перьям, созданным функцией CreatePen. Линия, нарисованная геометрическим пером, рассматривается не как простая последовательность пикселов, а как геометрическая фигура. Например, линия (0,0)—(100,0), нарисованная пером толщины 10, по форме совпадает с прямоугольником, определяемым противоположными углами (0,0) и (100,10). Мировые преобразования и режим отображения распространяются на линию в той же степени, как и на прямоугольники. На рис. 8.5 изображены контрольные точки повернутой геометрической линии (х0,у0) - (х1,у1). dx = penwidth * sin (о)/2 dy = penwidth * cos(o)/2 (x0-dx,y0+dy) (x0,y0) (x1-dx,y1+dy) (x1,y1) [x1+dx,y1-dy) (x0+dx,y0-dy) Рис. 8.5. Геометрическая линия как геометрическая фигура
Перья 437 Атрибут завершения геометрической линии определяет вид «наконечников», добавляемых к обоим концам линии или ее внутренних отрезков. Линия, изображенная на рисунке, выводится без завершений, что соответствует стилю PS_ ENDCAPFLAT (плоское завершение). К обоим концам утолщенных линий, нарисованных простыми перьями, добавляются полукруглые наконечники (PS_ENDCAP_ ROUND). При квадратном завершении (PSENDCAPSQUARE) к обоим концам присоединяются половины квадратов. В Windows NT/2000 для геометрических перьев реализованы все стили кроме стиля PS_ALTERNATE, зарезервированного для косметических линий. В отличие от простых перьев, созданных функцией CreatePen, утолщенные геометрические линии рисуются в соответствии со своим стилем и не преобразуются в сплошные линии. Геометрические перья не изображают одну точку тремя пикселами; вместо этого размер точки или отрезка масштабируется вместе с толщиной линии. При этом одна точка изображается одним пикселом, как при использовании косметических линий со стилем PS_ALTERNATE. С утолщением пера увеличиваются и размеры точек. Каждый отрезок или точка оформляются соответствующими завершениями. Следовательно, отрезки пунктирной линии могут заканчиваться плоскими, круглыми или квадратными завершениями. К сожалению, в Windows 95/98 реализация Win32 GDI API выглядит несколько иначе. В этих системах, основанных на 16-разрядных версиях GDI, утолщенные геометрические перья реализуются в виде сплошных линий. Геометрические перья уже не ограничиваются одним сплошным цветом. Функции ExtCreatePen передается структура L0GBRUSH, содержащая информацию о цвете, стили кисти и стиле штриховки. Структура L0GBRUSH обычно используется для определения логических кистей, предназначенных для заливки фигур. Однако геометрические линии принципиально не отличаются от геометрических фигур, поэтому вполне естественно, что GDI позволяет закрашивать их кистями. На рис. 8.6 изображены геометрические линии с разными завершениями, стилями и узорами. Обратите внимание: стиль PSALTERNATE для геометрических линий считается недействительным, а стиль PSNULL ничего не рисует. В отличие от косметических перьев геометрические перья рисуют линии в прозрачном режиме, игнорируя цвет и режим заполнения фона (с одним исключением — при работе со штриховой кистью эти атрибуты используются для закраски фона между штрихами). Также стоит обратить внимание на то, что величина промежутков в пользовательском стиле задается в пикселах, а не в логических единицах, которые изменяются вместе с толщиной пера. С утолщением пера или с добавлением завершений отрезки в линиях пользовательского стиля могут «наползать» друг на друга. Стыки утолщенных линий с завершениями могут выглядеть не так, как можно было бы предположить. На рис. 8.7 показано, как выглядит буква Z, состоящая из трех утолщенных линий с разными завершениями. Тонкие белые линии обозначают положение базовых осей. На рисунке видны завершения, созданные стилями PS_ENDCAP_SQUARE и PS_ENDCAP_ROUND. Плоские и квадратные завершения стыкуются неровно. Хотя линии с круглыми завершениями стыкуются нормально, при использовании режима R2_X0RPEN общие части линий будут отличаться по цвету, поскольку они прорисовываются дважды.
438 Глава 8. Линии и кривые w=3, flat PS_SOLID PS_DASH PS_DOT PS_DASHDOT PS_DASHDOTDOT PS_NULL PSJNSIDEFRAME PSJJSERSTYLE — - - PS ALTERNATE w=7, flat w=11, square w=15, round, hatch 0 # # m ф т m m ■« m # Рис. 8.6. Геометрические линии с различной толщиной, завершениями, штриховкой и стилями PS ENDCAP FLAT PS ENDCAP SQUARE PS ENDCAP ROUND PS_ENDCAP_ROUND R2 XORPEN Рис. 8.7. Соприкосновение линий с разными завершениями Для обеспечения плавной стыковки линий GDI позволяет объединить несколько линий и кривых в один графический вызов. Если геометрическое перо используется для рисования линии или кривой, состоящей из нескольких сегментов, способ стыковки сегментов определяется специальным атрибутом — соединением. Существует три типа соединений. Усеченное соединение строится по форме двух сегментов с плоскими соединениями; к стыку добавляется треугольник, заполняющий впадину. Заостренное соединение строится аналогичным образом, но в этом случае линии продолжаются до точки пересечения. Закругленное соединение выглядит так же, как и при стыковке двух линий с круглыми завершениями. На рис. 8.8 изображена та же Z-образная фигура, нарисованная функцией Polyline GDI, позволяющей за один вызов нарисовать несколько линий с разными типами соединений. Кроме улучшения внешнего вида соединений, функция Polyline рисует каждый пиксел только один раз, поэтому даже при использовании операции R2_X0RPEN вся линия будет нарисована одним цветом. PS_ENDCAP_FLAT PS JOIN BEVEL PS_ENDCAP_SQUARE PS JOIN MITER PS_ENDCAP_ROUND PS JOIN ROUND PS_ENDCAP_ROUND PS_JOIN_ROUND R2 XORPEN Рис. 8.8. Типы соединений при использовании функции Polyline
Перья 439 При стыковке линий под острыми углами длина заостренного соединения может быть очень большой. Чтобы избежать чрезмерного удлинения стыков, GDI позволяет приложению ограничить длину заостренного соединения при помощи функции SetMiterLimit: BOOL SetMiterLimitCHDC hDC. FLOAT eNewLimit. PFLOAT peOldLimit); Функция SetMiterLimit определяет угловой лимит — максимальное отношение длины заострения к толщине линии. На второй фигуре слева (см. рис. 8.8) длина заострения равна расстоянию от пересечения внешних границ линии до пересечения внутренних границ. Толщина пера равна расстоянию между двумя пересечениями по оси у. По умолчанию угловой лимит в контекстах устройств равен 10,0. На рисунке отношение равно примерно 4,35. Если отношение длины заострения к толщине линии превышает угловой лимит, вместо заостренного соединения используется усеченное. Если вы предпочитаете математический подход, то при пересечении двух линий под углом 9 угловое отношение определяется выражением l/sin(9/2), независящим от толщины пера. В приведенном выше примере ширина Z-образной фигуры вдвое превышает ее высоту, поэтому sin(0) = 1/V5, sin(8/2) = 0,229753, а угловое отношение равно 4,352502. Получение информации о логических перьях По известному манипулятору объекта логического пера приложение может узнать тип пера и получить его описание при помощи двух общих функций: DWORD GetObjectTypeCHGDIOBJ h); int GetObjectCHGDIOBJ hgdiobj. int cbBuffer. LPVOID IpvObject); Функция GetObjectType возвращает идентификатор типа объекта GDI. Для простого пера возвращается константа 0BJPEN; для расширенного пера возвращается 0BJEXTPEN. Функция GetObject заполняет буфер определением объекта GDI. Для простых перьев заполняется структура LOGPEN; а для расширенных — структура EXTLOGPEN. Структура LOGPEN имеет фиксированный размер, поэтому для простого пера функция GetObject работает просто. Однако структура EXTLOGPEN имеет переменную длину из-за массива описания стиля, поэтому функцию GetObject приходится вызывать дважды. При первом вызове определяется точный размер структуры EXTLOGPEN, а при втором вызове заполняется выделенный блок памяти нужного размера. В Win32 API подобные двухшаговые вызовы встречаются довольно часто. Приведенный ниже фрагмент показывает, как использовать эти две функции для заполнения структуры LOGPEN или EXTLOGPEN в зависимости от типа манипулятора объекта пера. LOGPEN logpen; EXTLOGPEN * pextlogpen - NULL; int size = 0; switch ( GetObjectType(hPen) ) { case 0BJ_PEN: GetObject(hPen, sizeof(logpen), & logpen); break;
440 Глава 8. Линии и кривые case OBJJXTPEN: size = GetObject(hPen. 0, NULL); pextlogpen = (EXTLOGPEN *) new char[size]; GetObjectChPen. size, pextlogpen); break; default: } if ( pextlogpen ) { delete [] (char *) pextlogpen; pextlogpen = NULL; } Класс для работы с объектами перьев GDI Чтобы воспользоваться нестандартным объектом пера, необходимо создать его и выбрать в контексте устройства; после использования объект исключается из контекста и удаляется. Сделать это несложно, но не слишком интересно. Ниже приведен простой класс КРеп C++, предназначенный для работы с простыми перьями и обычными расширенными перьями. // Класс для выбора объектов GDI class KSelect { HGDIOBJ mJiOld; HDC mJiDC: public: void Select(HDC hDC. HGDIOBJ hObject) { if ( hDC ) { m_hDC = hDC: mJiOld - SelectObject(hDC. hObject); } else { m_hDC - NULL; m_h01d - NULL; } } void UnSelect(void) { if ( m_hDC ) { SelectObject(m_hDC. m_h01d); mJiDC = NULL; mJiOld - NULL; } } }:
Перья 441 // Класс для работы с объектами перьев class KPen : public KSelect { public: HPEN m_hPen; KPen(int style, int width, COLORREF color, HDC hDC=NULL) { m_hPen = CreatePen(style, width, color); Select(hDC); } KPen(int style, int width. COLORREF color, int count. DWORD * gap. HDC hDC=NULL) { LOGBRUSH logbrush = { BSJOLID. color. 0 }; m_hPen = ExtCreatePenCstyle, width. & logbrush. count, gap); Select(hDC); } void Select(HDC hDC) { KSelect::Select(hDC. m_hPen); } -KPenО { UnSelectO: DeleteObject(m_hPen): } }: Класс KPen содержит два конструктора. Первый конструктор создает простые перья, а второй — расширенные перья без узорной кисти. В принципе можно написать еще один конструктор, который бы создавал расширенные перья с узорной кистью. Оба конструктора получают необязательный параметр — манипулятор контекста устройства. Если этот параметр задан, манипулятор созданного пера GDI выбирается в заданном контексте устройства. Деструктор исключает перо из контекста, если оно все еще остается выбранным, и удаляет объект. Два дополнительных метода предназначены для явного выбора и исключения манипулятора пера из контекста. Применять класс KPen чрезвычайно просто. Если в некотором фрагменте используется всего одно перо, заключите экземпляр класса KPen в соответствующий блок, передайте конструктору манипулятор контекста устройства, и в дальнейшем создание объекта GDI, его выбор в контексте, исключение и удаление будут выполняться автоматически. Если фрагмент программы работает с несколькими перьями, не передавайте манипулятор контекста конструктору; вместо этого в нужные моменты следует вызывать методы Select и UnSelect. Рассмотрим несколько простых примеров. { KPen red(PS_SOLID. 1. RGBCOxFF. 0. 0). hDC); // Рисовать красным пером
442 Глава 8. Линии и кривые // При выходе из этого блока перо автоматически // исключается из контекста и уничтожается } { KPen red (PS_S0LID. 1. RGB(0xFF, 0. 0)); KPen green (PS_S0LID, 1. RGB(0. OxFF. 0)); red.Select(hDC); // Рисовать красным пером red.UnSelect(hDC): green.Select(hDC): // Рисовать зеленым пером green.UnSelect(hDC): // При выходе из блока оба пера автоматически удаляются } Линии После того как в контексте устройства выбран действительный манипулятор объекта логического пера, можете использовать следующие функции для рисования прямых линий (по отдельности или нескольких сразу), а также для подготовки к рисованию линий: BOOL MoveToEx(HDC hDC. int X, int Y. LPPOINT IpPoint); BOOL LineTo(HDC hDC. int nXEnd, int nYEnd); BOOL PolylineTo(HDC hDC. CONST POINT * Ippt. DWORD cCount): BOOL Polyline(HDC hDC. CONST POINT * Ippt. int cPoints): BOOL PolyPolyline(HDC hDC. CONST POINT * Ippt. CONST DWORD * lpdwPolyPoints. DWORD nCount); Функция MoveToEx не рисует линий — она всего лишь перемещает текущую позицию пера в контексте устройства в точку с заданными координатами. Исходная позиция возвращается в параметре IpPoint. Такие функции, как LineTo, PolylineTo, PolyBezierTo и даже функции вывода текста, начинают вывод с текущей позиции пера. Все координаты задаются в логическом пространстве. Функция LineTo рисует линию от текущей позиции пера к точке (nXEnd, nYEnd) pi переводит текущую позицию в точку (nXEnd, nYEnd). Внешний вид линии зависит от всех атрибутов, описанных в предыдущем разделе. В процессе вывода также могут учитываться и другие атрибуты контекста устройства — например, мировое преобразование, режим отображения, бинарная растровая операция, режим заполнения фона, цвет фона и угловой лимит. При этом необходимо учитывать ряд обстоятельств. Во-первых, пиксел физического координатного пространства, отображаемый в точку (nXEnd, nYEnd), не рисуется, а начальная точка рисуется. Например, если вы проводите линию из точки (0,0) в точку (100,0) при тождественном отображении из логических координат в физические, рисуются пикселы (0,0)—(99,0), но не рисуется пиксел
Линии 443 (100,0). При этом текущая позиция пера перемещается в точку (100,0), поэтому эта точка будет нарисована при выводе следующей линии, проведенной из этой точки. Если вы рисуете несколько соединенных линий функцией LineTo, каждый пиксел, за исключением последнего, рисуется ровно один раз. Это условие сохраняется при смещениях, масштабированиях, поворотах и других преобразованиях координатного пространства. Если при выводе линии используются такие бинарные растровые операции, как R2X0RPEN, точки соединения отрезков по виду не отличаются от других пикселов. Если бы функция LineTo прорисовывала последний пиксел, точки стыков рисовались бы дважды и поэтому выводились бы цветом фона. Из-за этого правила и по другим причинам операции рисования линий являются направленными. Другими словами, для двух точек (х0,у0) и (х1,у1) линия, проведенная из (х0,у0) в (х1,у1), несколько отличается от линии, проведенной из (х1,у1) в (х0,у0). Также следует помнить о том, что каждый вызов функции рисования линии стилевым пером заново начинает цикл чередования пикселов. Совмещение стилей разных линий (что-то наподобие изменения базовой точки кисти) в GDI не поддерживается. Например, для трех точек (х0,у0), (х1,у1) и (х2,у2), расположенных на одной прямой, результат рисования двух линий (х0,у0) - (х1,у1) и (х1,у1) - (х2,у2) может отличаться от результата рисования одной линии (х0,у0) - (х2,у2). В следующем фрагменте функции MoveToEx и LineTo используются для рисования «розетки» — многоугольника, у которого каждая вершина соединена со всеми остальными вершинами. Цвет линии зависит от расстояния между вершинами. Для упрощения переключения цветов в этом фрагменте используется перо DC, однако его нетрудно заменить простым пером. const int N =19; const int Radius - 200; const double theta - 3.1415926 * 2 / N: SelectObject(hDC. GetStockObject(DC_PEN)); const C0L0RREF color[] = { RGB(0. 0. 0), RGB(255, 0. 0). RGB(0.255.0) RGB(255.255.0). RGB(0. 255. 255). RGBC255. 255 RGB(127. 255. 0). RGB(0. 127. 255). RGBC255. 0 }: for (int p=0; p<N; p++) for (int q=0; q<p; q++) { SetDCPenColor(hDC. color[min(p-q.N-p+q)]); MoveToEx(hDC. (int)(220 + Radius * sin(p * theta)). (int)(220 + Radius * cos(p * theta)). NULL); LineTo(hDC. (int)(220 + Radius * sin(q * theta)). (int)(220 + Radius * cos(q * theta))); Функции PolylineTo передается массив структур POINT, содержащих координаты хну. Сначала функция рисует первый отрезок от текущей позиции пера к первой точке массива POINT, а затем последовательно соединяет линиями все остальные точки массива. В конце рисования текущая позиция пера перемещается в последнюю точку. Функция Polyline получает те же параметры, что и RGB(0.0.255). 0). 127)
444 Глава 8. Линии и кривые PolylineT, но работает несколько иначе — она не использует и не обновляет текущую позицию пера. Функция Polyline проводит отрезок от первой точки массива ко второй, а затем последовательно соединяет все остальные точки массива. Для массива из п структур POINT функция PolylineTo рисует п отрезков, а функция Polyline рисует п-1 отрезок. В общем случае функции PolylineTo и Polyline нельзя заменить вызовами MoveToEx и LineTo. При использовании стилевого пера функции Polyline и PolylineTo при переходе к новому отрезку продолжают старый цикл чередования пикселов, а при нескольких вызовах LineTo рисунок каждый раз начинается заново. При работе с геометрическим пером атрибут завершения применяется к первой и последней точке, а атрибут соединения — к каждому стыку. Использование функций Polyline и PolylineTo улучшает внешний вид стыков, чего довольно трудно добиться с помощью функции LineTo. Это особенно важно при увеличении изображения или при печати, где характерны утолщенные перья. Достаточно сравнить рис. 8.7, где используются серии вызовов LineTo, и рис. 8.8, где используется один вызов Polyline. Кроме того, многократный вызов функции API обрабатывается дольше, чем однократный вызов для нескольких отрезков. Наконец, при выводе в метафайловый контекст устройства функции Polyline и PolylineTo занимают меньше места, чем серия вызовов LineTo. Однако функции PolylineTo и Polyline не идеальны; в частности, они не поддерживают концепцию замкнутых контуров. Завершение выводится в первой и последней точке всегда, даже если их координаты в точности совпадают. Если вы рисуете геометрические фигуры, все углы которых кратны 90°, квадратные завершения и заостренные соединения обеспечат нормальное замыкание контура, а для рисования замкнутых фигур с закругленными углами всегда можно воспользоваться закругленными завершениями и соединениями. Но если вы рисуете произвольный треугольник или многоугольник, в нем могут встретиться острые или тупые углы. Заостренное соединение обеспечивает правильность всех стыков, кроме самой первой точки (которая также является последней). Существует стандартное решение — добавить в конец дополнительный отрезок от первой точки ко второй. Даже при использовании бинарных растровых операций, при которых существуют различия между однократным и двукратным выводом, фигура, нарисованная за один вызов PolylineTo или Polyline, не содержит пикселов, нарисованных дважды. В следующем фрагменте функция Polyline используется для рисования треугольника. Необязательный параметр extra позволяет создать дополнительный отрезок, улучшающий вид конечной точки: void Triangle(HDC hDC. int xO. int yO. int xl. int yl. int x2, int y2. bool extra=false) { POINT corner[5]= {xO.yO. xl.yl. x2.y2. xO.yO, xl.yl}; if (extra) // Дополнительный отрезок, // улучшающий вид замкнутой фигуры Polyline(hDC. corner, 5); else Polyline(hDC, corner, 4); }
Линии 445 Функция PolyPolyline позволяет нарисовать несколько ломаных линий за один вызов. В ее последнем параметре nCount вместо количества вершин передается количество ломаных. Третий параметр, IpdwPolyPoints, представляет собой массив с количествами вершин в каждой ломаной. Функция PolyPolyline рисует всю фигуру в целом; она не сводится к простому вызову Polyline для каждой ломаной. Различия проявляются при использовании перекрывающихся ломаных и таких растровых операций, как R2X0RPEN. Если фигура рисуется одним вызовом, каждый пиксел прорисовывается всего один раз; в противном случае перекрывающиеся пикселы прорисовываются многократно, а их цвет обычно отличается от цвета пикселов при однократной прорисовке. На растровых устройствах (таких, как экран монитора или принтер) пикселы, образующие линию, выбираются при помощи специальных алгоритмов DDA (Digital Differential Analyzer). Примером классического алгоритма DDA является алгоритм Брезенхэма (Bresenham). Для представления поверхности физического устройства графический механизм Windows NT/2000 использует координаты с фиксированной точкой в формате 28.4. Линии рисуются по так называемому алгоритму G1Q (Grid Intersection Quantization), при котором каждый пиксел окружается воображаемым ромбом величиной 1x1 пиксел. Пиксел рисуется, если линия имеет общие точки с этим ромбом. Функция LineDDA позволяет передать координаты каждой точки, которую GDI собирается выводить, функции косвенного вызова, указанной приложением: BOOL LineDDACint nXStart. int nYStart. int nXEnd, int nYEnd. LINEDDAPROC IpLineProc. LPARAM lpData); Прототип LineDDA не производит особого впечатления. Среди параметров функции нет ни контекста устройства, ни логического пера. Следовательно, LineDDA возвращает точки в одной системе координат с параметрами, без учета специфики физической системы координат и стилей пера. В листинге 8.3 показано, как функции LineTo, Polyline и LineDDA используются для рисования равносторонних треугольников. Результат изображен на рис. 8.9. Поскольку мы используем толстое геометрическое перо, соединения на первом рисунке (функция LineTo) выглядят уродливо. На втором рисунке (функция Polyline с тремя отрезками) нарушено лишь последнее соединение. На третьем рисунке добавлен четвертый отрезок, обеспечивающий идеальную стыковку во всех точках. На четвертом рисунке показано, как при помощи функции LineDDA разместить вдоль базовой оси треугольника несколько маленьких треугольников. Функция косвенного вызова LineDDAProc рисует маленький треугольник при каждом 32-м вызове. Листинг 8.2. Рисование линий функциями LineTo, Polyline и LineDDA * void CALLBACK LineDDAProcCint x. int y. LPARAM lpData) { HDC hDC = (HDC) lpData; POINT cur; GetCurrentPositionEx(hDC. &cur); if ( (cur.x & 31)== 0 ) // Каждый 32-й вызов Triangle(hDC, x. y-16. x+20, y+18. x-20. y+18): Продолжение^
446 Глава 8. Линии и кривые Листинг 8.2. Продолжение cur.x ++; MoveToEx(hDC, cur.x, cur.у, NULL); void KMyCanvas::TestLine2(HDC hDC) { LOGBRUSH logbrush = { BS_S0LID. RGB(0, 0, OxFF), 0 }; HPEN hPen = ExtCreatePen(PS_GEOMETRIC | PS_S0LID | PSJNDCAPJLAT | PS_JOIN_MITER, 15. & logbrush, 0. NULL); HGDIOBJ hOld = SelectObject(hDC. hPen); // Нарисовать треугольник несколькими вызовами LineTo SetViewportOrgEx(hDC, 100. 50. NULL); Line(hDC. 0. 0. 50, 86); LineTo(hDC. -50, 86); LineTo(hDC. 0, 0); // Использование функции Polyline SetViewportOrgEx(hDC. 230. 50. NULL); Triangle(hDC. 0. 0, 50, 86, -50. 86, false ); // Использование функции Polyline с дополнительным отрезком SetViewportOrgEx(hDC. 360, 50, NULL); Triangle(hDC, 0. 0. 50. 86. -50. 86. true ); // Использование LineDDA SetViewportOrgEx(hDC. 490. 50, NULL); SelectObject(hDC. hOld); DeleteObject(hPen); hPen = ExtCreatePen(PS_GEOMETRIC | PS_D0T | PS_ENDCAP_ROUND. 3. & logbrush, 0. NULL); SelectObject(hDC. hPen): LineDDA( 0. 0. 50, 86. LineDDAProc. (LPARAM) hDC); LineDDA( 50, 86. -50. 86. LineDDAProc. (LPARAM) hDC); LineDDA(-50. 86. 0. 0. LineDDAProc. (LPARAM) hDC); SetViewportOrgEx(hDC, 50. 150. NULL); SelectObjecUhDC. hOld); DeleteObject(hPen); Рис. 8.9. Рисование треугольников функциями LineTo, Polyline и LineDDA AAA
Кривые Безье 447 Кривые Безье Предположим, у нас имеются точки Р1 и Р2 на плоскости и отрезок, соединяющий эти точки. Пусть переменная t принимает значения от 0 до 1; точка Р12(£) на отрезке Р1-»Р2 определяется по формуле P12(t) - (l-t)Pl + tP2 Отрезок Р1-»Р2 образуется значениями функции Р12(£) при изменении t от О до 1. Если добавить на плоскость еще одну точку РЗ, мы можем определить Р12(£) как точку между Р1 и Р2, а Р23(£) — как точку между Р2 и РЗ. Если теперь применить аналогичный метод для определения Р1223(£) как точки между Р12(£) и Р23(£), мы получим: P12(t) = (l-t)Pl + tP2 P23(t) - (l-t)P2 + tP3 P1223(t) = (l-t)(l-t)Pl + tP2) + t((l-t)P2 + tP3) = (l-t)A2Pl + 2t(t-l)P2 + tA2P3 Точки, описываемые функцией PI223(0 при изменении t от 0 до 1, уже не образуют прямую линию. Перед нами квадратичная кривая, или параболическая кривая второго порядка. Этот способ определения кривых изобрел П. де Касте- ло (P. de Casteljau) в 1959 году. Позднее, в 1962 году, теорию этих кривых заново разработал П. Безье (P. Bezier) в процессе работы над системами автоматизированного проектирования для компаний «Ситроен» и «Рено». Именно Безье впервые представил эти кривые широкой публике, поэтому они известны как кривые Безье. Квадратичные кривые Безье используются в шрифтах TrueType для описания контуров глифов. Для компьютерной графики характерны кривые, определяемые четырьмя точками по описанному выше принципу. Такие кривые называются кубическими кривыми Безье. На рис. 8.10 показан процесс конструирования кубической кривой Безье по следующим формулам: P12(t) = (l-t)Pl + tP2 P23(t) = (l-t)P2 + tP3 P34(t) = (l-t)P3 + tP4 P1223U) « (l-t)A2Pl + 2t(t-l)P2 + Г2РЗ P2334U) = (l-tr2P2 + 2t(t-l)P3 + Г2Р4 P(t) = (l-t)A3Pl + 3(l-t)"2tP2 + 3(l-t)tA2P3 + ГЗР4 Процесс, проиллюстрированный на рисунке, обычно называется алгоритмом де Кастело. Точки Р1, Р2, РЗ и Р4, задающие кривую Безье, называются ее определяющими точками. Точки Р1 и Р4 называются конечными, а точки Р2 и РЗ — контрольными. Кривые Безье обладают рядом интересных свойств, из-за которых они широко используются в системах автоматизированного проектирования и в производстве. О Аффинная инвариантность. Кривые Безье в результате аффинных преобразований, используемых GDI при отображении из мировой системы координат в страничную, переходят в кривые Безье. Следовательно, графическому механизму остается лишь преобразовать определяющие точки и нарисовать кривую в координатах устройства по преобразованным точкам.
448 Глава 8. Линии и кривые Р2 '■*■*.. Р23 PI P4 Рис. 8.10. Построение кубической кривой Безье по четырем определяющим точкам О Ограниченность. Кривая Безье всегда полностью лежит внутри выпуклой фигуры, вершинами которой являются ее определяющие точки. О Касательные в конечных точках. Линия, соединяющая точки Р1 и Р2, является касательной к кривой в точке Р1; линия, соединяющая точки РЗ и Р4, является касательной к кривой в точке Р4. Чтобы две кривые Безье (Р1,Р2, РЗ,Р4) и (Р4,Р5,Р6,Р7) соединялись плавно (то есть с непрерывной первой производной), достаточно, чтобы точки РЗ, Р4 и Р5 находились на одной линии. О Делимость. Кривая Безье легко делится на две кривые Безье. Кривую, изображенную на рис. 8.10, можно легко разделить на две кривые, соединяющиеся в точке Р; первая кривая определяется точками (Р1,Р12,Р1223,Р), а вторая - точками (Р,Р2334,Р34,Р4). На основании свойства делимости построен алгоритм рисования кривых Безье как совокупности прямых линий. Кривая Безье делится в средней точке (t = 0,5), после чего две полученные кривые рекурсивно делятся до тех пор. пока контрольные точки не окажутся достаточно близко к линии, что позволяет представить кривую отрезком. Рекурсивная функция для аппроксимации кривых Безье представлена в листинге 8.3. Сначала функция проверяет, расположены ли точки (х2,у2) и (хЗ,уЗ) па расстоянии меньше одной единицы от линии (х1,у1) - (х4,у4). Если это условие выполняется, функция рисует прямую линию; в противном случае кривая делится в середине на две кривые, вывод которых осуществляется рекурсивным вызовом. Точность вычислений обеспечивается использованием чисел с плавающей точкой. Листинг 8.3. Рисование кривых Безье из отрезков void Bezier(HDC hDC. double xl, double yl. double x2. double y2. double x3. double y3, double x4. double y4) {
Кривые Безье 449 double A = у4 - yl; double В = xl - х4; double С = yl * (x4-xl) - xl * ( y4-yl); // Ах + By + С = О - линия (xl.yl) - (х4,у4) double AB =А*А+В*В; // Расстояние от (х2.у2) до линии меньше 1 // Расстояние от (хЗ.уЗ) до линии меньше 1 if ( (A*x2 + B*y2 + C)*(A*x2 + B*y2 + C)<AB) if ( (А*хЗ + В*уЗ + С)*(А*хЗ + В*уЗ + С)<АВ) { MoveToEx(hDC. (int)xl. (int)yl. NULL); LineTo(hDC. (int)x4. (int)y4); return; } double xl2 double yl2 double x23 double y23 double x34 double y34 double xl223 double у1223 double x2334 double y2334 double x double у = xl+x2 = yl+y2 - x2+x3 - У2+УЗ = x3+x4 = y3+y4 = xl2+x23; = yl2+y23 = x23+x34 = y23+y34 = X1223 + x2334 = У1223 + y2334 Bezier(hDC. xl. yl. xl2/2. yl2/2. xl223/4. yl223/4. x/8. y/8); Bezier(hDC, x/8. y/8. x2334/4. y2334/4. x34/2. y34/2. x4. y4): } Давайте познакомимся с двумя функциями GDI, обеспечивающими поддержку кривых Безье. BOOL PolyBezier (HDC hDC. CONST POINT * Ippt. DWORD cPoints); BOOL PolyBezierTo(HDC hDC. CONST POINT * Ippt. DWORD cCount); Обе функции рисуют несколько кривых Безье за один вызов. Для рисования п кривых функция PolyBezier получает jxn+1 точек в массиве, на который ссылается указатель Ippt; при этом параметр cPoints должен быть равен jxn+ 1. Первые четыре точки lppt[0], lppt[l], lppt[2] и lppt[3] определяют первую кривую, lppt[3] со следующими тремя точками — вторую кривую и т. д. Функция PolyBezier не использует и не обновляет текущей позиции пера. Функция Poly- BezierTo рисует, начиная с текущей позиции пера, и переводит ее в последнюю точку, переданную в параметрах функции. Для рисования п кривых функция PolyBezierTo должна получить jxn точек. На рис. 8.10 изображена обычная кривая Безье с двумя контрольными точками, расположенными по одну сторону от линии Р1-»Р4; координаты х всех четырех контрольных точек упорядочены по возрастанию. Изменение положения контрольных точек приводит к кардинальному изменению внешнего вида кривой. В следующем фрагменте (листинг 8.4) рисуется последовательность из пяти кривых Безье с использованием функции PolyBezier.
450 Глава 8. Линии и кривые Листинг 8.4. Рисование серии кривых Безье с использованием функции PolyBezier HPEN hRed = CreatePen(PS_DOT, 0, RGBCOxFF. 0, 0)); HPEN hBlue - CreatePen(PS SOLID. 3. RGB(0, 0, OxFF)); for (int z=0; z<=200; z+=40) int x = 50, у = 240; POINT p[4]=(x.y, x+50,y-z, x+100,y-z. POINT q[4]=(x,y. x+50,y-z, x+100.y+z. POINT r[4]=(x,y, x+170,y-z, x-20.y-z. POINT s[4]=(x.y. x+170,y-z, x-20,y+z, POINT t[4]=(x+75,y. x.y-z. x+150,y-z. SelectObject(hDC, hRed); x+150.y} x+150,y} x+150,yj x+150.y} x+75,y}; x+=160 x+=180 x+=200 x+=180 PolylineChDC. p, 4) PolylineChDC. r, 4) PolylineChDC, t. 4) PolylineChDC, q, 4); PolylineChDC, s, 4); Select0bject(hDC. hBlue); PolylineChDC, p, 4) PolylineChDC, r. 4) PolylineChDC, t. 4) PolylineChDC. q, 4); PolylineChDC. s, 4); SelectObject(hDC. GetStockObject(BLACK_PEN)); DeleteObject(hRed); DeleteObject(hBlue); Этот фрагмент показывает, как определяются кривые Безье разной формы. У кривых первой группы контрольные точки лежат на одной стороне от линии, а у кривых второй группы они лежат на разных сторонах. В третьей группе значения координат х двух контрольных точек меняются местами, а четвертая группа совмещает признаки второй и третьей. Наконец, в последней группе позиции двух конечных точек совпадают. Результат работы этого фрагмента с пояснительными метками показан на рис. 8.11. Р2, рз . Р2, Р2 РЗ- /' РЗ Рис. 8.11. Галерея кривых Безье
Кривые Безье 451 С точки зрения применения перьев функции PolyBezier и PolyBezierTo аналогичны Polyline и PolylineTo. Простые перья используют режим заполнения и цвет фона для стилевых линий, геометрические и косметические перья всегда рисуют в прозрачном режиме, не обращая внимания на цвет и режим заполнения. При рисовании нескольких кривых внешний вид стыков определяется атрибутом соединения, конечные точки снабжаются завершениями, а весь вывод выполняется за одну операцию, при этом никакие части изображения не рисуются дважды. Poly Draw Функции PolyBezier и PolyBezierTo рисуют только непрерывные кривые Безье. В Windows NT/2000 GDI появилась новая функция PolyDraw, обладающая расширенными возможностями — она позволяет рисовать разъединенные линии, кривые Безье и даже замкнутые фигуры. BOOL PolyDrawCHDC hDC. CONST POINT * lppt, CONST BYTE * IpbTypes. int nCount); Функция PolyDraw получает два массива: параметр lppt содержит массив точек, а параметр 1 pbTypes — массив типов точек (по одному элементу для каждой точки первого массива). Пять допустимых типов точек перечислены в табл. 8.4. Таблица 8.4. Типы точек, используемые функциями PolyDraw и GetPath Тип Описание PTM0VET0 Начать новую отдельную фигуру. Текущая позиция пера перемещается в заданную точку PTLINET0 Провести линию от текущей позиции к заданной точке и обновить текущую позицию PTLINET01PTCLOSEFIGURE То же, что и PTLINET0, с замыканием фигуры от заданной точки к последней позиции PT_M0VET0 PTBEZIERTO Нарисовать кривую Безье, используя текущую позицию пера и три точки массива, начиная с заданной. У двух следующих точек также должен быть установлен флаг PTBEZIERTO. Текущая позиция пера перемещается в третью точку PTBEZIERTO | PTCLOSEFIGURE Последняя точка PTBEZIERTO соединяется с последней позицией PT_M0VET0 Функция PolyDraw обладает некоторыми выдающимися возможностями. Во- первых, она позволяет комбинировать кривые Безье с прямыми линиями. Хотя любую прямую можно преобразовать в кривую Безье, добавив две контрольные точки на самой линии, подобные преобразования выглядят неестественно. Во- вторых, PolyDraw позволяет нарисовать несколько отдельных фигур за один вызов. Как говорилось выше, рисование нескольких линий или кривых за один вызов функции гарантирует, что ни один пиксел не будет прорисован дважды,
452 Глава 8. Линии и кривые что бывает существенно при рисовании сложных фигур с использованием режима R2X0RPEN в графическом пакете. В-третьих, функция PolyDraw позволяет замыкать фигуры автоматическим соедршением конечной точки с начальной, что весьма удобно для приложений. При рисовании замкнутых фигур геометрическим пером завершения не используются; даже конечная точка оформляется соединением, чтобы фигура выглядела более гладкой. Фрагмент кода, приведенный в листинге 8.5, двумя разными способами рисует фигуру, состоящую из горизонтальной «восьмерки» и ромба. Для вывода используется утолщенное однородное геометрическое перо с плоским завершением и заостренным соединением; рисование осуществляется в режиме R2X0RPEN. В первой части программы «восьмерка» рисуется функцией PolyBezier, а ромб — функцией Polyline. Хотя обе фигуры замкнуты, в конечных точках видны заметные дефекты стыковки, а пересечения фигур окрашены в белый цвет. Во второй части две замкнутые фигуры рисуются простым вызовом PolyDraw, что решает обе проблемы. Результат показан на рис. 8.12. Листинг 8.5. Использование функции PolyDraw при рисовании замкнутых фигур const POINT P[12] - { 50, 200. 100. 50, 150. 350. 200. 200. 150. 50. 100. 350. 50. 200. 125. 275. 175. 200. 125. 75. 200. 125, 275 }: const BYTE T[12] = { PT_M0VET0. PT_BEZIERTO. PTJEZIERTO. PT_BEZIERTO. PT_BEZIERTO. PT_BEZIERTO. PT_BEZIERTO | PT_CLOSEFIGURE. PT_M0VET0. PT_LINET0. PT_LINETO. PT_LINET0. PT_LINET0 | PT_CLOSEIGURE }: SetR0P2(hDC. R2J0RPEN): LOGBRUSH logbrush = { BS_S0LID. RGB(0xFF. OxFF. 0). 0 }; HPEN hPen = ExtCreatePen(PS_GEOMETRIC | PS_S0LID | PS_ENDCAP_FLAT| PS_JOIN_MITER. 15. & logbrush. 0. NULL); SelectObjectChDC. hPen); PolyBezier(hDC. P. 7); // Две кривые Безье Polyline(hDC. P+7, 5); // Ромб SetViewportOrgEx(hDC. 200. 0. NULL); PolyDraw(hDC. P, T. 12); // Обе фигуры SetViewportOrgEx(hDC. 0. 0. NULL); SelectObject(hDC. GetStockObject(BLACK_PEN)); DeleteObject(hPen); К сожалению, в Windows 95/98 эта замечательная функция не поддерживается. В статье Q135059 MSDN Knowledge Base приведена возможная реализация PolyDraw для Windows 95, основанная на использовании функций MoveToEx, LineTo и PolyBezierTo. Предлагаемый код обладает рядом недостатков — он не pea-
Кривые Безье 453 лизует PTCLOSEFIGURE, использует множественные вызовы функций, приводящие к многократной прорисовке фрагментов изображения, и применяет завершения для геометрических перьев. В правильной реализации следует воспользоваться траекториями GDI, которые могут состоять из нескольких отдельных фигур с завершениями и соединениями. Объекты траекторий рассматриваются ниже в этой главе. Рис. 8.12. Использование функции PolyDraw при рисовании замкнутых фигур Альтернативное определение кривых Безье В стандартном определении кривой Безье используются две конечные и две контрольные точки. Такое определение весьма наглядно с геометрической точки зрения, поэтому оно хорошо подходит для интерактивных манипуляций. Однако с точки зрения программиста кривую было бы удобнее определять по точкам, расположенным на кривой, без контрольных точек. В частности, для кубических кривых Безье часто возникает вопрос — как определить по четырем точкам А, В, С, D кривую, которая бы проходила через точку А при t = О, через точку В при t = 1/3, через точку С при t = 2/3 и через точку D при t = 1? Проблема сводится к вычислению по известным А, В, С и D четырех точек Р1, Р2, РЗ и Р4, где точки Р1 и Р4 находятся на концах кривой, а точки Р2 и РЗ соответствуют контрольным точкам. В соответствии с параметрическим определением кривой Безье мы получаем следующую систему линейных уравнений: А - Р1 В = (2/ЗГЗ Р1 + 3(2/3)^2(1/3) Р2 + 3(2/3)(1/3)А2РЗ + Р4 С = (1/ЗГЗ Р1 + 3(1/2)^2(2/3) Р2 + 3(1/3)(2/ЗГ2РЗ + Р4 D = Р4 С точками Р1 и Р4 все просто. Уравнения для точек В и С преобразуются к виду: 12Р2 + 6РЗ = 27В - 8А - D 6Р2 + 12РЗ = 27С - А - 8D Решая эту систему уравнений для переменных Р2 и РЗ, мы получаем решение: PI - A Р2 - (- 5А + 18В - 9С + 2D)/6 РЗ = ( 2А - 9В + 18С - 5D) / 6 Р4 - D
454 Глава 8. Линии и кривые Эти формулы также убедительно доказывают, что для любых четырех точек на плоскости существует кривая Безье, проходящая через них при И 0, 1/3, 2/3 и 1. Дуги Из всех кривых наибольшей известностью пользуется эллипс, частным случаем которого является окружность. В простейшем случае оси эллипса параллельны осям координат. Эллипс определяется следующим уравнением: (х-хОГ2/аА2 + (у-уО)А2/ЬА2 = 1 Здесь (хО,уО) — центр эллипса, а а и Ъ — главная и вспомогательная оси. Благодаря простоте этого уравнения эллипсы или любые фигуры, созданные на их основе, очень просто представляются в GDI. В GDI эллипс определяется ограничивающим прямоугольником (хО - а,уО - Ь, хО + а, уО + Ь). Дуга представляет собой полный периметр эллипса или его часть. Часть периметра эллипса проще всего определяется по значениям начального и конечного углов; в этом случае дуга определяется как совокупность точек эллипса, лежащих в интервале от начального до конечного угла. Чтобы избежать вычислений с плавающей точкой, но при этом обеспечить необходимую точность, GDI использует две опорные точки (xStart,yStart) и (xEnd,yEnd), по которым легко вычисляется начальная и конечная точки дуги. Начальная точка дуги является пересечением линии, соединяющей центр эллипса (xStart,yStart) с периметром эллипса; аналогично, конечная точка дуги находится в пересечении линии «центр эллипса — (xEnd,yEnd)» с периметром эллипса. Следующие три функции GDI предназначены для рисования дуг: BOOL ArcCHDC hDC, int nLeft. int nTop, int nRight, int nBottom, int xStart. int nYStart, int nXEnd, int nYEnd); BOOL ArcToCHDC hDC, int nLeft, int nTop. int nRight. int nBottom, int xStart. int nYStart, int nXEnd, int nYEnd); BOOL AngleArc(HDC hDC, int X, int Y, DWORD dwRadius. FLOAT eStartAngle. FLOAT eSweepAngle); Для функций Arc и АгсТо параметры nLeft, nTop, nRight и nBottom задают ограничивающий прямоугольник эллипса, частью которого является дуга. Центр эллипса совпадает с центром прямоугольника. Следующие четыре параметра используются для вычисления начального и конечного углов дуги. Функция Arc рисует дугу от начального до конечного угла. В Windows 95/9 дуга рисуется против часовой стрелки; в Windows NT/2000 направление дуги определяется флагом контекста устройства (GetArcDi recti on, SetArcDi recti on). Первые две дуги на рис. 8.13 поясняют смысл параметров Агс/АгсТо и показывают, как направление рисования влияет на вывод. Функция Arc не использует и не обновляет текущую позицию пера. С функцией АгсТо дело обстоит несколько иначе — она проводит линию из текущей позиции пера в настоящую начальную точку дуги, после чего рисует дугу. После завершения дуги текущая позиция пера перемещается в конечную точку дуги.
Дуги 455 Arc, против часовой стрелки АгсТо, по часовой стрелке AngleArc Слева, сверху Слева, сверху (xStart, yStart) (xStart, yStart) (xEnd, yEnd) Снизу, справа (xEnd, yEnd) eStartAngle eStartAngle + eSweepAngle Снизу, справа Снизу, справа Рис. 8.13. Определение дуг в GDI Функции Arc и АгсТо позволяют нарисовать полный периметр эллипса; для этого достаточно задать конечную точку, совпадающую с начальной. Чтобы нарисовать часть эллипса, не входящую в заданную дугу, достаточно поменять местами начальную и конечную точки. На случай, если вам вдруг понадобится вычислить позицию начальной или конечной точек, ниже приведены формулы для начальной точки: ХО - (nLeft + nRight)/2; // Центр эллипса Y0 - (nTop + nBottom)/2; DXs = nXStart - ХО; DYs - nYStart - YO; Ds - sqrt(DXs * DXs + DYx * DYx); // Расстояние от центра Xs - ХО + (nRight-nLeft)/2 * DXs / Ds; Ys - YO + (nBottom-nTop)/2 * DYs / Ds; Определение дуги в градусах: функция AngleArc Функция AngleArc, поддерживаемая только в ОС семейства NT, нарушает общий принцип — избегать вещественных вычислений. Она получает начальный угол дуги и ее угловой размер в градусах (не в радианах!) в виде вещественных чисел. Угловой размер дуги указывает и ее направление, поэтому атрибут направления дуг контекста устройства в этом случае не используется. Другая особенность AngleArc заключается в том, что вы можете задать только радиус круга в логическом координатном пространстве. Чтобы нарисовать часть эллипса, приложение должно определить соответствующее преобразование или отображение. Функция AngleArc, как и АгсТо, проводит отрезок от текущей позиции пера к начальной точке дуги и перемещает текущую позицию в конечную точку дуги. Непонятно лишь одно — почему эта функция не называется AngleArcTo? При использовании AngleArc полный эллипс рисуется очень просто: достаточно нарисовать дугу с угловым размером 360 градусов. А что будет, если нарисовать дугу в 540 градусов? В MSDN утверждается, что если угловой размер дуги превышает 360 градусов, дуга рисуется несколько раз. Отсюда следует, что в режиме R2_X0RPEN 540-градусная дуга сначала нарисует 360-градусный полный круг, а затем добавит дугу в 180 градусов, восстанавливающую исходный фон, так что в
456 Глава 8. Линии и кривые итоге вы получите 180-градусную дугу. Наши эксперименты показали, что если угловой размер превышает 360 градусов, полная окружность рисуется всего один раз. Функция Angl еАгс легко реализуется на базе функции АгсТо; эта возможность может пригодиться на платформах, не входящих в семейство NT. Примерная реализация выглядит так: BOOL AngleArcTo(HDC hDC, int X. int Y. DWORD dwRadius, FLOAT eStartAngle, FLOAT eSweepAngle) { const FLOAT pioverl80 - (FLOAT) 3.141592653586/180; if ( eSweepAngle >= 360) // Если угол больше 360 градусов - eSweepAngle = 360; // оставить полную окружность else if (eSweepAngle <= -360 ) eSweepAngle = -360; FLOAT eEndAngle = (eStartAngle + eSweepAngle ) * pioverl80; eStartAngle = eStartAngle * pioverl80; int dir; // Угловой размер дуги определяет направление if (eSweepAngle > 0) dir = SetArcDirecti on(hDC, AD_COUNTERCLOCKWISE); else dir = SetArcDirecti on(hDC. AD_CLOCKWISE); // Угол задается в системе координат устройства BOOL rslt = ArcTo(hDC. X - dwRadius, Y - dwRadius. X + dwRadius. Y + dwRadius. X + (int) (dwRadius * 10 * cos(eStartAngle)), Y - (int) (dwRadius * 10 * sin(eStartAngle)), X + (int) (dwRadius * 10 * cos(eEndAngle)). Y - (int) (dwRadius * 10 * sin(eEndAngle))); SetArcDirecti on(hDC. dir); return rslt; } Функция удостоверяется в том, что размер рисуемой дуги не превышает полной окружности, преобразует градусы в радианы, задает направление дуги в соответствии со знаком углового размера, вычисляет начальную и конечную точки (при этом радиус круга умножается на 10 для повышения точности) и затем рисует дугу функцией АгсТо. Рисование дуг пером со стилем PS__INSIDEFRAME Поскольку дуги ограничиваются прямоугольниками и внутренняя сторона дуги легко отличается от внешней, GDI учитывает стиль пера PSINSIDEFRAME. Если перо с этим стилем используется для рисования дуги, центральная линия кривой смещается внутрь на половину толщины пера. Внешние пикселы линии соприкасаются с ограничивающим прямоугольником, а остальные пикселы рисуются внутри прямоугольника. При выводе дуг применение пера со стилем PS_ INSIDEFRAME реализуется очень просто — достаточно уменьшить ограничивающий прямоугольник на половину толщины пера. Сделать то же самое в общем случае (например, для замкнутой серии кривых Безье) гораздо сложнее.
Дуги 457 Следует заметить, что стиль пера PSINSIDEFRAME определяется на том же уровне, что и PSS0LID или PSD0T и не является атрибутом линии, как, например, атрибуты завершения и соединения. В результате внутренние перья всегда являются сплошными. Вероятно, это свойство следовало бы реализовать как независимый атрибут пера. Преобразование дуг в кривые Безье Вероятно, вы уже поняли, что рисование кривых является непростой задачей. Чтобы определить, какую часть периметра эллипса необходимо нарисовать, приходится использовать вычисления с плавающей точкой и разбираться с атрибутом направления дуг. Еще хуже то, что вы можете рисовать части только таких эллипсов, у которых оси параллельны осям логической системы координат. Если вы захотите нарисовать дугу после поворота или сдвига, вам придется вычислить нужное преобразование, причем в ОС, не входящих в семейство NT, эта возможность не поддерживается. На помощь приходят кривые Безье. Операции с ними очень просты, а вычисления в процессе рисования достаточно элементарны. В результате аффинных преобразований кривая Безье отображается в кривую Безье, причем результат всегда однозначен, поскольку четыре точки определяют ровно одну кривую. Остается единственный вопрос — как построить кривую Безье, аппроксимирующую эллиптическую дугу? Аппроксимация полной окружности одной кривой Безье обладает слишком высокой погрешностью. Даже представление половины окружности одной кривой Безье не гарантирует достаточной точности. Давайте попробуем вычислить позиции контрольных точек для кривой Безье, аппроксимирующей четверть окружности (90 градусов). Для четверти единичной окружности, расположенной в первом квадранте декартовой системы координат, нам известны конечные точки (0,1) и (1,0). 90-градусная дуга симметрична относительно линии X = Y, поэтому две контрольные точки тоже должны быть симметричными. Мы знаем, что линия, проведенная из начальной точки в первую контрольную точку, является касательной к дуге в начальной точке. Это означает, что линия должна проходить под углом 0 градусов, то есть первая контрольная точка должна иметь координаты (т,1) для неизвестной переменной т. По свойству симметрии вторая контрольная точка должна иметь координаты (1,/и). Итак, нам известны все четыре контрольные точки Р1:(0Д), Р2:(етг,1), Р3(1,га) и Р4(1,0); остается лишь найти неизвестную переменную т. На рис. 8.14 показана 90-градусная дуга единичной окружности в первом квадранте. Светлая ломаная соединяет четыре точки кривой Безье, которую мы пытаемся найти. Шесть светлых кривых показывают аппроксимации кривой Безье при изменении т от 0 до 1,0 с шагом 0,2. Темная кривая изображает аппроксимируемую дугу. Средняя точка кривой вычисляется подстановкой значения t = 0,5 в формулу из предыдущего раздела: Р(0,5) = (l-t)A3Pl + 3(l-t)A2tP2 + 3(l-t)tA2P3 + ГЗР4 = (PI + 3P2 + ЗРЗ + P4)/8
458 Глава 8. Линии и кривые (ОЛ) (т,1) т=0.552285, еггог(0.211) = +0.027253% Рис. 8.14. Преобразование 90-градусной дуги в кривую Безье Если вычислить по этой формуле среднюю точку кривой (0,1), (ти,1), (1,^0 и (1,0), мы получаем: Xmid = (0+3m+3+l)/8 - (3m + 4)/8 Ymid « (l+3+3m+l)/8 - (3m + 4)/8 Известно, что в единичном круге точка (Xmid,Ymid) имеет координаты (л/2/2, л/2/2), поэтому т = 4(л/2 - 1)/3 или приблизительно т = 0,552285. Если воспользоваться четырьмя такими кривыми Безье для аппроксимации полного эллипса, на всех углах, кратных 45 градусам, кривая Безье будет точно совпадать с кругом. Остается лишь понять, насколько близко она будет располагаться к остальным точкам? Изменяя t в интервале от 0 до 0,5 с небольшим приращением, мы можем получить координаты точек кривой Безье, вычислить их расстояние от начала координат и сравнить его с расстоянием для единичного круга. Наибольшая относительная погрешность составляет 0,027253 % и достигается при t = 0,211. Каков же размер этой погрешности в пикселах? При рисовании эллипса, занимающего весь экран (размеры которого обычно не превышают 1600 х 1200 пикселов), аппроксимация четырьмя кривыми Безье в худшем случае отклоняется от истинной кривой на 0,436 пиксела. В листинге 8.6 приведены две функции. Первая функция, EllipseToBezier, рисует полный эллипс, аппроксимированный кривыми Безье. Она вычисляет 13 определяющих точек для четырех кривых Безье, используя описанный выше
Дуги 459 способ. Вторая функция, AngleArcToBezier, рисует дугу с произвольными начальным и конечным углами, аппроксимируя ее одной кривой Безье. Листинг 8.6. Рисование периметра эллипса с использованием кривых Безье BOOL El IipseToBezier(HDC hDC, int left, int top. int right, int bottom) { const double M = 0.55228474983; POINT P[13]: int dx - (int) ((right - left) * (1-M) / 2); int dy - (int) ((bottom - top) * (1-M) / 2); P[ 0].x = right; // P[ 0].y - (top+bottom)/2; // 4 3 2 P[ l].x - right; // P[ l].y - top + dy; // 5 1 P[ 2].x = right - dx; // P[ 2].у = top; // 6 0,12 P[ 3].x = (left+right)/2: // P[ 3].y = top; // 7 11 // P[ 4].x = left + dx; // 8 9 10 P[ 4].у = top; P[ 5].x « left; PC 5].у = top + dy; P[ 6].x = left; P[ 6].у = (top+bottom)/2; P[ 7].x - left; P[ 7].у = bottom - dy; P[ 8].x - left + dx; P[ 8].у - bottom; P[ 9].x - (left+right)/2: P[ 9].у - bottom; P[10].x = right - dx; P[10].y - bottom; P[ll].x - right; P[ll].y - bottom - dy; P[12].x - right; P[12].y - (top+bottom)/2; return PolyBezier(hDC. P. 13); } BOOL AngleArcToBezier(HDC hDC, int xO, int yO, int rx, int ry, double startangle. double sweepangle, double * err) { double XY[8]; POINT P[4]; // Рассчитать кривую Безье для дуги, Продолжение &
460 Глава 8. Линии и кривые Листинг 8.6. Продолжение // симметричной относительно оси х // Против часовой стрелки: (О.-В), (х.-у). (х.у), (О,В) double В = ry * sin(sweepangle/2); double С = rx * cos(sweepangle/2); double A = гх - С; double X = A*4/3; double Y = В - X * (rx-A)/B; XY[0] = C: XY[1] = - B; XY[2] = OX; XY[3] = - Y; XY[4] = C+X; XY[5] « Y; XY[6] = C; XY[7] = B; // Вернуться к исходному углу double s = sin(startangle + sweepangle/2); double с = cos(startangle + sweepangle/2); for (int i=0; i<4; i++) { P[i].x = xO + (int) (XY[i*2] * с - XY[i*2+l] * s); P[i].y = yO + (int) (XY[i*2] * s + XY[i*2+l] * c); return PolyBezier(hDC. P. 4); } Метод преобразования дуги в кривую Безье, используемый функцией Ellipse- ToBezier, не подходит для произвольных дуг; он предназначен для аппроксимации дуг с угловым размером, кратным 90°. Функция AngleArcToBezier использует новый метод аппроксимации произвольной дуги кривой Безье, хотя в тех случаях, когда угловой размер превышает 90°, следует ожидать увеличения ошибок. Функция сначала поворачивает дугу, обеспечивая ее симметричность относительно оси х\ в результате половина дуги находится выше линии у = 0, а другая половина — ниже этой линии. Такое положение дуги существенно упрощает формулы для вычисления контрольных точек. После вычисления контрольные точки поворотом возвращаются в исходную позицию. С увеличением углового размера дуги значительно возрастает относительная погрешность аппроксимации. При угле 45° погрешность равна 0,00042 %; при 90° она возрастает до 0,02725 %, а при 180° ошибка составляет уже 1,835 %. Однако настоящая «прелесть» аппроксимации дуг кривыми Безье заключается в том, что с контрольными точками кривой можно выполнять преобразования и рисовать нестандартные дуги без применения мировых преобразований GDI, поддерживаемых только на платформах семейства Windows NT. Кроме того, свойство делимости кривых Безье позволяет легко реализовать новые стили линий, не поддерживаемые в GDI и в семействе Windows 95/98. Также обратите внимание на то, что при преобразовании дуги в кривые Безье перо со сти-
Траектории 461 лем PSINSIDEFRAME работает так же, как и обычные перья со стилем PSSOLID. Если вы хотите, чтобы линия размещалась внутри контура, уменьшите ограничивающий прямоугольник перед тем, как выполнять преобразование. Траектории При выводе линий и кривых средствами GDI API необходимы два источника информации — перо и другие атрибуты контекста, определяющие внешний вид линий и кривых, а также геометрическое описание (то есть координаты точек, через которые проходит линия, и их типы). Хорошо спроектированная графическая система должна по отдельности обрабатывать эти два информационных компонента, чтобы обеспечить максимальную гибкость. Для работы с перьями используются объекты перьев GDI, а объекты контекста устройства управляют другими внешними атрибутами линий и кривых. Остается лишь найти средства для геометрического Описания линий. Так мы приходим к объектам траекторий. Траекторией (path) называется объект GDI, описывающий геометрические формы. Точнее, траектория описывает упорядоченную последовательность замкнутых или разомкнутых фигур, которые представляют собой упорядоченные последовательности линий и кривых. Объекты траекторий принадлежат к числу объектов GDI, поэтому каждой траектории соответствует манипулятор GDI и запись в таблице объектов GDI. Однако в отличие от других стандартных объектов GDI (логических перьев, контекстов устройств и т. д.), объекты траекторий остаются скрытыми от пользователей. На уровне GDI объект траектории всегда связывается с контекстом устройства и не может существовать отдельно от него. Создание, модификация, выбор, использование и уничтожение траектории выполняется GDI при вызове специальных функций. Для работы с траекториями в GDI существуют следующие функции: BOOL BeginPathCHDC hDC); BOOL EndPath(HDC hDC); BOOL AbortPath(HDC hDC); BOOL CloseFigure(HDC hDC); int GetPath(HDC hDC, PPOINT pPoints, PBYTE pTypes. int nCount); BOOL FlattenPath(HDC hDC); BOOL WidenPath(HDC hDC); -BOOL StrokePathCHDC hDC); BOOL StrokeAndFillPath(HDC hDC); BOOL FillPath(HDC hDC); HRGN PathToRegion(HDC hDC): BOOL SelectClipPathCHDC hDC, int iMode); Построение траектории Прежде чем использовать траекторию, сначала ее необходимо построить. Функция BeginPath инициирует построение траектории в контексте устройства. Контекст переходит в режим построения траектории, сбрасывает объект траектории, неявно связанный с контекстом, и инициализирует его пустым объектом. Когда контекст устройства находится в режиме построения траектории, все функции,
462 Глава 8. Линии и кривые которые могут потребоваться при построении траектории, не рисуют в контексте устройства; вместо этого их вызовы добавляются в объект траектории, связанный с контекстом устройства. При построении траектории могут использоваться только функции GDI, создающие линии и кривые; вызовы остальных функций передаются на поверхность устройства. В табл. 8.5 перечислены функции, используемые при конструировании траекторий. Обратите внимание: функции заполнения областей и даже функции вывода текста тоже генерируют линии и кривые, поэтому они включаются в траекторию. Таблица 8.5. Функции построения траекторий Функция Описание Ограничение Angl eArc, Arc, АгсТо Ellipse, Chord, Pie CloseFigure ExtTextOut, TextOut Li neTo MoveToEx PolyBezier, PolyBezierTo PolyDraw Polygon, PolyPolygon Polyline, PolylineTo, PolyPolyline Rectangle RoundRect Добавляет в траекторию линию и дугу (функция Arc — только дугу) Добавляет полный периметр эллипса, сектор или сегмент Последняя точка помечается флагом замыкания фигуры Контуры символов добавляются как отдельные замкнутые фигуры Добавляет линию Начинает новую фигуру Добавляет линию и несколько кривых Безье (функция PolyBezier — только кривые Безье) Добавляет серию фигур Добавляет один или несколько многоугольников как отдельные замкнутые фигуры Добавляет одну или несколько ломаных Добавляет прямоугольник как новую замкнутую фигуру Добавляет замкнутую фигуру из четырех линий и четырех дуг Только в семействе NT Только в семействе NT Не поддерживается для растровых шрифтов Только в семействе NT Только в семействе NT Все функции, перечисленные в таблице, так или иначе выводят кривые и линии, включаемые в объект траектории. Функции вывода пикселов (например, SetPixel) линий не выводят, поэтому в таблице они отсутствуют. Понять, как при построении траектории используются такие функции, как LineTo, Polyline, Pol yli neTo, PolyPolyline, PolyBezier, PolyBezierTo, PolyDraw, Arc, АгсТо и Angl eArc, очень просто. Объекты перьев обычно не влияют на построение траекторий, по-
Траектории 463 скольку траектория отражает лишь геометрическую форму линий и кривых; исключение составляют функции рисования дуг пером со стилем PSINSIDEFRAME, при использовании которых ограничивающий прямоугольник дуги уменьшается на половину толщины пера. Обратите внимание: в системах, не входящих в семейство NT, функции PolyDraw, Arc, АгсТо и AngleArc либо не реализованы, либо их использование при построении траекторий недопустимо. Чтобы ваша программа работала на разных платформах, при включении таких фигур в траекторию их можно преобразовать в кривые Безье. Вторую категорию функций построения траекторий составляют функции GDI, выполняющие заливку областей, — например, функции Ellipse, Chord, Pie, Polygon, Rectangle и RoundRect. В общем случае эти функции закрашивают кистью замкнутую область и обводят ее контур пером. Подробности будут рассмотрены в главе 9, а пока достаточно запомнить, что эти функции определяют замкнутые геометрические фигуры (одну или несколько). Когда указанные функции вызываются в контексте устройства, находящемся в режиме построения траектории, эти геометрические фигуры включаются в текущую траекторию этого контекста. Третью категорию функций построения траекторий составляют функции вывода текста. В Windows используются три типа шрифтов: растровые (описываются растровыми изображениями), векторные (описываются прямыми линиями) и шрифты TrueType, описываемые линиями и кривыми Безье. Если в режиме построения траектории вызывается функция вывода текста для векторного шрифта или шрифта TrueType, контур глифа включается в текущую траекторию. Помимо этих трех категорий функций, в GDI существует специальная функция CloseFigure. Вся ее работа сводится к тому, что последняя точка помечается флагом замыкания. Пометка указывает на то, что эта точка должна быть соединена линией с начальной точкой фигуры, причем для геометрического пера стык должен быть оформлен в соответствии с атрибутом соединения, а не завершения. Рассмотрим небольшой пример построения траектории. В следующем фрагменте рисуются два эллипса с одинаковыми координатами; первый эллипс рисуется пером по умолчанию, а второй — пером толщиной в 21 единицу со стилем PS_INSIDEFRAME. Если перо по умолчанию не совпадает с внутренним пером толщиной в 21 единицу, построенная траектория состоит из двух концентрических кругов. BeginPath(hDC); Ellipse(hDC. О, 0, 100, 100): HPEN hPen = CreatePen(PS_INSIDEFRAME, 21. RGBCOxFF, 0. 0)); HPEN hOld - (HPEN) SelectObject(hDC. hPen); EllipseChDC, 0. 0, 100, 100); SelectObject(hDC, hOld): DeleteObject(hPen); EndPath(hDC); Получение информации о траектории Если построение траектории прошло успешно, после вызова EndPath приложение может получить описание траектории при помощи функции GetPath GDI.
464 Глава 8. Линии и кривые В графическом механизме объект траектории представляется довольно сложной структурой данных, которая обеспечивает экономию памяти, но при этом допускает простое увеличение размеров. ПРИМЕЧАНИЕ Структура данных траектории в Windows NT/2000 подробно описана в главе 3 (раздел «WinDbg и расширение отладчика GDI»). Функция GetPath преобразует внутреннее представление траектории, используемое GDI, в два массива: массив точек и массив флагов. В массиве точек хранятся координаты всех вершин и контрольных точек, определяющих траекторию. Для каждого элемента массива точек имеется соответствующий элемент в массиве флагов. Для каждой точки флаги сообщают, принадлежит ли данная точка линии или кривой Безье, является ли она начальной или конечной точкой фигуры. Допустимые флаги перечислены в табл. 8.4. Структура данных, возвращаемая GetPath, имеет такой же формат, как структура данных, передаваемая PolyDraw. Но куда же делись эллиптические кривые? Флага точки для эллиптической кривой не существует. Эти кривые преобразуются в кривые Безье способом, похожим на описанный в разделе «Дуги». Функция GetPath возвращает точки в логической системе координат. Во внутреннем представлении точки траектории хранятся в координатах устройства в формате с фиксированной точкой, обеспечивающем максимальную точность. При вызове GetPath GDI при помощи матрицы, обратной по отношению к матрице преобразования мировых координат в координаты устройства, вычисляет координаты точек траектории в логическом пространстве. Учтите, что логические координаты представляются целыми числами. Следовательно, если преобразование из логических координат в координаты устройства сопровождается значительным масштабированием, при возвращении данных GetPath может произойти потеря точности. Вызов GetPath сопряжен с некоторыми трудностями, поскольку вызывающая сторона должна выделить память для двух массивов неизвестного размера. Как это обычно бывает в Win32 API, функция GetPath вызывается дважды: в первый раз она возвращает количество точек, а во второй раз выделенные массивы заполняются настоящими данными. Приложение также может создать два массива разумных размеров в стеке, вызвать GetPath и возиться с динамическим выделением памяти лишь в том случае, если вызов завершится неудачей (это позволяет избежать дорогостоящих операций выделения памяти из кучи). Эвристическое правило рекомендует выделять в стеке блоки фиксированных размеров, которые бы подходили для 80 % случаев, и выделять память из кучи в оставшихся 20 %. В приведенном ниже классе KPathData реализован первый способ. Класс содержит две переменные для хранения двух массивов, возвращаемых при вызове GetPath. Метод GetPathData сначала вызывает GetPath для получения количества точек. После выделения блока памяти необходимого размера функция GetPath вызывается снова для получения настоящих данных траектории. Функция Магк- Points рисует рядом с каждой точкой небольшой маркер, обозначающий ее тип.
Траектории 465 class KPathData { public: POINT * m_pPoint; BYTE * m_pFlag; i nt mjnCount : KPathDataО { m_pPoint = NULL; m_pFlag = NULL; m_nCount = 0; } -KPathDataО { if ( m_pPoint ) delete m_pPoint; if ( m_pFlag ) delete m_pFlag; } int GetPathData(HDC hDC) { if ( m_pPoint ) delete m_pPoint; if ( m_pFlag ) delete m_pFlag; mjiCount = ::GetPath(hDC, NULL, NULL. 0); if ( m_nCount>0 ) { m_pPoint = new POINT[m_nCount]; m__pFlag = new BYTE[m_nCount]; if ( m_pPoint!=NULL && m_pFlag!=NULL ) m_nCount - ::GetPath(hDC, m_pPoint. m_pFlag, m_nCount); else m_nCount = 0; } return m_nCount; } void MarkPoints(HDC hDC, bool b$howLine=true); }: Функция GetPath помогает разобраться в том, как в GDI реализованы различные функции вывода линий и кривых, поскольку с ее помощью можно увидеть данные траектории. Например, если вы хотите узнать, как AngleArc преобразуется в кривые Безье, попробуйте выполнить следующий фрагмент: BeginPath(hDC); MoveToEx(hDC, 0. 0. NULL); AngleArc(hDC. 0, 0. 150. 0. 135); POINT P[] = { -75. 20, -20. -75. 40. -40 }; PolyBezier(hDC, P. 3); CloseFigure(hDC); EndPath(hDC); KPathData org; org.GetPathData(hDC);
466 Глава 8. Линии и кривые Между вызовами BeginPath и EndPath выполняются четыре вызова функций GDI. Функция MoveToEx переводит текущую позицию курсора в начало координат, AngleArc рисует 135-градусную дугу, PolyBezier подрисовывает к дуге кривую Безье, и функция CloseFigure замыкает фигуру. После вызова GetPathData можно просмотреть данные траектории в окне отладчика, вывести их в текстовый файл или снабдить точки графическими пометками на экране. Слева на рис. 8.15 изображена траектория, определяемая приведенным выше фрагментом. Вывод осуществлялся функцией PolyDraw для данных, возвращаемых GetPath. Справа изображены точки и флаги, возвращаемые функцией GetPath. Начальная точка фигуры помечена треугольником, точки линий — прямоугольниками, точки кривых Безье — кругами, а точка замыкания фигуры обозначается закрашенным маркером. .о о оч о" о —""^ *> \ \ / ^ ч# i 4— J \/ Дг 6 о Рис. 8.15. Траектория и данные траектории, возвращаемые функцией GetPath Из рисунка видно, что AngleArc проводит линию от текущей позиции пера (0,0) в начальную точку дуги (150,0); 135-градусная дуга представляется двумя кривыми Безье. Первая кривая Безье рисует начальную 90-градусную дугу, а вторая кривая рисует оставшиеся 45°. Также рисунок показывает, что функция CloseFigure не включает дополнительный отрезок в данные траектории, а всего лишь устанавливает флаг для последней точки. В документации Microsoft нет четкого определения того, где должен находиться флаг РТ CLOSEFIGURE. Иногда он ошибочно приписывается первой контрольной точке кривой Безье. Но если проанализировать данные, возвращаемые GetPath, все становится абсолютно ясно — флаг PT_CL0SEFIGURE должен устанавливаться для последней точки фигуры. Данные, полученные от GetPath, можно непосредственно передать функции PolyDraw, как это было сделано для построения левой части рисунка. Это чрезвычайно полезная возможность, особенно если учесть, что GDI не предоставляет приложениям манипулятора траектории. Приложение может сохранить данные траектории и использовать их в будущем. Если вы хотите нарисовать точки вместо кривых (например, с целью редактирования), возвращаемый массив точек можно передать функции Polyline, как это было сделано на правой части рисунка. Перед тем как передавать данные GDI, их можно подвергнуть любым преобразованиям. Помните о том, что GDI поддерживает только аффинные преобра-
Траектории 467 зования в системах семейства NT, а в других системах мировые преобразования вообще не поддерживаются. Реализовать преобразования для рисования линий и кривых несложно. Для аффинных преобразований, отображающих линии в линии, достаточно преобразовать все контрольные точки, а затем провести вывод с теми же флагами. Для не-аффинных преобразований, которые могут отображать линии в кривые, для повышения точности вам, возможно, придется разбить линии и кривые на сегменты меньших размеров. Преобразование объекта траектории В GDI для объектов траекторий определены два преобразования: замена криволинейных участков прямолинейными и утолщение линий. Функция Fl attenPath преобразует все кривые объекта траектории в последовательность отрезков, обеспечивающих приемлемую аппроксимацию кривой в логическом координатном пространстве. Функция FlattenPath ограничивается преобразованием кривых Безье. При этом используется рекурсивный алгоритм вроде приведенного в листинге 8.3: кривая делится надвое до тех пор, пока погрешность не станет незначительной. Название функции создает неверное впечатление, будто она приводит к значительному искажению кривой. На самом деле функция FlattenPath обеспечивает наилучшую аппроксимацию в логических координатах. Конечно, целочисленное представление приводит к некоторой потере точности, но если результат не масштабируется с большим коэффициентом, различия практически незаметны. Если результат должен выводиться в контексте устройства с высоким разрешением, приложение может увеличить разрешение логического координатного пространства или реализовать собственную версию Fl attenPath с вещественными вычислениями. Функция Fl attenPath вызывается после того, как EndPath завершает построение действительной траектории. Она модифицирует текущий объект траектории в контексте устройства. Обновленный объект траектории остается выбранным в контексте и может использоваться другими функциями, работающими с траекториями (например, функцией GetPath). При достаточно сложной форме траектории аппроксимация может потребовать немалых расходов памяти. Пример использования функции FlattenPath иллюстрирует рис. 8.16. Фигура слева была нарисована функцией PolyDraw по данным, полученным в результате вызова FlattenPath; справа выведена та же фигура с маркерами точек. Левая фигура почти не отличается от фигуры на рис. 8.15, однако при взгляде на правую фигуру становится видно, что точки кривой были заменены множеством линейных точек, довольно точно воспроизводящих форму траектории. Задача преобразования кривых в прямые часто встречается на практике, поскольку с линиями работать гораздо удобнее, чем с кривыми. Не существует простых формул, которые бы позволяли вычислить длину кривой Безье по определяющим точкам. В системах автоматизированного проектирования пользователи работают с шаблонами, созданными на основе кривых Безье; преобразование кривой в набор отрезков позволяет легко вычислить ее длину. В результате линейной аппроксимации замкнутая фигура практически превращается в многоугольник, а для вычисления площади многоугольника, его ограничивающего прямоугольника и проверки пересечений существует немало готовых алгорит-
468 Глава 8. Линии и кривые мов. Информация о длине кривой также применяется при рисовании стилевых линий. Процесс рисования стилевой линии можно представить как многократное чередование вывода отрезков и промежутков фиксированной длины. В разделе «Пример: рисование нестандартных стилевых линий» показано, как линейная аппроксимация траекторий помогает строить нестандартные стилевые линии. 0и Two Regions RGN_AND RGN_OR RGN_XOR RGN_DIFF RGN_COPY Рис. 8.16. FlattenPath: линейная аппроксимация кривой Вторым средством преобразования траекторий в GDI является функция WidenPath. В документации Microsoft сказано, что WidenPath преобразует текущую траекторию в область, которая была бы закрашена при прорисовке траектории пером, выбранным в этот момент в контексте устройства. Обратите внимание: WidenPath преобразует траекторию в область. Но траектория есть траектория, как она может быть областью? Никаких пояснений на этот счет MSDN не дает. В действительности WidenPath переопределяет текущую траекторию по периметру области, которая была бы закрашена при прорисовке траектории текущим пером. Функция WidenPath работает лишь в том случае, если текущее перо не является косметическим пером, созданным функцией ExtCreatePen. Для перьев, созданных функцией CreatePen, и геометрических перьев, созданных функцией ExtCreatePen, функция WidenPath рассчитывает периметр всей области с учетом толщины пера, его стиля (включая атрибуты соединения и завершения) и углового лимита. Функция WidenPath всегда генерирует замкнутые фигуры. Кроме того, она, как и FlattenPath, преобразует кривые в линии (как говорилось выше, с линиями удобнее работать). После вызова WidenPath вызывать FlattenPath уже не нужно, однако если вызвать FlattenPath перед вызовом WidenPath, сгенерированная траектория будет содержать больше точек (и более короткие отрезки). Обратите внимание на одно важное обстоятельство (по крайней мере, в Windows NT/2000): чтобы перо действовало при вызове WidenPath, оно должно быть выбрано в контексте устройства до последнего вызова, порождающего графический вывод. Если перо было выбрано после последней графической функции, но до CloseFigure и EndPath, будет использовано предыдущее перо. На рис. 8.17 показана довольно-таки уродливая картинка, созданная функцией WidenPath. Изображения были построены для геометрического пера толщиной в 17 единиц с плоским завершением и заостренным соединением. Функция WidenPath преобразует замкнутую фигуру в две замкнутые фигуры — первая на половину толщины пера выходит за границы траектории, а вторая на такую же
Траектории 469 величину уходит внутрь. Несомненно, функция WidenPath должна преобразовывать открытую траекторию в одну замкнутую фигуру, окружающую исходную траекторию на расстоянии в половину толщины пера с каждой стороны. Если бы вместо сплошного пера использовалось стилевое перо, для каждого пунктирного отрезка или точки была бы сгенерирована отдельная замкнутая фигура. Рис. 8.17. WidenPath преобразует одну замкнутую фигуру в две замкнутые фигуры Траектория, сгенерированная функцией WidenPath, уже разбита на прямолинейные отрезки. Но похоже, что GDI при этом не ограничивается тривиальным вызовом Fl attenPath с последующим расширением траектории. Если сделать это в приложении, сгенерированная траектория будет содержать больше точек и «петель». Уродливые петли на левом рисунке появляются при соприкосновении двух толстых линий под углом менее 180°. Они соответствуют перекрывающимся зонам, которые прорисовываются двумя линиями по отдельности. Если бы результат вызова WidenPath использовался только для реализации утолщенных геометрических линий, петли были бы полностью скрыты при закраске замкнутых фигур. Но если приложение захочет воспользоваться этим результатом для рисования двух замкнутых фигур, внутренней и внешней, ему придется основательно потрудиться над чисткой траекторий. Возможно, функция WidenPath или какая-нибудь ее внутренняя реализация используется GDI для преобразования геометрических линий в закрашиваемые области. Это также объясняет, почему при определении геометрических логических перьев используется структура L0GBRUSH, как и при определении кисти. С точки зрения GDI рисование геометрическим пером является усложненной формой заливки областей. Функция WidenPath позволяет приложению получить данные траектории, которые будут использоваться GDI для рисования геометрических линий. Впрочем, Microsoft нигде не объясняет, зачем приложению могут понадобиться эти внутренние данные. Вспомните, что говорилось выше — в GDI поддерживаются только аффинные преобразования; непосредственная поддержка не-аффинных преобразований (таких, как 1-, 2- и 3-точечные преобразования перспективы) отсутствует. Отличительной особенностью геометрических линий является наличие заметной толщины. С другой стороны, нарисованная геометрическая линия имеет постоянную толщину. Трехмерное изображение должно имитировать
470 Глава 8. Линии и кривые эффект перспективы, поэтому удаленные объекты, в том числе и геометрические линии, должны уменьшаться в размерах. Функция WidenPath обеспечивает необходимое «разделение труда» между GDI и приложениями. Приложение может получить данные траектории после применения WidenPath, применить к ним преобразование перспективы или любое другое преобразование, изменяющее толщину линий, и вернуть полученную траекторию для ее закраски кистью. В результате вы получите линии с переменной толщиной. В следующем фрагменте определяется абстрактный класс для выполнения двумерных преобразований K2DMap и производный от него класс KBiLinearMap, отображающий прямоугольное окно в произвольный четырехугольник. Метод K2DMap::Map выполняет отображение отдельных точек в данные, относящиеся к траектории. class K2DMap { public: virtual Mapdong & px, long & py) = 0; }: class KBiLinearMap : public K2DMap { double xOO, yOO, xOl. yOl. xlO. ylO, xll. yll; double orgx, orgy, width, height; public: void SetWindow(int x, int y, int w. int h) { orgx = x; orgy = y: width = w; height = h; } void SetDestination(POINT P[]) { xOO = P[0].x: yOO - P[0].y: xOl = P[l].x; yOl = P[l].y: xlO - P[2].x: ylO = P[2].y: xll = P[3].x; yll = P[3].y; } virtual Mapdong & px. long & py) { double x = (px - orgx ) / width: double у = (py - orgy ) / height; px - (long) ( (1-х) * ( xOO * (1-y) + xOl * у ) + x * ( xlO * (1-y) + xll * у ) ); py - (long) ( (1-х) * ( yOO * (1-y) + yOl * у ) + x * ( ylO * (1-y) + yll * у ) ): } }:
Траектории 471 Графические операции с использованием траекторий Непосредственная прорисовка траектории выполняется несколькими функциями: StrokePath, Fill Path и StrokeAndFi 11 Path. Функция StrokePath рисует линии и кривые, входящие в текущую траекторию, используя текущее перо и угловой лимит контекста устройства. С концептуальной точки зрения работа StrokePath эквивалентна получению данных траектории функцией GetPath и вызову функции PolyDraw — нарисованные линии будут одинаковыми. Главное различие заключается в том, что GetPath и PolyDraw не изменяют траектории в контексте устройства, а функция StrokePath освобождает ее после обводки. Следовательно, после вызова StrokePath приложение должно построить новую траекторию, если она ему нужна. Существует обходное решение — вызвать функцию SaveDC перед вызовом StrokePath и RestoreDC после него. Всего одна пара функций предохраняет объект траектории от уничтожения. Не рекомендуется вызывать StrokePath после вызова WidenPath с тем же утолщенным геометрическим пером. Если утолщенное перо, использованное для расширения траектории, остается выбранным в контексте, исходная траектория будет прорисована пером двойной толщины, что приведет к появлению уродливых пробелов и увеличению петель. При использовании тонкого пера уродливые петли, порожденные функцией WidenPath, становятся хорошо заметными (как в левой части рис. 8.17). Ниже приведен небольшой фрагмент, который использует функцию WidenPath для расширения траекторий очень толстыми геометрическими перьями, а затем вызывает функцию StrokePath с тонким косметическим пером для обводки траекторий, сгенерированных функцией WidenPath: for (int i=0; i<3; i++) { const WideStyle[] = { PS_ENDCAP_SQUARE | PS_JOIN_MITER. PS_ENDCAP_ROUND | PS_J0IN_R0UND, PSJNDCAPJLAT | PSJOINJEVEL }: const ThinStyle[] - { PS_ALTERNATE, PS_D0T. PS_S0LID }; const Color [] - { RGB(0xFF, 0, 0), RGB(0. 0. OxFF). RGB(O.O.O) }; KPen wide(PS_GEOMETRIC | PSJOLID | WideStyle[i], 70. RGB(0. 0, OxFF), 0, NULL); wide.Select(hDC); BeginPath(hDC); MoveToEx(hDC, 150, 0, NULL); LineTo(hDC, 0. 0): LineTo(hDC, 100, -100); EndPath(hDC); WidenPath(hDC); wide.UnSelect(hDC); if (1--2 ) {
472 Глава 8. Линии и кривые KPathData pd; pd.GetPathData(hDC); pd.MarkPoints(hDC, false); KPen thin(PS_COSMETIC thin.Select(hDC); StrokePath(hDC); thin.UnSelect(hDC); ThinStyle[i], 1. Color[i], 0, NULL); Приведенный фрагмент выполняется трижды: для пера с квадратным завершением и заостренным соединением, с закругленными завершением и соединением и, наконец, для пера с плоским завершением и усеченным соединением. Каждый раз строится траектория из двух линий, расположенных под углом 45°. Траектории расширяются и обводятся разными косметическими линиями. Для последнего пера точки расширенной траектории помечаются в соответствии с их типами. Этот фрагмент наглядно показывает, как рисуются линии с применением геометрических перьев с различными атрибутами (рис. 8.18). Рис. 8.18. Применение функций WidenPath и StrokePath Рисунок также наглядно иллюстрирует процесс возникновения петли при использовании функции WidenPath. Для пера с плоским завершением и усеченным соединением (на рисунке обозначено сплошной черной линией) WidenPath строит замкнутую фигуру, которая начинается с правого нижнего угла, помеченного треугольником. Траектория следует к центру другого конца линии, где она встречается с другой линией. После этого траектория переходит на вторую линию, следует по всему периметру до соединения, рисует соединение и замыкает фигуру. Петля генерируется в том случае, если две линии пересекаются под углом меньше 180°. Вероятно, алгоритму GDI следовало бы вычислить область пересечения периметров двух линий и удалить петли вместо того, чтобы просто следовать линии периметра. Для платформ, не входящих в семейство NT, функция StrokePath особенно важна для геометрических перьев, поскольку их атрибуты завершения и соединения требуются только при включении линий и кривых в траекторию. Другими
Пример: рисование нестандартных стилевых линий 473 словами, при вызове таких функций, как LineTo, Arc и BezierTo вне построения траектории, в этих системах атрибуты завершения и соединения геометрических перьев игнорируются, и вместо них используются значения по умолчанию. Функция Fill Path закрашивает область, занимаемую траекторией, при помощи кисти. Функция StrokeAndFi 11 Path закрашивает область, как Fill Path, и рисует линии и кривые, как StrokePath. С другой стороны, следующие подряд вызовы Fill Path и StrokePath не заменяют StrokeAndFi 11 Path, поскольку все эти функции перед возвращением управления уничтожают объект траектории. Функции Fi 11- Path и StrokeAndFi 11 Path рассматриваются в следующей главе. Преобразование пути в регион Остается рассмотреть еще две функции GDI, предназначенные для работы с траекториями. Функция PathToRegion преобразует текущую траекторию в контексте устройства в независимый регион, который может использоваться приложением для различных целей. Функция SelectClipPath преобразует текущую траекторию контекста устройства в регион и задействует его для обновления региона отсечения, определенного приложением в данном контексте. Мы еще вернемся к этим двум функциям после более подробного описания регионов и отсечения. Пример: рисование нестандартных стилевых линий На платформах, не входящих в семейство NT, утолщенные геометрические стилевые линии не поддерживаются. Иначе говоря, графические приложения, ориентированные на все платформы, не могут рассчитывать на поддержку утолщенных геометрических линий на уровне GDI. Впрочем, даже на платформах семейства NT поддержка стилевых линий в GDI обладает недостаточными возможностями. Вы не можете повторять произвольный пользовательский узор вдоль траектории, определять собственные типы завершений и соединений. Разбиение траектории на отрезки упрощает реализацию этих алгоритмов. С другой стороны, непосредственная работа с кривыми Безье экономит память и повышает точность. В листинге 8.7 приведены определения двух классов, обеспечивающих значительно большие возможности рисования стилевых линий по сравнению с GDI. Листинг 8.7. Классы для работы со стилевыми линиями class KDash { public: virtual double GetLengthdnt step) { return 10; } Продолжение &
474 Глава 8. Линии и кривые Листинг 8.7. Продолжение virtual BOOL DrawDash(double xl, double yl, double x2, double y2, int step) { return FALSE; } class KStyleCurve { i nt m_step; double m_xl, m_yl; KDash * m_pDash; public: KStyleCurve(KDash * pDash) { m_xl = 0; m_yl = 0; m_step = 0; m_pDash= pDash; } BOOL MoveTo(double x, double y); BOOL LineTo(double x, double y); BOOL PolyDraw(const POINT *ppt, const BYTE *pbTypes, int nCount); BOOL KStyleCurve::LineTo(double x. double y) { double x2 = x; double y2 = у; double curlen - sqrt((x2-m_xl)*(x2-m_xl) + (y2-m_yl)*(y2-m_yl)); double length = m_pDash->GetLength(m_step); while ( curlen >- length ) { double xl = m_xl; double yl - m_yl; m_xl += (x2-m_xl) * length / curlen; m^l +- (y2-mj/l) * length / curlen; if ( ! m_pDash->DrawDash(xl, yl. m_xl, m__yl, m_step) ) return FALSE; curlen -- length; m_step ++; length - m_pDash->GetLength(m_step); } return TRUE; } BOOL KStyleCurve::PolyDraw(const POINT *ppt. const BYTE *pbTypes. int nCount)
Пример: рисование нестандартных стилевых линий 475 { int lastmovex = 0; int lastmovey = 0; for (int i=0; i<nCount; i++) { switch ( pbTypes[i] & - PT_CLOSEFIGURE ) { case PT_M0VET0: m_xl = lastmovex = ppt[i].x; m_yl = lastmovey = ppt[i].y; break; case PT_LINET0: if ( ! LineTo(ppt[i].x. ppt[i].y) ) return FALSE; break; } if ( pbTypes[i] & PT_CLOSEFIGURE ) if ( ! LineTo(lastmovex, lastmovey) ) return FALSE; } return TRUE; } Задача рисования стилевых линий разделяется на две подзадачи, каждая из которых решается отдельным классом. Класс KDash управляет циклом чередования и выводом пунктирных отрезков. Он определяется в виде абстрактного класса с двумя виртуальными методами, позволяющими рисовать линии разных стилей для разных классов. Метод GetLength возвращает длину пунктирного отрезка по параметру step; в каком-то отношении он выполняет те же функции, что и массив с описанием пользовательского стиля. Если в стилевой линии используются одинаковые длины пунктирных отрезков и промежутков, GetLength просто возвращает константу. Если цикл чередования определяется фиксированным массивом, GetLength может вернуть lpStyle[step X dwStyleCount]. Метод DrawDash рисует отрезки и промежутки. В качестве параметров ему передаются две точки с вещественными координатами (для повышения точности) и номер текущего сегмента step. По значению этого параметра функция определяет, является ли текущий сегмент отрезком или промежутком. Например, при рисовании пунктирной линии функция может рисовать каждый второй сегмент. Класс KStyleCurve управляет делением линий и кривых на сегменты нужной длины. В приведенном варианте он содержит методы MoveTo для определения начальной позиции, LineTo для рисования линий и PolyDraw для рисования данных, возвращаемых GetPath. При необходимости класс легко расширяется для поддержки кривых Безье и эллиптических кривых без использования траекторий. Конструктор класса KStyleCurve получает указатель на объект класса KDash, который может потребоваться для получения информации. В классе KStyleCurve главная роль принадлежит функции KStyleCurve::LineTo. Функция получает от класса KDash информацию о текущей длине сегмента и пытается «отрезать» от
476 Глава 8. Линии и кривые текущей линии сегмент указанной длины. Процесс повторяется до тех пор, пока остаток линии не окажется слишком коротким; в этом случае обработка откладывается до следующей линии. Реализация всегда возвращает сегмент необходимого размера; все остатки меньших размеров сливаются со следующей линией. Такой подход гарантирует, что сегменты будут иметь требуемую длину, но иногда он может приводить к срезанию углов. Более изощренное решение должно поддерживать изгибы отрезков, их равномерное распределение и отсечение. Взаимосвязь между классами KStyleCurve и KDash очень похожа на отношения между функцией LineDDA и ее функциями косвенного вызова. Главное отличие заключается в том, что KStyleCurve позволяет работать с кривыми, а в классе KDash косвенный вызов организуется при помощи виртуальных функций C++. Ниже приведен пример класса KDiamond, производного от класса KDash. Класс KDiamond рисует стилевые линии, состоящие из ромбов разных размеров и цветов. Функция GetLength возвращает значение 8 или 16 в зависимости от четности или нечетности номера текущего сегмента. Функция DrawDash рассматривает точки (х1,у1) и (х2,у2) как углы ромба и использует их для вычисления двух оставшихся углов, после чего рисует ромбы как многоугольники различных цветов. Рисование многоугольников рассматривается в следующей главе. class KDiamond : public KDash { HDC mJiDC: virtual double Getl_ength(int step) { return 8 + (step & 1) * 8; } virtual BOOL DrawDash(double xl. double yl. double x2. double y2. int step); public: KDiamond(HDC hDC) { mJiDC = hDC; } }: BOOL KDiamond::DrawDash(double xl. double yl. double x2. double y2. int step) { HBRUSH hBrush - CreateSolidBrush(PALETTEINDEX(step * 20)); HGDIOBJ hOld = SelectObject(m_hDC. hBrush); SelectObject(m_hDC. GetStockObject(NULL_PEN)); double dx = (x2 - xl)/2; double dy = (y2 - yl)/2; POINT P[5] - { (int)xl. (int)yl. (int)((xl+x2)/2-dx). (int)((yl+y2)/2+dy). (int)x2. (int)y2. (int)((xl+x2)/2+dx). (int)(yl+y2)/2-dy). (int)xl. (int)yl }; Polygon(m_hDC. P. 5);
итоги 477 SelectObject(m_hDC. hOld); DeleteObject(hBrush); return TRUE; } На рис. 8.19 показано, как работает более гибкий класс, который вместо пунктирных отрезков рисует круги, квадраты, ромбы и треугольники. Рис. 8.19. Рисование нестандартных стилевых линий Итоги В этой главе, основанной на материале предыдущих глав, подробно рассматривается процесс рисования линий и кривых в Windows GDI. В ней изучаются бинарные растровые операции, режимы заполнения фона, логические перья, линии, кривые Безье, дуги и траектории, в которых линии объединяются с кривыми. Бинарные растровые операции определяют способ объединения пикселов пера с пикселами приемника и формирования новых пикселов приемника. Самым распространенным случаем является режим R2C0PYPEN, при котором цвет приемника заменяется цветом пера. Инвертируемые растровые операции часто используются в интерактивной компьютерной графике — например, для рисования эластичных линий и перекрестий, а также временных контуров перетаскиваемых объектов. Набор из шестнадцати бинарных растровых операций не так уж велик. Альфа-наложение, которое также является разновидностью объединения цвета выводимых пикселов с пикселами приемника, при рисовании линий не поддерживается. Применительно к линиям и кривым режим заполнения фона определяет, должны ли выводиться пикселы фона между отрезками. Режим заполнения фона относится только к простым перьям; косметические и геометрические перья рисуют только пикселы линий. Логические перья, определяемые в платформах на базе NT, обладают достаточно серьезными возможностями. Однако на платформах Win32, не входящих в семейство NT, поддержка перьев ограничена, что затрудняет написание уни-
478 Глава 8. Линии и кривые версальных приложений. В системах Windows 95/98 для геометрических перьев не поддерживаются стилевые линии, утолщенные линии центруются неточно, завершения и соединения поддерживаются только функциями построения траекторий, а альтернативные и пользовательские стили перьев не поддерживаются. В разделе «Пример: рисование нестандартных стилевых линий» приведен пример класса, позволяющего рисовать нестандартные стилевые линии. Этот класс дает возможность имитировать несуществующие геометрические стилевые перья. В GDI предусмотрена достаточно солидная поддержка рисования линий, кривых Безье и эллиптических кривых. Тем не менее функции Angl еАгс, АгсТо и PolyDraw реализованы только в системах семейства NT. В этой главе приведено достаточно теоретического материала и примеров, чтобы позволить приложениям создавать свои собственные реализации этих функций. Мы подробно рассмотрели теорию кривых Безье, а также процесс преобразования полных эллипсов и эллиптических дуг в кривые Безье. При решении этих практических задач используются несложные математические выкладки. Траектории — очень важный аспект графического программирования GDI, которому обычно не уделяют должного внимания. В этой главе показано, что собой представляет траектория, как она строится, преобразуется и используется при выводе. Практическое применение траекторий продемонстрировано на примере построения нестандартных стилевых линий и применения не-аффинных преобразований для построения линий переменной толщины. Траектории широко используются графическим механизмом Windows NT/2000 и интерфейсом DDI, через который графический механизм взаимодействует с драйверами графических устройств. За подробным описанием внутреннего представления траекторий обращайтесь к главе 3. Геометрические линии, которые могут иметь значительную толщину, несомненно реализуются посредством заливки областей, а не простым размещением пикселов вдоль траектории. По этой причине в этой главе встречается несколько ссылок на материал следующей главы. Настало время перейти к новому рубежу — заливке областей в GDI. Пример программы Эта глава сопровождается всего одним примером программы LineCurve (табл. 8.6). Впрочем, программа достаточно велика и в ней нашли отражение все темы, рассмотренные в главе. Кстати говоря, все рисунки к этой главе представляют собой снимки экранов программы LineCurve. Таблица 8.6. Программа главы 8 Каталог проекта Описание Samples\Chapt_08\LineCurve Меню Test содержит десятки команд, демонстрирующих применение бинарных растровых операций, перьев DC, простых и расширенных перьев, линий, кривых Безье, дуг, траекторий и нестандартных пользовательских стилей, описанных в разделе «Пример: рисование нестандартных стилевых линий»
Глава 9 Замкнутые области У истоков современной математики лежат две старые математические задачи — поиск касательной к заданной кривой и вычисление площади внутри заданной замкнутой кривой. Первая проблема решается в области дифференциального исчисления, а вторая — в области интегрального. Некая аналогия прослеживается и в графическом интерфейсе Windows API — одни функции выстраивают пикселы вдоль линий и кривых, а другие заполняют замкнутые фигуры, образованные линиями и кривыми. От одномерных линий и кривых, подробно описанных в главе 8, мы переходим в новое измерение и займемся заливкой областей. В этой главе рассматриваются основные темы, связанные с заливкой, — кисти; базовые структуры данных кистей; прямоугольники и регионы; основные виды геометрических фигур (прямоугольники, многоугольники, эллипсы, секторы и сегменты) и такое модное направление, как градиентные заливки. Кисти В процессе закраски области приходится учитывать множество факторов: геометрическую форму, правила ее интерпретации, растровые операции, режим заполнения фона, цвет и узор. В графическом интерфейсе Windows API сведения о цвете и узоре, используемом при заливке областей, группируются в объекте кисти. Как ни странно, рисовать кистью в Windows проще, чем пером, потому что перья рисуют линии и кривые с разной толщиной и стилем, а у кистей такая возможность не предусмотрена. Цвет кисти определяет основной цвет пикселов при закраске замкнутых фигур, а узор создает различные повторяющиеся эффекты заполнения. Объект логической кисти В GDI существует несколько функций для создания объектов кистей, или выражаясь точнее — объектов логических кистей. Логическая кисть описывает требо-
480 Глава 9. Замкнутые области вания, предъявляемые к заливке со стороны приложения (прежде всего цвет и узор). Она сообщает драйверу устройства, как должна выглядеть заливка, однако драйверы устройств для представления собственной интерпретации кисти работают с различными структурами данных, которые обычно называются «физическими кистями». Внутренними структурами данных логической кисти управляет GDI, как и структурами данных других объектов (контекстов устройств, логических перьев, логических шрифтов и т. д.). После создания логической кисти ее манипулятор возвращается приложению и используется при последующих ссылках на кисть. Манипуляторы объектов GDI описываются общим типом HGDI0BJ; для манипуляторов логических кистей зарезервирован тип HBRUSH. С каждым контекстом устройства связывается атрибут логической кисти, для работы с которым используются функции GetCurrentObject, SelectObject, GetObject и EnumObjects. Эти функции, рассматривавшиеся в главе 8 применительно к объектам перьев, выполняют одни и те же операции с разнотипными объектами GDI. Объект логической кисти, как и любой другой объект GDI, расходует ресурсы пользовательского процесса и ядра, а также занимает по крайней мере один элемент в таблице объектов GDI, поэтому ненужные логические кисти следует удалять функцией Del eteObject. Стандартные кисти GDI определяет несколько стандартных объектов кистей, которые могут легко использоваться любым приложением. Чтобы получить манипулятор стандартной кисти, достаточно вызвать функцию GetStockObject с одной из констант BLACK_BRUSH, DKGRAY_BRUSH, GRAYJRUSH, LTGRAYJRUSH, WHITEJRUSH, NULL_BRUSH (то же, что и H0LL0W_BRUSH) или DCBRUSH. Черная, темно-серая, серая, светло-серая и белая стандартные кисти представляют собой однородные кисти с различными уровнями интенсивности серого цвета. По умолчанию в контексте устройства выбирается белая кисть. При выборе пустой стандартной кисти (NULLBRUSH или H0LL0WBRUSH) внутренняя часть области не закрашивается (по аналогии с тем, как пустое перо не рисует линий). Поскольку функция GetStockObject работает с обобщенными объектами GDI, результат ее вызова для стандартных объектов кистей обычно преобразуется к типу HBRUSH. Стандартная кисть DC, возвращаемая вызовом GetStockObject(DC_BRUSH), принадлежит к числу новых средств Windows 98/2000. Кисти DC, как и перья DC, являются псевдообъектами GDI и могут изменять цвет после выбора в контексте устройства. Для работы с цветом кисти DC, выбранной в контексте устройства, используются следующие функции: C0L0RREF GetDCBrushColor(HDC hDC); C0L0RREF SetDCBrushColorCHDC hDC. C0L0RREF crColor); Функция GetDCBrushColor возвращает текущий цвет кисти DC; функция Set- DCPenColor назначает новый и возвращает старый цвет. Эти функции могут использоваться даже в том случае, если кисть DC не выбрана в контексте устройства, однако в этом случае они ни на что не влияют. Стандартные объекты заранее создаются операционной системой и совместно используются всеми процессами, работающими в системе. После завершения
Кисти 481 работы со стандартными объектами их манипуляторы удалять не нужно. Впрочем, вызов Del eteOb ject для манипулятора стандартной кисти абсолютно безопасен — функция просто возвращает TRUE, не выполняя никаких действий. Пользовательские кисти Вряд ли кого-нибудь обрадует картина, нарисованная всего пятью оттенками серого цвета. Для создания или получения разноцветных пользовательских кистей с интересными узорами используются следующие функции: HBRUSH CreateSolidBrushCCOLORREF crColor); HBRUSH CreateHatchBrushdnt fnStyle, COLORREF crRef); HBRUSH CreatePatternBrush(HBITMAP hbmp); HBRUSH CreateDIBPatternBrushPt(CONST VOID * IpPackedDIB, UINT iUsage); HBRUSH CreateDIBPatternBrush(HGLOBAL hglbDIBPacked. UINT fuColorSpec); HBRUSH GetSysCo1orBrush(int nlndex); Однородные кисти Проще всего создаются однородные кисти — для этого достаточно указать цвет. Функция CreateSolidBrush создает только логическую кисть. Когда манипулятор кисти выбирается в контексте устройства, GDI и драйвер устройства должны согласовать между собой реализацию кисти. Если контекст устройства не использует палитру, описатель цвета без особых хлопот преобразуется в составляющие RGB. С другой стороны, для контекстов, использующих палитру, описатель цвета должен преобразовываться в индекс палитры. Если совпадение отыскивается, найденный индекс задеиствуется при выводе; в противном случае устройство имитирует пикселы нужного цвета, комбинируя доступные цвета путем так называемого смешения (dithering). Смешение позволяет воспроизводить дополнительные цвета на 16- и 256-цветных видеоадаптерах и даже имитировать вывод в оттенках серого на черно-белых принтерах. В следующем фрагменте показано, как создать однородную кисть и выбрать ее в контексте устройства. Программа рисует прямоугольник 8x8 каждого из 256 цветов в интервале от синего до белого и отображает увеличенные изображения 16-цветных прямоугольников, расположенных на диагональной линии. Если запустить программу в 256-цветном видеорежиме, вы увидите узоры смешения, показанные на рис. 9.1. // Прямоугольник не обводится контуром SelectObject(hDC. GetStockObject(NULL_PEN)); for (int y=0; y<16; y++) for (int x=0; x<16; x++) { HBRUSH hBrush = CreateSolidBrush(RGB(y*16+x. y*16+x, OxFF)); HGDIOBJ hOld = SelectObject(hDC, hBrush); Rectangle(hDC, 235+x*10. y*10, 235+x*10+9, y*10+9); if ( x==y ) // Увеличить цветные квадраты по диагонали
482 Глава 9. Замкнутые области ZoomRectChDC. 235+х*10. у*10. 235+х*10+8. у*10+ 80*(х£8). 180+80*(х/8). 6); SelectObject(hDC, hOld): DeleteObject(hBrush); } SelectObjectChDC. GetStockObject(BLACK PEN)); яяяяяяяяяяяяяжяя яяявяяняняняняяя вяякяяккяякяяяяя Рис. 9.1. Смешение при рисовании однородной кистью на устройствах, использующих палитру Для цветов, не входящих в текущую аппаратную палитру, смешение создает узоры, цвет которых в среднем приближается к исходным цветам. При использовании двух чистых цветов в узоре 8x8 возможно 64 уровня интенсивности цвета. На устройствах с низким разрешением смешение порождает довольно заметные скопления точек. Но на устройствах высокого разрешения (например, на современных принтерах) драйвер устройства обычно задействует изощренные полутоновые алгоритмы для имитации однородных оттенков цвета. Благодаря ничтожно малым размерам точек современные принтеры обеспечивают качество печати, близкое к качеству фотографических изображений. Штриховые кисти Функция CreateHatchBrush создает логическую кисть с одним из шести стандартных узоров, образующих равномерный рисунок в виде повторяющихся линий. Тип штрихового узора определяется параметром fnStyle (рис. 9.2). В верхней части рисунка показан результат применения штриховых кистей для заполнения прямоугольных областей. Как нетрудно убедиться, рисунок создается многократным повторением маленьких «блоков», изображенных в нижней части рисунка. Обычно для реализации штриховых кистей драйверы устройств используют растры размером 8x8 пикселов, однако архитектура интерфейса DDI позволяет выбирать и другие реализации в зависимости от таких факторов, как разрешение.
Кисти 483 ||1щ wMm HS_HORIZONTAL HS_VERTICAL HS_FDIAGONAL HS_BDIAGONAL HS_CROSS HS_DIAGCROSS Рис. 9.2. Стили штриховых кистей При работе со штриховыми кистями необходимо учитывать размер штрихового узора, цвет, режим заполнения фона и выравнивание штрихового узора. Штриховые кисти обычно определяются растрами 8 х 8 в единицах устройства (то есть 8 пикселов на 8 пикселов). В отличие от большинства других средств GDI, размер узора штриховой кисти не определяется в логических единицах. Например, при выводе на экран штриховые кисти всегда используют шаблоны 8x8 пикселов независимо от действующих преобразований из логических координат в физические. На обычном экране такой узор смотрится нормально, но при сильном уменьшении он начинает выглядеть странно из-за искажения пропорций. С другой стороны, если приложение использует штриховые кисти при выводе на принтер с высоким разрешением, заливка, созданная с применением штриховой кисти, может вообще не порождать сколько-нибудь заметного узора. Какой размер соответствует блоку 8x8 пикселов при печати с разрешением 2400 dpi? — 1/300 дюйма. Чтобы штриховой узор имел те же физические размеры, как и на экране с разрешением 120 dpi, принтер с разрешением 2400 dpi должен использовать штриховые узоры размером 160 х 160 пикселов. Следовательно, если приложение поддерживает просмотр или печать документов в различных масштабах, стандартных штриховых кистей GDI лучше избегать. Вместо этого следует искать альтернативные решения, масштабируемые с учетом разрешения устройства и отображения логических координат в координаты устройства. В штриховых кистях пикселы делятся на основные (на рис. 9.2 выделены темным цветом) и фоновые (изображены светлым цветом). Основные пикселы выводятся всегда, а фоновые пикселы выводятся лишь в том случае, если установлен режим заполнения фона OPAQUE. Для работы с атрибутом режима заполнения фона в контекстах устройств используются функции GetBkMode и SetBkMode. Второй параметр функции CreateHatchBrush (параметр crRef) задает основной цвет, а фоновый цвет определяется атрибутом цвета фона в контексте устройства (функции GetBkColor и SetBkColor). Как для основного, так и для фонового цвета GDI подбирает ближайший чистый (присутствующий в системной палитре) цвет и использует его при выводе; смешение для штриховых кистей не поддерживается. При использовании штриховых кистей несколькими графическими объектами или при поддержке прокрутки может возникнуть проблема совмещения узоров. Для выравнивания кистей в контекстах устройств GDI задействует атрибут базовой точки кисти, для работы с которым требуются следующие функции:
484 Глава 9. Замкнутые области BOOL GetBrushOrgExCHDC hDC. LPPOINT lppt): BOOL SetBrushOrgEx(HDC hDC. int nxOrg. int nyOrg, LPPOINT lppt): Базовая точка кисти представляет собой точку (ЬхО, ЬуО) в системе координат устройства, которая определяет привязку левого верхнего пиксела штрихового узора; остальные пикселы выстраиваются соответствующим образом. Выражаясь точнее, точке (х,у) в системе координат устройства соответствует точка узора Pattern[(x-bxO) % pattern_width. (y-byO) % pattern_height] По умолчанию координаты базовой точки кисти равны (0,0). Чтобы обеспечить правильное выравнивание кистей после изменения преобразований или отображений, следует вызвать функцию SetBrushOrgEx. Следующий фрагмент обеспечивает выравнивание кистей по точке (0,0) в логической системе координат: POINT Р = { 0. О }; // Начало координат LPtoDP(hDC, &P. 1); // Отображение в координаты устройства SetBrush0rgEx(hDC. P.x. Р.у, NULL); Раньше штриховые кисти очень часто использовались в деловой графике — например, для выделения разных данных в гистограммах или круговых диаграммах. С появлением современных видеоадаптеров, отображающих миллионы цветов, и цветных принтеров штриховые кисти утратили свое значение — считается, что они приносят больше хлопот, чем пользы. Если приложение должно выводить масштабируемые рисунки, напоминающие штриховые узоры GDI, вместо штриховых кистей можно воспользоваться другими средствами (например, линиями или растрами). Растровые кисти Шести стандартных штриховых кистей явно недостаточно, поэтому GDI позволяет приложениям создавать кисти на базе растровых изображений. В GDI поддерживаются два основных типа растров: аппаратно-зависимые и аппаратно-не- зависимые. Оба типа подробно рассматриваются в следующих трех главах, а сейчас будет лишь показано, как создать на основе растра растровую кисть1 (bitmap brush) и воспользоваться ею. Функция CreatePatternBrush получает манипулятор аппаратно-зависимого растра (DDB) и создает растровую кисть. Многочисленные экземпляры кисти «выкладываются» наподобие мозаики и заполняют область в операциях заливки. GDI создает копию растра, поэтому манипулятор растра не используется логической кистью после ее создания. Функция CreateDIBPatternBrushPt создает кисть по указателю на упакованный аппаратно-независимый растр (DIB); функция Create- DIBPatternBrush создает кисть по глобальному манипулятору блока памяти, содержащему данные DIB. В программировании Win32 глобальный манипулятор блока памяти ресурса (HGL0BAL) в действительности представляет собой указатель в 32-разрядном линейном адресном пространстве. Однако манипулятор глобального блока, возвращаемый функцией GlobalAlloc, отличается от соответствующего указателя, который может быть получен при вызове функции Global - Lock. Различия между манипулятором блока и указателем на блок унаследованы из парадигмы 16-разрядного программирования. Также встречается термин «узорная кисть» (pattern brush). — Примеч. перев.
Кисти 485 В приведенном ниже фрагменте показано, как эти три функции используются для создания растровых кистей. Прежде всего мы должны где-то найти готовые растровые изображения, не создавая собственных ресурсных файлов. Вам доводилось играть в карточные игры под Windows (например, раскладывать пасьянс, входящий в комплект поставки системы)? Изображения карт хранятся в библиотеке cards.dll и могут использоваться другими приложениями. В приведенном фрагменте DLL загружается функцией LoadLibrary. Функция LoadBitmap создает манипулятор DDB, а функции FindResource и LoadResource создают манипулятор глобального блока. HINSTANCE hCards = LoadLibrary("cards.сПГ); for (int i=0; i<3; i++) { HBRUSH hBrush: int width, height; switch ( i ) { case 0: { HBITMAP hBitmap = LoadBitmap(hCards. MAKEINTRES0URCE(52)); BITMAP bmp; GetObjectChBitmap. sizeof(bmp). & bmp); width = bmp.bmWidth; height = bmp.bmHeight; hBrush = CreatePatternBrush(hBitmap); DeleteObject(hBitmap); } break; case 1: { HRSRC hResource = FindResource(hCards, MAKEINTRES0URCE(52-14). RT_BITMAP); HGLOBAL hGlobal = LoadResource(hCards. hResource); hBrush = CreateDIBPatternBrushPt( LockResource(hGlobal). DIB_RGB_C0LORS); width = ((BITMAPCOREHEADER *) hGlobal)->bcWidth; height = ((BITMAPCOREHEADER *) hGlobal)->bcHeight; } break; case 2: { HRSRC hResource = FindResource(hCards, MAKEINTRES0URCE(52-28), RT_BITMAP); HGLOBAL hGlobal = LoadResource(hCards, hResource); hBrush = CreateDIBPatternBrush(hGlobal, DIB_RGB_C0L0RS); width - ((BITMAPCOREHEADER *) hGlobal)->bcWidth; height = ((BITMAPCOREHEADER *) hGlobal)->bcHeight;
486 Глава 9. Замкнутые области } HGDIOBJ hOld - SelectObjecKhDC, hBrush); POINT P « { i*140+20 + width*i/4. 250 + height*i/4 }; LPtoDP(hDC, &P. 1); SetBrushOrgEx(hDC. P.x, P.y, NULL);// Выровнять изображения карт // в прямоугольнике Rectangle(hDC. 1*140+20. 250, i*140+20+width*3/2+l. 250+height*3/2+l); SelectObjecKhDC. hOld): DeleteObject(hBrush); } Программа в цикле перебирает три возможных случая. В первом случае король пик загружается как DDB-растр, на основе которого создается растровая кисть. Во втором случае дама червей загружается и фиксируется в памяти; полученный указатель на упакованный DIB-растр используется для создания растровой кисти DIB. В третьем случае валет бубен загружается и фиксируется в памяти для получения манипулятора блока, требующегося при создании другой растровой кисти DIB. Созданные кисти обеспечивают закраску трех прямоугольников, размеры которых примерно в 1,5 раза превышают размеры карт по каждой из сторон. Чтобы обеспечить совмещение растров с левым верхним углом прямоугольника, мы используем функцию LPtoDP для отображения логических координат в координаты устройства и вызываем функцию SetBrushOrg, которая и производит непосредственное выравнивание. Результат показан на рис. 9.3. Рис. 9.3. Растровые кисти и совмещение базовой точки Обратите внимание: в Windows 95/98 cards.dll является 16-разрядной библиотекой DLL и не может напрямую загружаться приложениями Win32. При работе с растровыми кистями необходимо помнить о некоторых обстоятельствах. Во-первых, на уровне GDI растровые кисти не обеспечивают полноценной замены штриховых кистей. Для растровой кисти каждый пиксел интерпретируется как пиксел основного цвета; фоновых пикселов не существует.
Кисти 487 Во-вторых, размер растровых кистей, как и размер штриховых кистей, задается в единицах координат устройства. Узоры, нарисованные растровыми кистями, всегда имеют одинаковую ориентацию и размеры в системе координат устройства. Чтобы создавать узоры, масштабируемые в соответствие с разрешением устройства, приложению приходится задействовать несколько разных растров. Но самой серьезной является третья проблема: на платформах, не входящих в семейство NT, максимальный размер растровой кисти ограничивается величиной 8x8 пикселов. Например, если вы попытаетесь использовать в Windows 95/98 растр больших размеров, выведен будет только левый верхний угол. В следующей главе описаны решения, позволяющие добиться нужного эффекта при помощи растровых функций GDI. Растровые узорные кисти часто используются для рисования горизонтальных и вертикальных пунктирных линий. Вспомните прошлую главу — в псевдоточечных линиях PS_D0T одна точка изображается тремя пикселами, а настоящий точечный стиль PS_ALTERNATE поддерживается только в семействе NT. Если вам не хочется рисовать пунктирную линию пиксел за пикселом, существует простое решение — воспользоваться растровой кистью. Приложение может создать кисть с шахматным узором и рисовать прямоугольники с шириной или высотой, равной одному пикселу, или же закрашивать области, в результате отсечения сведенные к ширине или высоте в один пиксел. В приведенном ниже фрагменте создается кисть с шахматным узором, которая используется для обводки контура прямоугольника, имитируя стиль PSALTERNATE. Узор генерируется на основе черно-белого растра 8x8, созданного функцией CreateBitmap. Для рисования линий толщиной в один пиксел применяется функция PatBlt, работающая в режиме отображения ММ_ТЕХТ. В других режимах отображения или в расширенном графическом режиме требуется отсечение, которое гарантирует, что толщина нарисованной линии окажется равной одному пикселу. Этот фрагмент также иллюстрирует второе распространенное применение шахматной кисти — рисование полупрозрачных узоров. Допустим, в текущем контексте устройства выбран черный цвет текста, белый цвет фона, пиксел 0 соответствует черному цвету (RGB(O.O.O)), а пиксел 1 соответствует белому цвету (RGB(255,255,255)). Цвет кисти объединяется с цветом приемника растровой операцией R2MASKPEN. Таким образом, черные пикселы кисти остаются черными, а белые пикселы не изменяют содержимого приемника. При закраске половина пикселов приемника затемняется, и возникает эффект «полупрозрачности». void Frame(HDC hDC, int xO. int yO, int xl, int yl) { unsigned short ChessBoard[] = { OxAA, 0x55. OxAA. 0x55, OxAA, 0x55, OxAA, 0x55 }; HBITMAP hBitmap = CreateBitmap(8, 8. 1. 1, ChessBoard); HBRUSH hBrush = CreatePatternBrush(hBitmap); DeleteObject(hBitmap); HGDIOBJ hOld = SelectObject(hDC. hBrush); // Прямоугольник PS_ALTERNATE PatBlUhDC, xO. yO. xl-xO. 1. PATCOPY); PatBltChDC. xO. yl. xl-xO, 1. PATCOPY);
488 Глава 9. Замкнутые области PatBltChDC. хО, уО. 1, yl-yO. PATCOPY); PatBltChDC. xl, уО, 1. yl-yO. PATCOPY); int old = SetROP2(hDC. R2_MASKPEN); Rectangle(hDC, xO+5, yO+5. xl-5. yl-5); SetR0P2(hDC. old); SelectObjectChDC. hOld); DeleteObject(hBrush); } На рис. 9.4 показан эффект от применения шахматного узора. Пожалуй, разработчикам из Microsoft следовало бы включить этот узор в состав штриховых кистей. Рис. 9.4. Применение шахматного узора: пунктирные линии и полу прозрачность Кисти системных цветов В системе управления окнами используются десятки цветов, предназначенных для вывода различных частей окна — строк заголовков, рамок, меню, полос прокрутки, кнопок и т. д. Эти цвета называются системными и настраиваются в специальном приложении панели управления или на программном уровне, при помощи функций API GetSysColor и SetSysColor. Для каждого системного цвета система создает стандартную кисть. Приложения могут получать кисти системных цветов при помощи функции GetSysColorBrush, передавая ей значения в интервале от C0L0R_SCR0LLBAR до COLOR_GRADIENTACTIVECAPTION. Кисти системных цветов применяются для закраски областей, цвета которых должны соответствовать областям, закрашиваемым системой. Если окно самостоятельно управляет прорисовкой неклиентской области, кисти системных цветов оказываются исключительно полезными. Эти кисти принадлежат к числу стандартных объектов GDI, которые не нужно удалять после использования. Вызовы функции DeleteObject для кистей системных цветов игнорируются.
Кисти 489 Кисти системных цветов могут указываться в качестве фоновых кистей при регистрации классов окон, при этом допускается использование индексов системных цветов в формате (HBRUSH)(C0L0RWIND0W + 1). В соответствии с MSDN, в некоторых системах вызов GetSystemColorBrush в случае многократной загрузки и выгрузки user32.dll может завершиться неудачей. Это связано с тем, что при каждой загрузке user32.dll система создает кисти системных цветов, но при выгрузке забывает их удалить. Таким образом, после того как библиотека user32.dll будет загружена и выгружена несколько сотен раз, таблица объектов GDI может переполниться. Подобные ситуации возникают только в консольных приложениях, выполняющих динамическую загрузку/выгрузку DLL, в частности user32.dll. В GUI-приложениях библиотека user32.dll загружена всегда, она никогда не выгружается и не перезагружается. В Windows NT/2000 кисти системных цветов создаются всего один раз и совместно используются всеми процессами. Структура LOGBRUSH Подведем краткие итоги всего, о чем говорилось выше. Кисти предназначены для внутренней закраски областей. В GDI поддерживаются три типа кистей: однородные, штриховые и узорные. Однородная кисть определяется описателем цвета, при реализации которого на устройствах с палитрой может использоваться смешение. Особенностью штриховых кистей является деление пикселов на основные и фоновые; последние выводятся лишь в режиме заполнения фона OPAQUE. Штриховые кисти применяют лишь для простых экранных изображений, поскольку их узоры обычно не масштабируются в соответствие с разрешением и масштабом устройства. Узорная кисть определяется на основе аппаратно-зависимо- го или аппаратно-независимого растра. В процессе закраски растр многократно размещается в границах области по принципу мозаики. В реализации GDI для Windows 95/98 максимальный размер используемой части растра равен 8x8 пикселов, что существенно снижает полезность этого удобного средства. Все три типа кистей описываются структурой LOGBRUSH, которая может передаваться функции CreateBrushlndirect при создании логической кисти. Соответствующие определения приведены ниже. typedef struct tagLOGBRUSH { UINT lbStyle: COLORREF IbColor; LONG IbHatch; }; HBRUSH CreateBrushIndirect(CONST LOGBRUSH * lplb): Основные трудности при работе со структурой LOGBRUSH связаны с тем, что при выборе стиля BS_PATTERN поле IbHatch содержит манипулятор DDB, а для стилей BSDIBPATTERN и BSDIBPATTERNPT в этом поле указывается манипулятор блока DIB или указатель, а значение LOWORD(lbColor) равно либо DIBPALC0L0RS, либо DIB_RGB_C0L0RS. Структура LOGBRUSH может использоваться для получения информации об объекте кисти GDI при помощи функции GetObject: LOGBRUSH logbrush; GetObject(hBrush. sizeof(LOGBRUSH), & logbrush);
490 Глава 9. Замкнутые области Не рассчитывайте найти в структуре L0GBRUSH, возвращаемой GetObject, действительный манипулятор или указатель на растр узорной кисти. При создании узорной кисти GDI создает копию растра во внутренней структуре данных, скрытой от пользовательских приложений. Структура L0GBRUSH также используется при создании расширенных перьев. При рисовании линий и кривых геометрическим пером на самом деле задейст- вуется кисть, поэтому здесь также могут потребоваться смешение, штриховка и растры. Объект логической кисти находится под управлением GDI. В Windows NT/ 2000 объект логической кисти состоит из компонента пользовательского режима, оптимизирующего процесс частого создания и уничтожения однородных кистей, и компонента режима ядра, в котором хранится полная информация о логической кисти. В частности, объект режима ядра содержит данные об основном и фоновом цвете, расширенный набор флагов стиля, растр, маску и т. д. Маска нужна для реализации штриховых кистей, сохраняющих фоновый рисунок. На уровне DDI графический механизм позволяет драйверу устройства предоставить собственные растры для реализации штриховых кистей на уровне графического устройства, чтобы штриховой узор лучше различался. Предусмотрена специальная точка входа, при помощи которой драйвер устройства реализует логическую кисть — другими словами, создает свою внутреннюю интерпретацию логической кисти, которая позднее используется в графических операциях с применением логической кисти. Логические кисти (за исключением узорных) занимают очень мало памяти. Для узорных кистей в таблице GDI создается дополнительный манипулятор (как для узорных кистей DDB, так и для DIB) и выделяется память для хранения копии растра. Прямоугольники Основной геометрической фигурой в Windows API является прямоугольник. Прямоугольники применяются при определении окон и клиентских областей, различных фигур с прямоугольным ограничивающим контуром, при форматировании текста и даже при отсечении. В Win32 определяется структура данных и API для работы с прямоугольниками как структурами данных и для разнообразной закраски прямоугольных областей. Прямоугольник как структура данных В Win32 API прямоугольники определяются при помощи структуры RECT, для которой определяется около десятка всевозможных операций. typedef struct _RECT { LONG left; LONG top; LONG right: LONG bottom; }
Прямоугольники 491 BOOL SetRect(LPRECT Iprc. int xLeft, int yTop. int xRight. int yBottom); BOOL SetRectEmpty(LPRECT Iprc); BOOL IsRectEmpty(CONST RECT * Iprc); BOOL EqualRect(CONST RECT *lprcl. CONST RECT *lprc2); BOOL CopyRect(LPRECT IprcDst, CONST RECT * IprcSrc): BOOL OffsetRect(LPRECT Iprc, int dx. int dy); BOOL PtInRect(CONST RECT * Iprc. POINT pt); BOOL InflateRectCCONST LPRECT Iprc, int dx. int dy); BOOL IntersectRectCCONST LPRECT IprcDst. CONST RECT * lprcSrcl. CONST RECT * lprcSrc2); BOOL SubtractRect(LPRECT lprcDst2. CONST RECT * lprcSrcl. CONST RECT * lprcSrc2); BOOL UnionRect(LPRECT IprcDst. CONST RECT *lprcSrcl, CONST RECT * lprcSrc2); Прямоугольник определяется минимальными и максимальными координатами по обеим осям, что соответствует левому верхнему и правому нижнему углу в системе координат устройства. При работе с функциями, использующими структуру RECT, всегда предполагается, что левая координата не больше правой, а верхняя не больше нижней. Дело в том, что эти функции поддерживаются диспетчером окон для выполнения операций с прямоугольниками окон и клиентских областей, задаваемыми в экранных координатах. Прежде чем передавать данные этим функциям, приложение должно нормализовать их, иначе результаты могут быть весьма неожиданными. Также предполагается правильность передаваемых указателей на RECT — проверка указателей в текущих реализациях весьма ограничена (вероятно, по соображениям быстродействия). Функция SetRect заполняет все четыре поля структуры RECT новыми значениями и используется главным образом для инициализации новых прямоугольников. Функция SetRectEmpty обнуляет все четыре поля, в результате чего прямоугольник оказывается пустым. Функция IsRectEmpty проверяет, пуст ли заданный прямоугольник (то есть является ли его высота или ширина нулевой или отрицательной величиной). Функция Equal Rect проверяет, содержат ли два прямоугольника попарно совпадающие поля. Функция CopyRect копирует исходный прямоугольник в заданную структуру. Функция OfffsetRect смещает прямоугольник (то есть прибавляет к его левой и правой координате величину dx, а к верхней и нижней — величину dy). Функция PtlnRect проверяет, принадлежит ли точка прямоугольнику; при этом верхняя и левая стороны прямоугольника включаются в проверку, а правая и нижняя — нет. Другими словами, точки, расположенные на левой и верхней сторонах, считаются принадлежащими прямоугольнику, а точки правой и нижней стороны в прямоугольник не входят. Функция InflateRect расширяет прямоугольник на dx единиц по горизонтали и на dy единиц по вертикали (с каждой из сторон). Если задать отрицательные значения, прямоугольник уменьшается. Функция IntersectRect вычисляет область пересечения двух прямоугольников (получается либо прямоугольник, либо пустая область). Функция SubtractRect исключает прямоугольник из другого прямоугольника. Всем известно, что в общем случае при таком исключении генерируется непрямоугольная область, которая описывается тремя прямоугольниками. В Win32 API результат вызова SubtractRect определяется ограничивающим пря-
492 Глава 9. Замкнутые области моугольником. Таким образом, если перекрывающаяся область прямоугольников А и В по ширине или высоте совпадает с прямоугольником А, она исключается из результата А - В; в противном случае прямоугольник А остается без изменений. Функция UnionRect возвращает ограничивающий прямоугольник области, точки которой принадлежат хотя бы одному из двух прямоугольников. При работе с RECT все эти функции реализуются очень просто. При критических требованиях к быстродействию приложение может реализовать их в виде подставляемого (inline) кода вместо вызова функций Win32 API. Например, вызов SetRect с пятью параметрами требует минимум пяти инструкций, а при использовании подставляемого кода можно обойтись всего четырьмя инструкциями. Формат структуры RECT в памяти точно совпадает с форматом массива из двух структур POINT. При преобразовании координат функциями LPtoDP или DPtoLP структура RECT может передаваться вместо массива из двух структур POINT. Но если к структуре RECT применяются преобразования или отображения, вы должны принять дополнительные меры предосторожности и убедиться в том, что прямоугольник остается нормализованным и сохраняет параллельность осям. Рисование прямоугольников В Win32 API предусмотрено несколько функций, которые закрашивают внутреннюю область прямоугольника кистью, обводят его контуры пером или делают то и другое: BOOL Rectangle(HDC hDC. int nLeftRect, int nTopRect, int nRightRect, int nBottomRect); int FillRect(HDC hDC, CONST RECT * Iprc, HBRUSH hbr); int FrameRect(HDC hDC. CONST RECT * lprc, HBRUSH hbr); BOOL InvertRectCHDC hDC, CONST RECT * lprc); BOOL DrawFocusRect(HDC hDC, CONST RECT * lprc); Rectangle Функция Rectangle рисует прямоугольник, определяемый четырьмя координатами. На процесс рисования влияет достаточно большое количество атрибутов контекста. Поскольку функция Rectangle принадлежит к числу базовых функций GDI, мы рассмотрим ее более подробно. Рисунок 9.5 иллюстрирует результат применения функции Rectangle при разных атрибутах контекста устройства. Если контекст находится в совместимом графическом режиме, правая и нижняя стороны прямоугольника в системе координат устройства не рисуются (что соответствует традиционным правилам включения/исключения сторон). Обратите внимание на то, что правая сторона прямоугольника может и не соответствовать nBottomRect; она определяется максимальным значением nTopRect и nBottomRect при отображении на систему координат устройства. Но если контекст устройства находится в расширенном графическом режиме, выводятся все четыре стороны прямоугольника, как показывает нижний рисунок в левом столбце. Вероятно, это изменение неизбежно, поскольку возможность применения произвольных аффинных преобразований в расширенном режиме затрудняет определение правой и нижней сторон (по сравнению с верхней и нижней). Периметр прямоугольника обводится текущим объектом пера, выбранным в контексте устройства.
Прямоугольники 493 Если толщина пера равна один пикселу в координатах устройства, рисуются только пикселы периметра. Если перо имеет толщину в п пикселов, один пиксел рисуется на центральной линии, (п-1)/2 пикселов рисуются снаружи прямоугольника и еще (п-1)/2 — внутри прямоугольника. При использовании пера со стилем PSINSIDEFRAME один пиксел рисуется на центральной линии, а еще (п-1) пикселов — внутри прямоугольника. Другим особым случаем является пустое перо, которое не обводит периметр прямоугольника. При использовании пустого пера ширина и высота прямоугольника уменьшаются на один пиксел. •R »R "R Черное перо Пустое перо Перо с толщиной 3 пиксела Черное перо, Перо с толщиной 3 пиксела, Синее перо, расширенный режим рисование внутри контура R2_NOTCOPYPEN Рис. 9.5. Различные стили прямоугольников Текущий объект кисти закрашивает ту область, которая не была закрашена пером, а в случае пустого пера — весь прямоугольник, уменьшенный на один пиксел. На результаты применения кисти также влияет режим заполнения фона, цвет фона и базовая точка кисти. Текущая растровая операция в контексте устройства распространяется как на периметр, так и на внутреннюю часть прямоугольника. Например, если выбрать операцию R2 NOP, функция Rectangle не выполняет никаких действий. FillRect Функция FillRect закрашивает кистью прямоугольник, определяемый структурой RECT. Правая и нижняя стороны в системе координат устройства исключаются всегда, даже в расширенном графическом режиме. В отличие от вызова Rectangle с пустым пером, приводящим к рисованию уменьшенного прямоугольника, функция FillRect закрашивает весь прямоугольник. Она не использует атрибут бинарной растровой операции в контексте устройства. Параметр-кисть, передаваемый FillRect, также может содержать индекс системного цвета в формате (HBRUSH)(индекс +1). Различия между FillRect и Rectangle
494 Глава 9. Замкнутые области объясняются тем, что в реализации FillRect используются средства GDI для работы с растрами. FillRect вызывает недокументированную функцию PolyPatBlt GDI, работа которой основана на вызове PatBlt (см. следующую главу). FrameRect Функция FrameRect закрашивает периметр прямоугольника кистью (а не пером!). При этом контур нарисованного изображения имеет те же размеры, как и при вызове FillRect. Толщина периметра равна одной логической единице, а его реальная толщина в пикселах определяется мировым преобразованием и режимом отображения. Прорисовка периметра прямоугольника позволяет создавать интересные эффекты, которые трудно выполнить при помощи пера GDI. В частности, кисть позволяет использовать смешанные цвета, не создавая геометрическое перо, или рисовать полноценные точечные контуры прямоугольников растровой кистью с шахматным узором. Функция FrameRect также реализуется с применением недокументированной функции PolyPatBlt. InvertRect Функция InvertRect инвертирует цвет каждого пиксела прямоугольника по аналогии с тем, как перо в режиме R2_N0T инвертирует пикселы линии. В устройствах, использующих палитру, инвертируются индексы палитры и цвет определяется расположением цветов в палитре. В устройствах без палитры черный цвет переходит в белый, белый цвет переходит в черный, а RGB-значение каждого пиксела инвертируется. При двукратном вызове InvertRect с одинаковыми параметрами восстанавливается исходное содержимое контекста устройства. Функция InvertRect реализуется функцией PatBlt, поэтому она подчиняется тем же правилам включения/исключения сторон, что и функции FrameRect и FillRect. DrawFocusRect Функция DrawFocusRect напоминает функцию FrameRect. Она рисует периметр прямоугольника шахматной узорной кистью с применением растровой операции «исключающего ИЛИ». Как и в случае с функцией InvertRect, повторный вызов DrawFocusRect восстанавливает исходное содержимое контекста устройства. Функция DrawFocusRect реализуется функцией PolyPatBlt с узорной кистью и растровой операцией «исключающего ИЛИ». Название DrawFocusRect связано с применением этой функции в модуле управления окнами. Например, в диалоговых окнах функция DrawFocusRect рисует точечный контур прямоугольника на кнопке, получающей фокус ввода с клавиатуры. Когда фокус переходит к другой кнопке, прямоугольник стирается повторным вызовом DrawFocusRect, после чего вызывается функция рисования прямоугольника на кнопке, получившей фокус. Функция DrawFocusRect также может использоваться при выводе «эластичных» прямоугольников. Применяя эту функцию в интерактивном взаимодействии, проследите за правильностью определений прямоугольников. MSDN преувеличивает проблему и предупреждает программистов, что нарисованные функцией DrawFocusRect прямоугольники не могут прокручиваться. На самом деле с прокруткой проблем нет: обновляемый регион
Прямоугольники 495 после прокрутки содержит только вновь открывшиеся области, поэтому вызов DrawFocusRect при обработке сообщения WM_PAINT не приводит к стиранию прямоугольника. Но если контекст устройства был получен функцией GetDC, которая не настраивает системный регион, проще стереть прямоугольник перед прокруткой. На рис. 9.6 продемонстрирован результат вызова функций FillRect, FrameRect, InvertRect и DrawFocusRect, которые не относятся к числу функций GDI, хотя и используют функции GDI в своей работе. Эти функции поддерживаются системой управления окнами (user32.dll) и относятся именно к ней. FillRect FrameRect InvertRect DrawFocusRect Рис. 9.6. Функции рисования прямоугольников, используемые системой управления окнами Прорисовка границ и элементов управления В Win32 API входит ряд функций, с помощью которых система управления окнами рисует разнообразные границы и элементы управления (controls) и которые имеют непосредственное отношение к рисованию прямоугольников. Эти функции могут использоваться при реализации элементов, прорисовка которых осуществляется владельцем, при нестандартном выводе неклиентской области или при имитации внешнего вида окон и элементов управления. Ниже приведены прототипы двух важнейших функций, DrawEdge и DrawFrameControl. BOOL DrawEdge(HDC hDC. LPRECT Iprc, UINT edge, UINT grFlags); BOOL DrawFrameControl(HDC hDC. LPRECT Iprc. UINT uType. UINT uState); Обе функции получают манипулятор контекста, структуру RECT с описанием рисуемой области и два флага. Флаги и их смысл описаны в документации MSDN, а мы лишь приведем примеры их использования. Следующий фрагмент показывает, как рисовать различные границы (рис. 9.7). for (int e=0; e<4;e++) { const int Edge[] « {EDGE_RAISED. EDGE_SUNKEN. EDGE ETCHED. EDGE BUMP); int int Edge[] Flag[] = { EDGE RAISED. EDGE SUNKEN. = { BF MIDDLE | BF BOTTOM. BF MIDDLE BF MIDDLE BF MIDDLE BF MIDDLE BF MIDDLE BF MIDDLE BF BOTTOMLEFT. EDGE_ BF BOTTOMLEFT | BF TOP. BF RECT. BF RECT | BF FLAT. BF RECT | BF MONO. BF RECT | BF SOFT. _ETCHED. EDGE JUMP };
496 Глава 9. Замкнутые области BF_MIDDLE | BF_RECT | BF_DIAGONAL, BF_MIDDLE | BF_RECT | BF_ADJUST }; for (int f»Q: f<sizeof(Flag)/sizeof(Flag[0]); f++) { RECT rect = { f*56+20, e*56 + 20, f*56+60, e*56+60 }; InflateRect(&rect, 3. 3); // Увеличить фон FillRectChDC. & rect. GetSysColorBrush(COLOR_BTNFACE)); InflateRect(&rect. -3. -3); // Восстановить размер DrawEdge(hDC, & rect. Edge[e]. Flag[f]): *4, r~~™~™*% J ■^\kl r.'.r., ■ \ \ ' ' SlIsL | i ' j :' - ' \ ■ ■ < \ к ff } ' '" V >' s '' ' - \ , / '< •^ Л :..r.i*i .} ,; Рис. 9.7. Использование функции DrawEdge для рисования границ Функция DrawFrameControl рисует всевозможные элементы управления, обычно встречающиеся в строке заголовка, строке меню, полосах прокрутки, кнопках и всплывающих меню. Некоторые примеры приведены на рис. 9.8. За подробным описанием обращайтесь к MSDN. 'jJ5sJ ',,!S!,„J -„Ы,] r.ifecj , ж,,,,! Lf i .„ifrt.J ±1 zJ_±J ±1 zJ ^k'k ^ ГО'#г rjQJ x _ ness Рис. 9.8. Рисование различных элементов управления функцией DrawFrameControl
Эллипсы, секторы, сегменты и закругленные прямоугольники 497 Показанные на рисунке элементы управления нарисованы в прямоугольниках 40 х 40 пикселов, что значительно превышает стандартные размеры, используемые в системе. Обратите внимание на отсутствие неровных краев, которые обычно появляются при увеличении мелких растровых изображений. Возможно, вы и не подозревали, что при рисовании этих крестиков, стрелок, вопросительных знаков и т. д. Windows использует символы шрифта TrueType Marlett, чтобы изображение было полностью масштабируемым. Эллипсы, секторы, сегменты и закругленные прямоугольники В GDI предусмотрено несколько функций для рисования эллипса, его частей и даже гибрида прямоугольника с эллипсом — закругленного прямоугольника. Прототипы этих функций приведены ниже. BOOL Ellipse(HDC hDC. int nleftRect. int nTopRect. int nRightRect. int nBottomRect); BOOL Chord(HDC hDC. int nLeftRect. int nTopRect. int nRightRect. int nBottomRect. int nXRadiall. int nYRadiall. int nXRadial2. int nYRadial2); BOOL Pie(HDC hDC. int nLeftRect. int nTopRect. int nRightRect. int nBottomRect. int nXRadiall. int nYRadiall. int nXRadia!2. int nYRadia!2); BOOL RoundRect(HDC hDC. int nLeftRect. int nTopRect. int nRightRect. int nBottomRect. int nWidth. int nHeight); Эти четыре функции используют такие же ограничивающие прямоугольники, как и описанная выше функция Rectangle. Сходство проявляется и в принципе рисования: контур обводится текущим объектом пера в контексте устройства, а внутренняя область закрашивается текущей кистью. В совместимом графическом режиме, если толщина пера в системе координат устройства равна одному пикселу, правая и нижняя стороны не рисуются в соответствии с правилами включения/исключения. Однако в расширенном графическом режиме рисуются все четыре стороны. В документации Microsoft лишь упоминается тот факт, что в двух графических режимах прямоугольники рисуются по-разному. Линии, нарисованные пером со стилем PS_INSIDEFRAME, полностью находятся в ограничивающем прямоугольнике. На рис. 9.9 показано, как нарисованный эллипс выглядит на уровне пикселов. Первый эллипс нарисован однородным пером толщиной в один пиксел в совместимом графическом режиме; нижняя и правая стороны в ограничивающий прямоугольник не входят. Второй эллипс нарисован в расширенном графическом режиме с включением правой и нижней стороны. Третий эллипс нарисован пером толщиной в два пиксела со стилем PSINSIDEFRAME, в результате чего образовалась уродливая несимметричная фигура. Возможно, нарушение симметрии связано с аппроксимацией кривых Безье и выводом кривых Безье в виде набора
498 Глава 9. Замкнутые области отрезков. Хотя выше говорилось о том, что такая аппроксимация обеспечивает минимальную погрешность, для такой крошечной фигуры даже разница в один пиксел становится очень заметной. Left •Left м [ШИПИ Top [fflflfr^ | | | 111 п^шфп 111 1 1 1 1 I 1 1 1 1 1 1 1 1 1 1 В otto m 'Right Однородное перо «Left Bottom Right Bottom PSJNSIDEFRAME Рис. 9.9. Нарисованный эллипс (в увеличении) Однородное перо, расширенный режим Right На рис. 9.10 сравниваются результаты вызова функций Ellipse, Pie и Chord. Функция Ellipse рисует полный периметр эллипса текущим пером и закрашивает его внутреннюю часть текущей кистью. Окружность является частным случаем эллипса, ширина и высота которого выражена в физических единицах. Функция Pie рисует сектор — клиновидную фигуру, образованную частью периметра и двумя радиусами. Функция Chord рисует сегмент, образованный частью периметра эллипса и секущей, соединяющей две точки периметра. В обеих функциях, Pie и Chord, начальный и конечный угол задаются двумя точками (по аналогии с функцией Arc). Таким образом, в совместимом графическом режиме окончательный вид фигуры зависит от атрибута направления дуг контекста. В расширенном режиме дуги всегда рисуются против часовой стрелки в логической системе координат. Chord, против часовой стрелки (Left, Top) Pie, по часовой стрелке (xEnd, у End).. (Left, Top) (xEnd.yEnd) у" (xStart, yStart) (Bottom, Right) (xStart, yStart) (Bottom, Right) (Bottom, Right) Рис. 9.10. Функции Ellipse, Pie и Chord
Эллипсы, секторы, сегменты и закругленные прямоугольники 499 Кривые, нарисованные этими функциями, являются замкнутыми. Таким образом, при использовании геометрического пера на всех стыках применяется его атрибут соединения, а атрибут завершения не применяется. Периметры, нарисованные этими функциями, также могут включаться в объекты траекторий. В примере с функцией Chord задействовано косметическое перо со стилем PS_ALTERNATE, а для функции Pie — утолщенное геометрическое перо с заостренным соединением и стилем PS_INSIDEFRAME. Обратите внимание: дуга полностью расположена внутри ограничивающего прямоугольника, но на радиусах пикселы распределяются симметрично с обеих сторон. Для функции Ellipse на рисунке использовано узорное геометрическое перо и узорная кисть. Следующая функция рисует простейшие круговые диаграммы. void DrawPieChartCHDC hDC, int xO. int yO. int xl. int yl. double data[]. COLORREF co1or[], int count) { double sum - 0; for (int i=0; i<count; i++) sum +« data[i]; double angle - 0; for (i-0: i<count: i++) { double a - data[i] * 2 * 3.14159265358 / sum; HBRUSH hBrush - CreateSolidBrush(color[i]); HGDIOBJ hOld - SelectObjectChDC, hBrush); PieChDC. xO. yO. xl. yl. (int) ((xl-xQ) * cos(angle)). - (int) ((yl-yO) * sin(angle)). (int) ((xl-xO) * cos(angle+a)), - (int) ((yl-yO) * sin(angle+a))); angle += a; SelectObject(hDC. hOld); DeleteObject(hBrush); } } Функция RoundRect, предназначенная для рисования прямоугольников с закругленными углами, позволяет нарисовать прямоугольник, эллипс или любую промежуточную фигуру. При вызове ей передаются те же параметры ограничивающего прямоугольника, как и при вызове Rectangle или Ellipse. Последние два параметра, nWidth и nHeight, определяют размеры закругленных углов. Закругленные углы можно рассматривать как четыре четверти эллипса с шириной nWidth и высотой nHeight, соединенные прямыми линиями. Если оба параметра nWidth и nHeight равны нулю, функция Rou ndRect рисует прямоугольник. Если параметр nWidth равен ширине ограничивающего прямоугольника, а параметр nHeight совпадает с его высотой, функция RoundRect рисует эллипс. Если приложение передает в параметре nWidth или nHeight отрицательное число, GDI использует
500 Глава 9. Замкнутые области в вычислениях его абсолютную величину (модуль). Размеры ограничивающего прямоугольника также ограничивают размеры углов. Смысл параметров функции RoundRect иллюстрирует рис. 9.11. (nLeft nTop) InLeft + nWidth U* "XI Г" \} i nTop + nHeight * » 4 ) i • • / Ч !* ;'••» ^ ч' **! •'•*' *'-*-♦->*'♦•♦♦ *•*•♦ *■♦■♦ ♦•♦•♦ ♦•♦•♦>»Ч>-»--*"*'*'- - (nBottom, nRight) Рис. 9.11. Функция RoundRect: от прямоугольника к эллипсу Многоугольники Прямоугольник является частными случаем многоугольника — замкнутой фигуры, состоящей из двух и более вершин, соединенных прямыми линиями. Многоугольник может представлять собой треугольник, параллелограмм, прямоугольник, квадрат, восьмиугольник и т. д. В GDI API многоугольник представляется массивом структур POINT, определяющих координаты вершин. При одном вызове функции GDI позволяет нарисовать один или сразу несколько многоугольников. Данные нескольких многоугольников передаются в двух массивах — в одном содержится количество вершин каждого многоугольника, а в другом — координаты всех вершин. Ниже приведены прототипы функций GDI, предназначенных для рисования многоугольников. int GetPolyFillMode(HDC hDC); int SetPolyFillMode(HDC hDC, int iPloyFillMode); BOOL Polygon(HDC hDC, CONST POINT * IpPoints, int nCount); BOOL PolyPolygon(HDC hDC. CONST POINT * IpPoints. CONST int * IpPolyCounts. int nCount); Принцип рисования многоугольников функцией Polygon напоминает рисование ломаных линий функцией Polyline. Параметр IpPoints указывает на массив структур POINT, содержащих координаты вершин. Параметр nCount определяет количество вершин в массиве (не менее двух). Функция Polygon автоматически замыкает фигуру и при использовании геометрического пера оформляет каждую вершину в соответствии с атрибутом соединения. Контур многоугольника обводится текущим пером, а его внутренняя часть закрашивается текущей кистью.
Многоугольники 501 На этом сходство не заканчивается: рисование нескольких многоугольников функцией PolyPolygon напоминает процедуру рисования нескольких ломаных функцией PolyPolyline. В параметре nCount передается количество многоугольников. Параметр lpPolyCounts указывает на массив с количествами вершин для каждого многоугольника (не менее двух). Параметр lpPoints указывает на массив структур, содержащих координаты вершин. Функция PolyPolygon автоматически замыкает каждый многоугольник. Контур каждого многоугольника обводится текущим пером, а внутренняя часть закрашивается текущей кистью. В отличие от функций с ограничивающими прямоугольниками (таких, как Rectangle, Ellipse и Arc), исключающих правую и нижнюю стороны в совместимом графическом режиме, функции Polygon и PolyPolygon рисуют все свои вершины, причем это поведение сохраняется как в совместимом, так и в расширенном графическом режиме. Следовательно, прямоугольник, нарисованный как многоугольник по четырем углам, несколько отличается от обычного прямоугольника, нарисованного в совместимом режиме. В частности, это объясняет различия между поведением функции Rectangle в двух графических режимах. В результате применения аффинных преобразований с поворотами и сдвигом прямоугольник может превратиться в параллелограмм или утратить параллельность осям координат, поэтому графический механизм GDI в общем случае не может нарисовать преобразованный прямоугольник исходными средствами. Режим заполнения многоугольников Для простого (например, выпуклого) многоугольника внутренняя область определяется достаточно четко. Однако невыпуклый прямоугольник может состоять из нескольких частей, что затрудняет определение его внутренней области. В Windows GDI внутренняя область многоугольника определяется при помощи двух правил, которые в терминологии GDI называются «режимом заполнения многоугольников» (polygon fill mode). Режим заполнения многоугольников принадлежит к числу атрибутов контекста устройства, и для работы с ним используются функции GetPolyFillMode и SetPolyFillMode. Существует два допустимых значения режима — ALTERNATE и WINDING. Режим ALTERNATE, используемый в контекстах устройств по умолчанию, очень прост и нагляден. Принадлежность точки внутренней области многоугольника в режиме ALTERNATE проверяется следующим образом: из этой точки проводится луч в бесконечность в любом направлении и подсчитывается количество пересечений этого луча с контуром многоугольника. При нечетном количестве пересечений точка считается находящейся внутри, а при четном — снаружи. Примеры использования режима ALTERNATE приведены на рис. 9.12. На первом рисунке изображен простой ромб, являющийся выпуклым многоугольником. Каждая строка развертки внутри ромба дважды пересекается с периметром; точки между пересечениями образуют внутреннюю область многоугольника. На втором рисунке слева при соединении вершин многоугольника получается фигура в виде восьмиконечной звезды. Каждая строка развертки пересекается с периметром до шести раз, поэтому все точки между вторым
502 Глава 9. Замкнутые области и третьим, а также четвертым и пятым пересечением не считаются принадлежащими многоугольнику. На двух последних рисунках фигура состоит из двух многоугольников (меньший прямоугольник находится внутри большего). В режиме ALTERNATE эти два примера выглядят одинаково. Рис. 9.12. Режим заполнения многоугольников ALTERNATE Практическая реализация вычисляет ограничивающий прямоугольник для каждого многоугольника и проверяет серию строк развертки у = ymin + 0,5, ymin + 1,5, ..., у = углах - 0,5 на пересечение с контуром. Для каждой строки развертки общее количество пересечений всегда четно. В режиме ALTERNATE точки, находящиеся между первым и вторым, третьим и четвертым и т. д. пересечениями, считаются внутренними. Главный недостаток режима ALTERNATE связан с обработкой перекрывающихся многоугольников. К сожалению, некоторые из перекрывающихся частей исключаются из фигуры, а эта ситуация достаточно часто встречается в компьютерной графике. Как было показано в предыдущей главе, траектории, сгенерированные функцией WidenPath, могут содержать петли, которые в действительности являются перекрывающимися частями изображения. Перекрытия также очень часто возникают при пересечении нескольких многоугольников. Для решения этой проблемы в GDI был предусмотрен более сложный режим заполнения многоугольников WINDING. В режиме WINDING учитывается такой фактор, как направление кривых. Принадлежность точки многоугольнику проверяется тем же способом — из точки проводится луч в бесконечность и проверяются пересечения луча с контуром. Для каждого луча поддерживается счетчик с нулевым исходным значением. При каждом пересечении с участком контура, направленным по часовой стрелке, значение счетчика увеличивается, а при пересечениях с участками, направленными против часовой стрелки, счетчик уменьшается. Если итоговое значение счетчика отлично от нуля, считается, что точка принадлежит внутренней области фигуры; в противном случае точка считается внешней. На рис. 9.13 изображены те же многоугольники, нарисованные в режиме WINDING. Все контуры многоугольников направлены по часовой стрелке, кроме маленького многоугольника на последнем рисунке — он нарисован против часовой стрелки. Как видно из рисунка, вторая и третья фигуры в режиме WINDING выглядят иначе. Ниже приведен фрагмент программы, который использовался при построении рис. 9.12 и 9.13.
Многоугольники 503 Рис. 9.13. Режим заполнения многоугольников WINDING for (int t=0; t<2; t++) { logbrush.lbColor - RGB(0. 0. OxFF); KGDIObject pen (hDC. ExtCreatePen(PS_GEOMETRIC | PSJOLID | PS_JOIN_MITER. 3. & logbrush. 0. NULL)); if ( t—0 ) SetPolyFillMode(hDC. ALTERNATE); else SetPolyFillMode(hDC. WINDING); for (int m=0; m<4; m++) { SetViewportOrgEx(hDC. 120+m*220. 350+t*220, NULL); const int s0[] = { 4. 4. 100. 0. 100. 1. 100. 2. 100. 3 }; const int sl[] - { 8. 8. 100. 0. 100. 3. 100. 6. 100. 1. 100. 4. 100. 7, 100. 2. 100. 5 }; const int s2[] - { 10. 5. 100. 0. 100. 1. 100. 2. 100. 3, 100. 4. 50. 0. 50. 1. 50. 2. 50. 3. 50. 4 }; const int s3[] - { 10. 5. 100. 0. 100. 1. 100. 2. 100. 3. 100. 4. 50. 4. 50. 3. 50. 2. 50. 1, 50. 0 }; const int * spec[] - { sO. si. s2. s3 }; POINT P[10]; int n - spec[m][0]; int d = spec[m][l]: const int * s * spec[m]+2; for (i-0; i<n; i++) { P[i].x = (int) ( s[i*2] * cos(s[i*2+l] * 2 * 3.1415927 / d) ); P[i].y - (int) ( s[i*2] * sin(s[i*2+l] * 2 * 3.1415927 / d) ); } if ( m<2 ) Polygon(hDC. P. n); else { int V[2] = { 5. 5 }; // Количество точек // Количество вершин // каждого многоугольника // Индекс вершины
504 Глава 9. Замкнутые области PolyPolygon(hDC. P. V, 2); } } } Замкнутые траектории Траекторией в GDI называется объект, состоящий из нескольких линий, дуг и кривых Безье. Траекторию, построенную вызовами функций рисования линий и кривых между вызовами BeginPath и EndPath, можно обвести пером, закрасить кистью или сделать то и другое одновременно. Процесс обводки траектории пером рассматривается в главе 8, а сейчас нас больше интересует закраскг траекторий кистью. В GDI эта задача решается двумя функциями: BOOL FillPath(HDC hDC); BOOL StrokeAndFillPath(HDC hDC); Функция Fill Path замыкает все незамкнутые фигуры в текущей траектории, неявно связанной с контекстом устройства, и закрашивает их текущей кистью, выбранной в контексте устройства. Как было сказано выше, траектория состоит из одной или нескольких групп линий или кривых. В результате аппроксимации кривых траектория фактически превращается в совокупность многоугольников. Функция Fill Path практически эквивалентна вызову PolyPolygon с пустым пером. Как и при вызове PolyPolygon, при определении принадлежности точек внутренней области закрашиваемой траектории учитывается режим заполнения многоугольников. Перед тем как вернуть управление, функция Fill Path освобождает объект траектории в контексте устройства. Функция StrokeAndPath замыкает все незамкнутые фигуры текущей траектории, закрашивает их текущей кистью и обводит контуры текущим пером. Эта функция очень похожа на функцию PolyPolygon. Как и функция Fill Path, она тоже освобождает объект траектории в контексте устройства перед возвратом управления. Учтите, что StrokeFillPath в общем случае нельзя заменить последовательными вызовами StrokePath и Fill Path, что объясняется двумя причинами. Во-первых, каждая из этих функций освобождает траекторию, поэтому следующий вызов завершится неудачей, если только вы не позаботитесь о сохранении и восстановлении контекста. Во-вторых, при раздельных вызовах Stroke- Path и Fill Path генерируются перекрывающиеся области, что приводит к возникновению нежелательных эффектов при использовании некоторых растровых операций. Многоугольник или совокупность многоугольников всегда можно без потери точности преобразовать в траекторию. Траектория, не содержащая кривых, легко преобразуется в совокупность многоугольников. Траекторию с кривыми можно аппроксимировать функцией Fl attenPath, а затем преобразовать в совокупность многоугольников, однако линейная аппроксимация кривых приводит к потере точности и увеличению объема данных, обрабатываемых GDI. Следовательно, там, где это возможно, функциям траекторий следует отдавать предпочтение перед функциями многоугольников.
Замкнутые траектории 505 На рис. 9.14 приведены примеры использования функций FillPath и Fi 11- AndStrokePath, а также иллюстрируются последствия вызова WidenPath и режима заполнения многоугольников. ш ш Рис. 9.14. Функции FillPath, StrokeAnd Fill Path и режимы заполнения многоугольников Мы имеем дело с восемью разными случаями. В каждом случае траектория образуется двумя перекрывающимися эллипсами, повернутыми на 45°. Изображения в первом ряду были получены в режиме WINDING, во втором — в режиме ALTERNATE. В первом столбце использовалась функция FillPath с кистью светлого оттенка, а изображения второго столбца были построены функцией StrokeAnd- Fill с темным толстым пером. Третий столбец был создан функцией WidenPath с толстым пером, после чего была вызвана функция FillPath. Изображения последнего столбца были построены функцией Wi denPath с толстым пером и последующим вызовом StrokeAndFi 11 Path с тонким пером. Вспомните, о чем говорилось в главе 8, — функция WidenPath генерирует новую траекторию по периметру области, которая была бы закрашена при обводке траектории текущим пером, и петли в новой траектории возникают в результате соединения линий и кривых. Ниже приведен фрагмент кода, при помощи которого был построен рис. 9.14. Программа строит траекторию из двух эллипсов, получает ее данные функцией GetPath, поворачивает на 45° и с помощью результата строит траектории, используемые непосредственно при рисовании. void KMyCanvas::TestFi11PathCHDC hDC) { const int nPoint = 26; POINT Point[nPoint]: BYTE Type[nPoint]; // Построение траектории из двух эллипсов BeginPath(hDC); Ellipse(hDC. -100, -40. 100, 40); Ellipse(hDC. -40, -100. 40, 100); EndPath(hDC); // Получение данных траектории и поворот на 45 градусов GetPath(hDC, Point, Type, nPoint);
506 Глава 9. Замкнутые области for (int i=0; i<nPoint; i++) { double x = Point[i].x * 0.707; double у = Point[i].y * 0.707; Point[i].x = (int) (x - y); Point[i].y = (int) (x + y); KGDIObject brush(hDC, CreateSolidBrush(RGB(OxFF. OxFF, 0))); KGDIObject pen (hDC. CreatePen(PS_SOLID. 19, RGB(0, 0, 0xFF))): for (int t=0; t<8; t++) { SetViewportOrgEx(hDC. 120+(tS4)*180. 120+(t/4)*180. NULL); // Построение траектории из повернутых эллипсов BeginPath(hDC); PolyDraw(hDC, Point, Type. nPoint); EndPath(hDC); if ( t>=4 ) SetPolyFillMode(hDC, ALTERNATE); else SetPolyFillMode(hDC, WINDING); switch ( t % 4 ) { case 0 case 1 case 2 case 3 FillPath(hDC); break StrokeAndFillPath(hDC); break WidenPath(hDC); FillPath(hDC) WidenPath(hDC); break; KGDIObject thin(hDC. CreatePen(PS_SOLID, 3, RGB(0, 0. OxFF))); StrokeAndFillPath(hDC); SetViewportOrgEx(hDC. 0. 0, NULL); Регионы В главе 7 мы в общих чертах познакомились с регионами, уделяя основное внимание их использованию при отсечении. В Win32 GDI регионы важны не только в качестве структур данных, но и при выводе. В этом разделе подробно рассматриваются регионы и основные области их применения. Ниже перечислены важнейшие области применения регионов в Windows- программировании (некоторые из них уже упоминались в главе 7).
Регионы 507 О Определение формы окна: SetWindowRgn. О Хранение информации об участках окна, нуждающихся в перерисовке: Inva- lidateRgn, GetUpdateRgn. О Отсечение: SelectClipRgn, SetMetaRgn. О Графический вывод: регион можно непосредственно воспроизвести на экране. О Проверка принадлежности: регионы могут использоваться для представления геометрических фигур. О DirectDraw: структура данных региона используется интерфейсом IDirect- Clipper. С точки зрения GDI регион определяет совокупность точек в координатном пространстве. Эта совокупность может быть пустой, а может занимать все координатное пространство; иметь прямоугольную или любую неправильную форму. Объект региона находится под управлением GDI и представляет некоторый регион в системе. Как и в случае с другими объектами GDI, после создания объекта региона приложение получает лишь его манипулятор, который может передаваться GDI при ссылках на этот объект. Манипуляторы регионов в GDI относятся к типу HRGN. Внутренняя структура данных, представляющая объект региона, достаточно сложна и может иметь весьма внушительные размеры. Когда объект региона станет ненужным, его следует удалить функцией DeleteObject. Создание объекта региона При создании новых объектов регионов используются следующие функции: HRGN CreateRectRgnCint nLeftRect. int nTopRect, int nRightRect, int nBottomRect); HRGN CreateRectRgnIndirect(CONST RECT * lprc); HRGN CreateRoundRectRgn(int nLeftRect.int nTopRect, int nRightRect, int nBottomRect, int nWidthEllipse. int nHeightEllipse): HRGN CreateEllipticRgnUnt nLeftRect, int nTopRect, int nRightRect, int nBottomRect); HRGN CreateEllipticRgnIndirect(CONST RECT * lprc); HRGN CreatePolygonRgnCCONST POINT * Ippt. int cPoints, int fnPolyFillMode); HRGN CreatePolyPolygonRgnCCONST POINT * lppt. CONST INT * lpPolyCounts. int nCount. int fnPolyFillMode); HRGN PathToRegion(HDC hDC); Все функции этой группы, за исключением PathToRegion, не зависят от контекста устройства, пера или кисти. Объект региона является независимым объектом, представляющим геометрическую фигуру. В другом контексте координаты региона интерпретируются как логические координаты или координаты устройства. Функция CreateRectRgn создает регион, содержащий все точки прямоугольной области, которая обычно определяется своими левым верхним и правым нижним углами. Две точки, определяющие прямоугольник, не обязательно должны быть правильно упорядочены; GDI нормализует их по правилам внутреннего представления GDI (левая координата меньше правой, верхняя координата
508 Глава 9. Замкнутые области меньше нижней). Функция CreateRectRgnIndirect представляет собой упрощенную разновидность CreateRectRgn, которая получает параметры через структуру RECT. В Windows NT/2000 реализация CreateRectRgnlndirect сводится к простому вызову CreateRectRgn. При использовании прямоугольного объекта региона он всегда интерпретируется по правилу исключения нижней и правой сторон. Это правило действует как в совместимом, так и в расширенном графических режимах. Например, вызов CreateRectRgn(0,0,0,0) создает пустой регион (вместо региона, содержащего единственную точку (0,0)). В системе координат устройства вызов CreateRectRgn (0,0,1,1) создает регион, содержащий единственную точку (0,0), и в этом смысле он эквивалентен вызову CreateRectRgn(1,1,0,0). Функция CreateRoundRectRgn(0,0,0,0) создает регион, состоящий из всех точек прямоугольника с закругленными углами. Каждый из четырех углов соответствует одной четверти эллипса nWidthET I ipsexnHeightEl 1 i pse. По каким-то неизвестным причинам при создании региона в виде прямоугольника с закругленными углами его нижняя и правая стороны исключаются из внутренней структуры данных, представляющей регион. Обратите внимание: ситуация отличается от прямоугольного региона, у которого нижняя и правая стороны включаются во внутреннее представление. Таким образом, при использовании в контексте устройства прямоугольника с закругленными углами с правой и нижней стороны исключаются по два ряда пикселов. При использовании в логической системе координат ширина исключаемых краев равна одной логической единице плюс одной единице устройства. Функция CreateEllipticRgn создает регион, состоящий из всех внутренних точек эллипса. Функция CreateEllipticRgnlndirect представляет собой упрощенный вариант, который переадресует вызов этой функции. Как и CreateRoundRectRgn, функция CreateEllipticRgn исключает нижнюю и правую стороны из внутреннего представления региона. Таким образом, при использовании эллиптического региона правая и нижняя стороны исключаются дважды. В Microsoft Knowledge Base имеется статья, посвященная проблеме исключения сторон при работе с функцией CreateEllipticRgn (Q83807). В ней сказано, что функция Ellipse включает в вычисления правый нижний угол ограничивающего прямоугольника, а функция CreateEllipticRgn исключает эту точку. Впрочем, утверждения Microsoft Knowledge Base расходятся с практикой. Из рис. 9.8 видно, что при вызове функции Ellipse в совместимом графическом режиме правая и нижняя стороны также исключаются. Как показывает рис. 9.15, функция CreateEllipticRgn в совместимом графическом режиме всегда исключает на одну логическую единицу больше, чем функция Ellipse. На рис. 9.15 изображены прямоугольник, прямоугольник с закругленными углами и эллипс. Рисунок позволяет изучить их строение на уровне отдельных пикселов. Эти три базовые фигуры были нарисованы несколькими способами — прямыми вызовами GDI API, созданием и выводом региона, преобразованием региона в траекторию и преобразованием траектории в регион. Все способы были проверены как в совместимом, так и в расширенном графическом режиме. Из рисунка видно, что в совместимом режиме функции Rectangle, Ellipse и RoundRectangle исключают правую и нижнюю стороны, а в расширенном режиме эти стороны включаются в расчеты. Функция CreateRectRgn всегда исключает
Регионы 509 правую и нижнюю стороны, а функции CreateRoundRectRgn и CreateEllipticRgn исключают их дважды. Однако из рисунка не видно, что фигура, нарисованная функцией CreateEllipticRgn, несколько отличается по форме от эллипса, нарисованного функцией Ellipse, даже если учесть поправку и увеличить ее размер на единицу. Если вам нужна стопроцентная точность (например, если созданный регион требуется для отсечения результата вызова Ellipse), Microsoft рекомендует использовать функцию региона. R 3 R R О О П п я п w й Прямой вызов функций GDI API Создание и вывод региона Преобразование региона в траекторию Преобразование траектории в регион GM_COMPATIBLE GM_ADVANCED GM_COMPATIBLE GM_ADVANCED GM_COMPATIBLE GM_ADVANCED Рис. 9.15. Функции CreateRectRgn, CreateRoundRectRgn и CreateEllipticRgn Функция CreatePolygonRgn создает регион, состоящий из всех внутренних точек многоугольника. Как упоминалось в разделе «Многоугольники», вопрос о принадлежности точки многоугольнику решается с учетом действующего режима заполнения многоугольников. Чтобы функция CreatePolygonRgn не зависела от контекста устройства, режим заполнения передается ей в последнем параметре. Обе функции включают все координаты в свои внутренние представления регионов, однако при закраске региона правая и нижняя стороны исключаются. Следовательно, площадь региона, созданного в результате вызова CreatePolygonRgn, меньше площади прямоугольника, созданного функцией Polygon с теми же параметрами. Последняя функция создания регионов, PathToRegion, преобразует текущую траекторию контекста устройства в регион. Объект траектории отличается от остальных объектов GDI тем, что он всегда остается связанным с конкретным контекстом устройства на уровне GDI API, поэтому GDI имеет возможность хранить объекты траекторий в координатах устройства, а не в логических координатах. Функция PathToRegion замыкает все незамкнутые фигуры траектории и преобразует их в регион в соответствии с текущим режимом заполнения многоугольников, выбранным в контексте устройства. Созданный регион использует систему координат устройства данного контекста — в отличие от функции GetPath, которой требуется обратное преобразование для перевода данных траектории из координат устройства в логические координаты. Хотя при построении региона функция PathToRegion задействует все исходные координаты, в процессе использования региона происходит исключение его правой и нижней сторон. Подведем итоги. Различия в площади региона и фигуры, нарисованной соответствующей функцией GDI, объясняется тремя причинами. Во-первых, функции CreateRectRgn, CreateRectRgnlndirect, CreatePolygonRgn, CreatePolyPolygonRgn
510 Глава 9. Замкнутые области и PathToRgn при построении внутреннего представления региона используют исходные координаты, а функции CreateRoundRectRgn, CreateEllipticRgn и CreateEllip- ticRgnIndirect уменьшают координаты правой и нижней сторон ограничивающего прямоугольника на единицу. Вероятно, это обстоятельство следует считать дефектом реализации, а не сознательным архитектурным решением. Во-вторых, при использовании региона в контексте устройства (с целью отсечения или при рисовании) его правая и нижняя стороны всегда исключаются. В-третьих, функции создания регионов одинаково ведут себя в обоих графических режимах, а функции рисования прямоугольников, эллипсов и прямоугольников с закругленными углами включают правую и нижнюю стороны в расширенном графическом режиме. Операции с объектами регионов Регион представляет собой множество точек двумерного пространства, поэтому определение операций над множествами для объектов регионов выглядит вполне естественно. В GDI предусмотрен богатый ассортимент функций для получения информации, перемещения, преобразования, сброса и объединения регионов. Прототипы этих функций приведены ниже. BOOL PtlnRegiorKHRGN hrgn, int X, int Y); BOOL RectInRegion(HRGN hrgn. CONST RECT * Iprc); BOOL EqualRgn(HRGN hSrcRgnl. HRGN hSrcRgn2); int GetRgnBox(HRGN hrgn. LPRECT Iprc); int CombineRgnCHRGN hrgnDest. HRGN hrgnSrcl. hrgnSrc2. int fnCombineMode); int OffsetRgnCHRGN hrgn.int nXOffset. int nYOffset): DWORD GetRegionData(HRGN hRgn. DWORD dwCount. LPRGNDATA lpRgnData); HRGN ExtCreateRegion(CONST XFORM * IpXForm, DWORD nCount. CONST RGNDATA * lpRgnData); Получение информации о регионе Функция PtlnRegion проверяет, принадлежит ли точка (х,у) множеству точек региона. Считается, что правая и нижняя стороны региона ему не принадлежат. Например, для пустого региона, созданного вызовом CreateRectRgn(0,0,0,0), функция PtlnRegion всегда возвращает FALSE; для региона из одной точки, созданного вызовом CreateRectRgn(0,0,l,l), функция PtlnRegion возвращает TRUE только для точки (0,0). Функция PtlnRegion чрезвычайно полезна при реализации экзотических разновидностей кнопок или интерактивных областей, изменяющих цвет под курсором мыши (что говорит о том, что щелчок на этой области обрабатывается каким-то особым образом). Приложение должно лишь создать объект региона, соответствующий интерактивной области, и вызвать функцию PtlnRegion при обработке сообщения WMM0USEM0VE для изменения изображения. Аналогичные действия следует включить и в обработку сообщений мыши, чтобы обнаружить щелчок внутри интерактивной области.
Регионы 511 В листинге 9.1 приведен класс KButton для работы с интерактивными кнопками, а также два производных класса для работы с прямоугольными и эллиптическими кнопками. Функция DefineButton задает ограничивающий прямоугольник кнопки. Виртуальная функция DrawButton создает объект региона и рисует кнопку в зависимости от того, был ли на ней сделан щелчок мышью. Функция IsOnButton при помощи PtlnRegion проверяет, находится ли точка (х,у) внутри кнопки. Функция UpdateButton обновляет изображение кнопки в соответствии с текущей позицией курсора мыши. Листинг 9.1. Класс KButton class KButton { protected: HRGN mJiRegion; bool m_bOn; int m_x, m_y, m_w. m_h; public: KButtonО { mJiRegion = NULL: m bOn = false; virtual -KButtonO { } void DefineButton(int x, int y, int w, int h) { m_x = x; m_y = y; m_w = w; m_h = h; } " virtual void DrawButton(HDC hDC) { void UpdateButton(HDC hDC. LPARAM xy) { if ( m_bOn != IsOnButton(xy) ) { m_bOn = ! m_bOn: DrawButton(hDC); bool IsOnButton(LPARAM xy) const { return PtlnRegionCmJiRegion. LOWORD(xy). HIWORD(xy)) != 0: Продолжение &
512 Глава 9. Замкнутые области Листинг 9.1. Продолжение class KRectButton : public KButton { public: void DrawButton(HDC hDC) { RECT rect = { m_x, m_y, m_x+m_w, m_y+m_h }; if ( mJiRegion == NULL ) m_hRegion = CreateRectRgnIndirect(& rect); InflateRect(&rect. 2. 2); FillRect(hDC. & rect. GetSysColorBrush(COLOR_BTNFACE)); InflateRect(&rect. -2. -2); DrawFrameControKhDC. &rect. DFC_CAPTION. DFCS_CAPTIONHELP | (m_bOn ? 0 : DFCSJNACTIVE)); } }: class KEllipseButton : public KButton { publi с: void DrawButtonCHDC hDC) { RECT rect = { m_x. m_y. m_x+m_w. m_y+m_h }; if ( mJiRegion == NULL ) m_hRegion = CreateEllipticRgnIndirect(& rect); if ( m_bOn ) { FillRgn(hDC. mJiRegion, GetSysColorBrush(COLOR_CAPTIONTEXT)); FrameRgn(hDC. m_hRegion. GetSysColorBrush(COLOR_ACTIVEBORDER). 2, 2); } else { FillRgn(hDC. m_hRegion. GetSysColorBrush(COLORJNACTIVECAPTIONTEXT)); FrameRgn(hDC. mJiRegion. GetSysColorBrush(COLORJNACTIVEBORDER). 2. 2); } } }: Код следующего фрагмента отображает клиентскую область с двумя интерактивными кнопками, изменяющими цвет при наведении на них курсора мыши. Если щелкнуть мышью на любой из этих кнопок, на экране появляется окно с сообщением. LRESULT KMyCanvas::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM lParam) { switch( uMsg )
Регионы 513 { case WM_CREATE: rbtn.DefineButtondO. 10. 50. 50): ebtn.DefineButtondO. 70. 50. 50); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = BeginPaint(m_hWnd. &ps); rbtn.DrawButton(hDC); ebtn.DrawButton(hDC); EndPaint(m_hWnd. &ps); } return 0; case WM_MOUSEMOVE: { HDC hDC = GetDC(hWnd) rbtn.UpdateButton(hDC ebtn.UpdateButton(hDC ReleaseDCChWnd. hDC); } return 0; case WM_LBUTTONDOWN: if ( rbtn.IsOnButton(lParam) ) MessageBox(hWnd. "Rectangle Button Clicked". NULL. MB_OK); if ( ebtn.IsOnButton(lParam) ) MessageBoxChWnd. "Ellipse Button Clicked". NULL. MB_OK); return 0; default: lr = DefWindowProcChWnd. uMsg. wParam. IParam); } } Еще одна функция, RectinRegi on, проверяет, принадлежат ли какие-либо из точек прямоугольника, заданного параметром lprc (за исключением правой и нижней сторон), заданному региону. Обратите внимание: функция не проверяет, находится ли весь прямоугольник внутри региона. Возможно, ее следовало бы переименовать в RectTouchRegion. Функция Equal Region сравнивает два региона и проверяет, содержат ли они одинаковые множества точек. Если в двух параметрах передаются одинаковые манипуляторы, несомненно, регионы совпадают. Но даже разные манипуляторы могут соответствовать одинаковым множествам точек. Например, вызовы CreateRectRgn(0,0,0,0) и CreateRectRgnQ, 1,1,1) создают пустые регионы, которые с точки зрения функции Equal Rect являются равными. По наличию функции Equal Region можно сделать обоснованное предположение о том, что объекты регионов обладают однозначным внутренним представлением, то есть каждому множеству точек соответствует ровно одно представлерше. В противном случае функция Equal Region работала бы очень медленно. IParam); IParam);
514 Глава 9. Замкнутые области Функция GetRgnBox возвращает ограничивающий прямоугольник региона. Для пустого множества точек ограничивающий прямоугольник всегда определяется квартетом {0,0,0,0}. Для других прямоугольных регионов ограничивающий прямоугольник представляет собой исходный прямоугольник региона, нормализованный таким образом, чтобы поле left было меньше right, a top — меньше bottom. Как говорилось выше, для регионов в форме эллипса или прямоугольника с закругленными углами GDI удаляет по одной единице с правой и нижней сторон, поэтому ограничивающий прямоугольник получается меньше прямоугольника, указанного при определении региона. Например, CreateEl I i pti cRgn(10,10,1,1) возвращает регион с ограничивающим прямоугольником {1,1,9,9}. По ограничивающему прямоугольнику региона можно быстро узнать, принадлежит ли отдельная точка или какие-либо точки области заданному региону; это особенно важно при критических требованиях по быстродействию. Прямоугольник, возвращаемый функцией GetRgnBox, позволяет приложению выполнить быструю проверку без применения функций GDI и переходов из пользовательского режима в режим ядра. Прямоугольник rcPaint в структуре PAINTSTRUCT, заполняемой функцией BeginPaint, содержит данные ограничивающего прямоугольника для системного региона окна. При помощи этого прямоугольника многие приложения определяют, нужно ли перерисовывать те или иные объекты при обработке сообщения WMPAINT. Операции с множествами Функция CombineRgn позволяет выполнять с объектами регионов некоторые полезные операции, позаимствованные из теории множеств. Функция получает три объекта регионов hrgnDest, hrgnSrcl и hrgnSrc2, а также целочисленный параметр fnCombineMode. При вызове CombineRgn параметр hrgnDest должен содержать манипулятор действительного объекта региона. Функция заменяет объект региона, представленный этим манипулятором, объектом, сгенерированным при вызове функции. Параметр fnCombi neMode определяет операцию, выполняемую с регионами hrgnSrcl и hrgnSrc2, — копирование, пересечение, объединение, вычитание или симметричная разность. Операции с регионами, обеспечивающие пять различных режимов комбинирования регионов, были перечислены в главе 7 (см. табл. 7.1), а графическое представление этих операций приведено на рис. 9.16. Два региона RGN_AND RGN_OR RGN_XOR RGN_DIFF RGN_COPY Рис. 9.16. Операции с регионами
Регионы 515 Функции GetRgnBox и CombineRgn возвращают целочисленный код сложности сгенерированного региона или код ошибки. Возможные результаты перечислены в табл. 9.1. Таблица 9.1. Результаты вызовов функций GetRgnBox и CombineRgn Константа Описание NULLREGION Пустой регион SIMPLEREGION Регион определяется одним прямоугольником C0MPLEXREGI0N Регион определяется несколькими прямоугольниками ERROR Ошибка — недопустимые значения параметров или нехватка памяти. Регион не создается Если регион представляет собой множество точек, как должно выглядеть универсальное множество (то есть множество, содержащее все возможные точки)? В Win32 GDI логические координаты задаются в виде 32-разрядных целых чисел, а координаты устройства в системах семейства NT задаются 27-разрядными неотрицательными целыми числами. В системах, не входящих в семейство NT, координаты усекаются до 16-разрядных целых чисел. Следовательно, универсальное множество для регионов должно определяться ограничивающим прямоугольником [0x80000000,0x80000000,0x7FFFFFFF,0x7FFFFFFFl. Однако в системах семейства NT, похоже, GDI усекает эти числа до 28-разрядных целых, поэтому ограничивающий прямоугольник универсального множества уменьшается до [-(1«27),-(1«27),(1«27)-1,(1«27)-1]. Действует и другое недокументированное ограничение: при использовании функций регионов значения логических координат ограничиваются 28-разрядными целыми числами со знаком вместо 32-разрядных целых чисел со знаком. Операции с множествами очень полезны при геометрических вычислениях. Если вы хотите узнать, перекрываются ли две замкнутые траектории, можно преобразовать их в многоугольники функцией FlattenPath и самостоятельно реализовать алгоритм проверки перекрытия для многоугольников, но сделать это не так уж просто. Существует другое решение: преобразовать траектории в регионы и вычислить их пересечения функцией CombineRgn(RGN_AND). Если пересечение не пусто, значит, две исходные траектории пересекаются друг с другом. Такие проверки часто встречаются при программировании игр, где соприкосновение двух объектов обычно сопровождается теми или иными действиями. Используя операции с множествами, можно легко определить, содержится ли регион внутри другого региона. Выше уже говорилось о том, что функция RectlnRegion проверяет лишь факт соприкосновения, то есть наличия общих точек у прямоугольника и региона. Приведенная ниже функция проверяет, содержится ли прямоугольник внутри региона. Для этого она вычисляет объединение прямоугольника с регионом функцией CombineRgn и затем при помощи функции Equal Rgn проверяет, совпадает ли объединенный регион с исходным. BOOL RectContainedlnRegiorKHRGN hrgn. CONST RECT * Iprc) { HRGN hCombine = CreateRectRgnIndirect(Iprc);
516 Глава 9. Замкнутые области CombineRgrU hCombine. hrgn, hCombine, RGN_0R); BOOL rslt = EqualRgnChCombine. hrgn); DeleteObject(hCombine); return rslt; Преобразования данных регионов GDI поддерживает преобразования смещения, зеркального отражения и масштабирования между страничной системой координат и системой координат устройства. В системах семейства NT интерфейс GDI поддерживает более общие аффинные преобразования между мировыми и страничными координатными пространствами, обеспечивающие возможность поворота и сдвига. Все эти преобразования поддерживаются и для объектов регионов с одним логичным ограничением — повороты и сдвиг поддерживаются напрямую только в системах семейства NT. Функция OffsetRgn обеспечивает простейшее преобразование — смещение. Она получает величины смещений по осям х и г/, прибавляет их ко всем координатам объекта региона и возвращает код сложности региона. Функция OffsetRgn может применяться для многократного рисования объектов на поверхности устройства (прорисовкой самого региона или его применением для отсечения). Приложение может использовать регион, переместить его в другое место функцией OffsetRgn и воспользоваться им снова. Кроме того, при помощи этой функции можно отслеживать движущиеся объекты в игре или анимационном ролике. Если, например, регион описывает контуры гоночной машины, то при движении машины должен перемещаться и регион, используемый для обнаружения столкновений. Более общие преобразования выполняются двумя функциями: GetRegionData и ExtCreateRegion. Функция GetRegionData преобразует внутреннюю структуру данных региона в структуру RGNDATA, которая может использоваться в программе. Функция ExtCreateRegion получает структуры RGNDATA и XF0RM (определение аффинного преобразования), преобразует данные и создает новый регион. Важнейшая структура RGNDATA определяется следующим образом: typedef struct _RGNDATAHEADER { DWORD dwSize; // sizeof(RGNDATAHEADER) DWORD iType; // RDH_RECTANGLES DWORD nCount; // количество прямоугольников в регионе DWORD nRgnSize; // размер буфера с данными региона RECT rcBounds; // ограничивающий прямоугольник } RGNHEADER; typedef struct _RGNDATA { RGNDATAHEADER rdh; char Buffer[l]; // переменный размер } RGNDATA; При знакомстве со структурой RGNDATA следует обратить внимание на некоторые интересные обстоятельства. Во-первых, RGNDATA не является внутренней структурой данных, используемой для представления регионов в GDI. В системах семейства NT регионы представляются более эффективной структурой данных — динамическим массивом REGI0N0BJ, содержащим массив структур SCAN. Структу-
Регионы 517 pa SCAN описывает «строку развертки региона», то есть пересечение региона с областью, ограниченной двумя горизонтальными линиями, при условии, что пересечение контура региона с этой областью состоит только из вертикальных отрезков. В структуре SCAN хранится массив координат х этих отрезков, причем количество элементов массива всегда четно. Нет никаких фактов, которые бы подтверждали, что регионы представляются трапециевидными фигурами, как заявлено в документации Microsoft. Описание региона массивом структур SCAN позволяет хранить для каждого пересечения только координату х, поскольку координаты у у них одинаковые; тем самым обеспечивается экономия памяти. Кроме того, упорядоченность массива структур SCAN сверху вниз и слева направо обеспечивает однозначное представление регионов и эффективные операции с ними. Например, при объединении нескольких мелких регионов в один большой регион функцией CombineRgn внутреннее представление итогового региона не должно зависеть от порядка объединения регионов. За подробностями обращайтесь к разделу «WinDbg и расширение отладчика GDI» в главе 3. В системах, не входящих в семейство NT, вместо 32-разрядных координат используются 16-разрядные. Объем памяти, занимаемой структурой REGI0N0BJ, зависит от сложности региона. Предположим, регион делится на N строк развертки и среднее количество пересечений на строку равно М. Минимальная высота строки развертки равна единице, но может быть равна и нескольким единицам. Объем структуры REGI0N0BJ вычисляется по формуле: sizeof(REGIONOBJ) = (4*М + 16)*(N+2)+40 Для прямоугольного региона одна строка развертки занимает весь прямоугольник, поэтому N - 1, М = 2; объем структуры равен всего 112 байтам. GDI может хранить только ограничивающий прямоугольник и специальный флаг, который указывает на то, что это простой регион. Для эллиптического региона количество строк развертки приближается к 2/3 высоты эллипса, М = 2. При создании региона для эллипса, занимающего всю страницу на принтере с разрешением 600 dpi, N = 2/3 х И х 600 = 4400, sizeof (REGI0N0BJ) = 111 Кбайт. Благодаря регионам приложение может реализовать цветовые ключи; для этого регион создается на базе всех пикселов растра, цвет которых отличается от цветового ключа. Этот регион используется для отсечения при выводе растра, в результате чего все пикселы, цвет которых совпадает с цветом ключа, не выводятся. В худшем случае значение N равно высоте растра, М — половине ширины растра, а структура REGI0N0BJ содержит по 2 байта на каждый пиксел. Если ваше приложение интенсивно работает с регионами, не забывайте о затратах памяти. В действительности графический механизм выделяет больше памяти, чем необходимо для представления региона; излишек предназначен для возможного увеличения размера растра. Аналогичная стратегия используется при работе с динамическими массивами для сведения к минимуму затрат на динамическое выделение памяти и копирование данных. Структура REGI0N0BJ фактически является двумерной; первое измерение представляет собой массив структур SCAN, упорядоченных по возрастанию координаты у, а второе измерение — упорядоченный массив координат х. Такая архитектура обеспечивает приемлемое быстродействие операций с регионами.
518 Глава 9. Замкнутые области Предположим, требуется узнать, принадлежит ли точка региону. Если точка прошла проверку на принадлежность ограничивающему прямоугольнику, ее координату у можно сравнить с координатой у каждой структуры SCAN методом линейного поиска. Размер структуры SCAN хранится в ее начале и в конце, что значительно упрощает переход к следующей структуре. После нахождения нужной структуры SCAN производится следующий линейный поиск по координате х. Таким образом, время выполнения PtlnRegion имеет порядок 0(N) + 0(М). Оптимальный алгоритм с применением бинарного поиска обеспечивает порядок OClog(N)) + 0(log(M)), но это требует усложнения структуры данных. GDI по возможности старается использовать небольшие структуры данных, объединяемые указателями, чтобы свести к минимуму динамическое выделение памяти. Функция CombineRgn для объединения, пересечения и вычитания регионов обладает аналогичной сложностью в отношении количества необходимых сравнений. Впрочем, копирование данных в новый регион требует дополнительного времени. Таким образом, при объединении п регионов функцией CombineRgn в худшем случае сложность имеет порядок 0(п2), то есть с удвоением количества регионов затраты времени возрастают в четыре раза. Подобных алгоритмов следует по возможности избегать. Структура RGNDATA обеспечивает единый интерфейс для работы с регионами в приложениях Win32, работающих на разных платформах. Кроме того, структура RGNDATA используется интерфейсом IDirectDrawClipper DirectDraw. Эта структура содержит заголовок фиксированного размера с информацией о размере региона, типе и ограничивающем прямоугольнике, а также массив структур RECT. В RGNDATA двумерное внутреннее представление региона в GDI преобразуется в одномерную структуру данных. Для представления региона с N строками развертки и средним количеством пересечений на строку, равным М, необходимо М/2 х N прямоугольников. Общие затраты памяти вычисляются по следующей формуле: sizeof(RGNDATA) = 8 * М * N + 32 Для региона, состоящего из одной выпуклой фигуры (например, прямоугольника, эллипса или прямоугольника с закругленными углами) М = 2, поэтому структура RGNDATA занимает примерно 2/3 от объема REGI0N0BJ. При больших значениях М объем структуры RGNDATA почти вдвое превышает объем REGI0N0BJ. Структура RGNDATA (как и структура REGI0N0BJ) генерируется GDI, и ее элементы всегда располагаются в строго определенном порядке. Входящие в нее структуры RECT упорядочиваются слева направо, сверху вниз. Все структуры RECT нормализованы, то есть поле left меньше right, a top меньше bottom. Поскольку структура RGNDATA представляет собой линейный массив прямоугольников, приложение может ускорить работу некоторых алгоритмов. Например, проверку наличия общих точек у прямоугольника с регионом, представленным структурой RGNDATA, можно осуществить с применением бинарного поиска в массиве вместо линейного, что позволяет уменьшить сложность до 0(log(M x N)). С другой стороны, при операциях с множествами, использующими CombineRgn, производится копирование данных, при этом затраты времени связаны линейной зависимостью с объемом данных. Функция GetRegionData записывает структуру RGNDATA в буфер, предоставленный приложением. Впрочем, перед вызовом GetRegionData точный размер струк-
Регионы 519 туры обычно неизвестен приложению. Одна из возможных стратегий выглядит так: приложение берет размер RGNDATA, подходящий для 80 % случаев, выделяет буфер соответствующего размера (вероятно, в стеке) и вызывает функцию GetRegionData, передавая ей размер буфера и указатель на него. Если буфер окажется достаточно большим, он заполняется структурой RGNDATA, точный размер которой возвращается функцией. Если буфер слишком мал, функция GetRegionData возвращает 0 и буфер не заполняется. В этом случае приложение вызывает GetRegionData, передавая 0 в параметре dwCount и NULL в параметре IpRgnData; GDI возвращает требуемый размер буфера. Приложение выделяет память (как правило, из кучи) и снова вызывает GetRegionData, передавая точный размер буфера и указатель на него. Конечно, приложение может отказаться от самого первого вызова и сразу вызвать GetRegionData для получения размера буфера. На рис. 9.17 показано, как выглядит структура RGNDATA для регионов в виде прямоугольника, прямоугольника с закругленными углами, эллипса и треугольника; все эти регионы имеют одинаковый ограничивающий прямоугольник {0,0,21,21}. Прямоугольный регион состоит из единственного прямоугольника; регион в форме закругленного прямоугольника содержит 7 прямоугольников, в основном для закругленных углов; эллиптический регион содержит 13 прямоугольников, а у треугольного региона количество прямоугольников достигает 21. Ограничивающие прямоугольники RGNDATA на рисунке обведены черным пером, а прямоугольники массива RECT окрашены попеременно в темно-серый и светло-серый цвета. CreateRectRgn: (0,0,21,21) rcBound: (0,0,21,21) nCount: 1 CreateRoundRectRdn: (0,0,21,21,10,10) rcBound: (0,0,20,20) nCount: 7 CreateEllipticRgn: (0,0,21,21) rcBound: (0,0,20,20) nCount: 13 CreatePolygonRgn: (0,0,21,0,10,21) rcBound: (0,0,21,21) nCount: 21 Размер: 48 байт Размер: 144 байта Размер: 240 байт Размер: 368 байт Рис. 9.17. Структура RGNDATA для разных видов регионов Функция ExtCreateRegion позволяет создать объект региона по структуре RGNDATA с возможностью применения аффинного преобразования к данным региона. Структуру RGNDATA можно получить либо непосредственно от GDI при помощи функции GetRegionData, либо сгенерировать в приложении. При вызове ExtCreateRegion все структуры RECT должны быть нормализованы, а поле rcBounds структуры RGNDATA должно содержать общий ограничивающий прямоугольник. В противном случае попытка вызова завершится неудачей или регион будет сгенерирован неверно.
520 Глава 9. Замкнутые области Первый параметр функции ExtCreateRegion содержит указатель на матрицу аффинного преобразования (в системах, не входящих в семейство NT, преобразование не может включать сдвиги и повороты). Преобразования регионов очень часто требуются в приложениях. Например, регион, возвращаемый функцией PathToRegion, определяется в системе координат устройства. Если регион используется непосредственно для рисования, а не для отсечения, приложение должно преобразовать его в логическую систему координат. Для простейшего смещения достаточно функции Of fsetRgn, но для более общих преобразований следует воспользоваться функцией ExtCreateRegion. Структура RGNDATA представляет регион в целочисленных координатах; кривые аппроксимируются отрезками, как при вызове FlattenPath для траектории. Следовательно, при масштабировании часто возникают «зазубрины». Если регион можно определить в виде траектории, преобразование траектории с последующим переходом к региону обеспечит более точный результат. По имеющейся информации функция ExtCreateRegion в системах, не входящих в семейство NT, не может одновременно работать более чем с 4000 прямоугольников. Существует обходное решение — разделить большую структуру RGNDATA на несколько меньших, вызвать ExtCreateRegion для каждой структуры, а затем объединить результаты функцией Combi neRgn. В листинге 9.2 приведен простой класс для работы с функциями GetRegion- Data и ExtCreateRegion. Листинг 9.2. Класс KRegion: работа с данными региона class KRegion { public: int mjiRegionSize; i nt mjiRectCount ; RECT * m_pRect; RGNDATA * m_pRegion; KRegionO { mjiRegionSize mjiRectCount m_pRegion m_pRect } void Reset(void) { if ( m_pRegion ) delete [] (char *) m_pRegion; m_pRegion = NULL; mjiRegionSize = 0; mjiRectCount = 0: m_pRect = NULL; } - 0; = 0; « NULL; = NULL; -KRegionO
Регионы 521 ResetO; BOOL GetRegionData(HRGN hRgn); HRGN CreateRegionCXFORM * pXForm); BOOL KRegion::GetRegionDataCHRGN hRgn) { ResetO; mjiRegionSize = : :GetRegionData(hRgn. 0, NULL); if ( m_nRegionSize==0 ) return FALSE; m_pRegion = (RGNDATA *) new char[mjiRegionSize]; if ( m_pRegion==NULL ) return FALSE; ::GetRegi onData(hRgn, mjiRegi onSi ze, m_pRegi on); mjiRectCount = m_pRegion->rdh.nCount; m_pRect = (RECT *) & m_pRegion->Buffer; return TRUE; HRGN KRegion::CreateRegion(XFORM * pXForm) { return ExtCreateRegion(pXForm, mjiRegionSize. m_pRegion); } Функции GetRegi onData и ExtCreateRegi on также позволяют приложениям самостоятельно строить и преобразовывать структуры RGNDATA и передавать их GDI для создания регионов. Такая возможность может пригодиться для реализации поворотов или сдвигов в системе, не входящей в семейство NT, или для преодоления нежелательных затрат 0(п2) при объединении п регионов функцией CombineRgn. Прорисовка регионов В GDI предусмотрено несколько функций для прорисовки области, занимаемой регионом с заданным манипулятором: BOOL FilIRgnCHDC hDC, HRGN hrgn, HBRUSH hbr); BOOL PaintRgn(HDC hDC, HRGN hrgn); BOOL FrameRgn(HDC hDC, HRGN hrgn. HBRUSH hbr, int nWidth, int nHeight); BOOL InvertRgn(HDC hDC. HRGN hrgn); Все эти функции получают манипулятор контекста устройства и манипулятор региона. Координаты региона задаются в логической системе координат,
522 Глава 9. Замкнутые области а не в системе координат устройства, как координаты регионов отсечения. Следовательно, этим функциям нельзя непосредственно передать манипулятор региона, возвращаемый функцией PathToRegion (разве что логические координаты идентичны координатам устройства или же вы действуете сознательно). Графический механизм преобразует объект региона в координаты устройства с исключением правой и нижней сторон. Функция FillRgn закрашивает регион кистью, определяемой параметром hbr. Функция PaintRgn делает то же самое, но задействует текущую кисть контекста устройства. В GDI для этих двух функций используется одна и та же реализация. Функция FrameRgn обводит контур региона кистью, ширина и высота которой определяются при вызове функции. Это позволяет создавать контуры переменной толщины; при использовании обычного пера, которое всегда рисует линии постоянной толщины, это невозможно. Функция FrameRgn интерпретирует свое «перо» как прямоугольник, параллельный осям, причем весь вывод осуществляется только внутри региона и никогда не выходит за его пределы. Функция InvertRgn инвертирует пикселы кадрового буфера устройства так же, как при использовании растровой операции R2N0T. По принципу работы она напоминает функцию InvertRect. Первые три функции, FillRgn, PaintRgn и FrameRgn, используют текущую бинарную растровую операцию и учитывают действующий режим заполнения фона. Функция InvertRgn инвертирует весь регион операцией R2_N0T. На рис. 9.18 показано, как перечисленные функции трансформируют закругленный прямоугольник, нарисованный функцией RoundRect. PaintRgn FrameRgn(1,1) FrameRgn(10,1) InvertRgn RoundRect FillRgn FrameRgnfUO) FrameRgn(10,10) Рис. 9.18. Функции PaintRgn, FillRgn, FrameRgn и InvertRgn На первый взгляд функции регионов принципиально не отличаются от других функций GDI, однако создание объектов регионов и операции с ними связаны со значительными затратам времени и памяти, особенно при усложнении формы региона. Если форму региона можно легко воспроизвести другими средствами GDI (функциями рисования прямоугольников, эллипсов, закругленных прямоугольников и траекторий), предпочтение следует отдать этому способу. Функции прорисовки регионов следует использовать для замены более дорогостоящих операций — например, функций вывода отдельных пикселов или заливок. Допустим, приложение рисует на экране два перекрывающихся круга и хочет закрасить общую область некоторой кистью. В современных реализациях GDI получить определения двух дуг, ограничивающих эту область, не так просто, зато средствами GDI можно легко вычислить пересечение двух круговых регионов.
Градиентные заливки 523 Градиентные заливки До недавнего времени средства GDI позволяли закрасить замкнутую область одноцветной однородной кистью, двуцветной штриховой кистью или узорной кистью, количество цветов в которой определялось количеством цветов в растре. Но с распространением видеоадаптеров и принтеров, обладающих повышенной цветовой глубиной, приложения начали использовать большее количество цветов, чтобы изображение выглядело более привлекательно. Одной из разновидностей цветовых эффектов являются градиентные заливки — заполнение области многочисленными цветами, сгенерированными по определенному правилу. Поддержка градиентных заливок впервые была реализована в профессиональных приложениях (таких, как Photoshop, CorelDraw и Microsoft Office). Начиная с Windows 98 и Windows 2000, градиентные заливки стали частью GDI. В Win32 GDI поддержка градиентных заливок обеспечивается одной функцией, работа которой определяется тремя новыми структурами данных. typedef struct _TRIVERTEX { LONG x; LONG y; C0L0R16 Red; C0L0R16 Green; C0L0R16 Blue; C0L0R16 Alpha; } TRIVERTEX. * PTRIVERTEX, * LPTRIVERTEX; typedef struct _GRADIENT_TRIANGLE { ULONG Vertexl; ULONG Vertex2; ULONG Vertex3; } GRADIENTJRIANGLE. *PGRADIENTJRIANGLE. *LPGRADIENTJRIANGLE: typedef struct _GRADIENT_RECT { ULONG UpperLeft; ULONG LowerRight; } GRADIENT_RECT. *PGRADIENT_RECT. *LPGRADIENT_RECT; BOOL GradientFilKHDC hDC. CONST PTRIVERTEX pVertex. DWORD dwNumVertex. CONST PVOID pMesh, DWORD dwNumMesh. DWORD dwMode); Функция GradientFm обладает рядом отличительных особенностей. Во-первых, перед типами указателей отсутствуют префиксы long и far, унаследованные от Win 16. Во-вторых, к традиционному 3-канальному формату RGB добавился новый альфа-канал. В-третьих, 8-разрядных цветовых каналов оказывается недостаточно, поэтому используются 16-разрядные каналы. Все это наглядно свидетельствует о постепенном совершенствовании API. Функция GradientFill заполняет один или несколько прямоугольников (или треугольников — в зависимости от последнего параметра dwMode). В настоящий момент параметр dwMode может принимать три допустимых значения, перечисленных в табл. 9.2.
524 Глава 9. Замкнутые области Таблица 9.2. Режимы функции GradientFill Значение параметра dwMode Смысл GRADIENTFILLRECTH Прямоугольник заполняется цветами, изменяющимися слева направо. По вертикали цвет остается постоянным GRAD I ENTF ILLRECTV Прямоугольник заполняется цветами, изменяющимися сверху вниз. По горизонтали цвет остается постоянным GRAD I ENTF ILLRECTTRI ANGLE Треугольник заполняется цветами, интерполированными по трем вершинам Отдельный прямоугольник или треугольник называется «ячейкой» (mesh) — этот жаргонный термин пришел из программирования компьютерных игр. Количество ячеек передается в параметре dwNumMesh; указатель pMesh ссылается на массив структур (либо GRADIENT_RECT, либо GRADIENTJRIANGLE). Структура GRADIENT_ RECT содержит индексы левого верхнего и правого нижнего угла прямоугольника. Структура GRADIENTTRI ANGLE содержит индексы трех вершин треугольника. Индексы относятся к массиву TRIVERTEX, на который ссылается параметр pVertex. Параметр dwNumVertex определяет количество элементов в массиве TRIVERTEX. Итак, для вершины каждого прямоугольника или треугольника существует структура TRIVERTEX, определяющая ее позицию и цвет. Позиция задается в логической системе координат с использованием 32-разрядных значений. Их цвет определяется четырьмя 16-разрядными каналами (красный, зеленый, синий и альфа-канал). Для горизонтальной градиентной заливки прямоугольника, если левый верхний угол имеет координаты (хО,уО), а правый нижний — (х1,у1), цвет точки (х,у) вычисляется по формуле: С(х.у) - ( C(xl.yl) * (х-хО) + С(хО.уО) * (xl-x) ) / (xl-xO) Здесь С(х,у) означает интенсивность одного из цветовых каналов в точке (х,у). При вертикальной градиентной заливке прямоугольников используется аналогичная формула, зависящая от координаты у: С(х.у) - ( C(xl.yl) * (у-уО) + С(хО.уО) * (yl-y) ) / (yl-yO) С градиентными заливками треугольников дело обстоит несколько сложнее. Если три вершины имеют координаты (хО,уО), (х1,у1) и (х2,у2)> то из внутренней точки (х,у) можно провести три отрезка, разделяющие треугольник на три меньших треугольника. Если ai — площадь треугольника, противолежащего по отношению к точке (xi,yi), цвет в точке (х,у) вычисляется по формуле: С(х.у) - ( С(хО.уО) * аО + C(xl.yl) * al + C(x2.y2) * д2 ) / (аО + al + a2) Для прямоугольников формула интерполяции зависит от расстояния, поэтому вполне естественно, что формула интерполяции для треугольников зависит от площади. Для прямоугольников цвет образует прямую линию на плоскости, образованной одной из осей и каждым цветовым каналом. При градиентной заливке треугольника цвет образует плоскость в трехмерном пространстве, образованном осями х, у и каждым цветовым каналом.
Градиентные заливки 525 Градиентная заливка прямоугольников Чтобы изучить использование функции GradientFill на конкретном примере, давайте попробуем создать разные градиентные заливки для одного прямоугольного региона. Сколько вариантов вы сможете изобрести? 14 самых распространенных комбинаций изображены на рис. 9.19. Рис. 9.19. Градиентная заливка прямоугольной области В верхних четырех заливках цвет изменяется в одном направлении — слева направо, сверху вниз или по диагонали. В следующем ряду прямоугольник делится на две части, и заполнение осуществляется от центра в противоположных направлениях. В нижнем ряду заливка распространяется из одного угла на весь прямоугольник. В двух последних примерах (справа) заливка идет от наружного контура в центр. Программный код, при помощи которого был построен этот рисунок, частично приведен в листинге 9.3. Листинг 9.3. Градиентная заливка прямоугольных областей inline C0L0R16 R16CC0L0RREF с) { return GetRValue(c)«8; } inline C0L0R16 G16CC0L0RREF с) { return GetGValue(c)«8; } inline C0L0R16 B16CC0L0RREF c) { return GetBValue(c)«8; } inline C0L0R16 R16(C0L0RREF cO. COLORREF cl) { return ((GetRValue(cO)+GetRValue(cl))/2)«8; } inline C0L0R16 G16CC0L0RREF cO, COLORREF cl) { return ((GetGValue(cO)+GetGValue(cl))/2)«8; } inline C0L0R16 B16(C0L0RREF cO. COLORREF cl) { return ((GetBValue(cO)+GetBValue(cl))/2)«8; } BOOL GradientRectangleCHDC hDC, int xO, int yO. int xl, int yl. COLORREF cO. COLORREF cl. int angle) { TRIVERTEX vert[4] = { { xO. yO. R16(c0). G16(c0). B16(c0). 0 }. { xl. yl. R16(cl). G16(cl). B16(cl). 0 }. Продолжение #
526 Глава 9. Замкнутые области Листинг 9.3. Продолжение { хО. yl. R16(c0. cl). G16(c0. cl). B16(c0. cl). О }. { xl. уО. R16(c0. cl). G16(c0. cl). B16(c0. cl). О } }: ULONG Index[] = { 0. 1. 2. 0. 1. 3}: switch ( angle % 180 ) { case 0: return GradientFilKhDC. vert. 2. Index. 1, GRADIENT_FILL_RECT_H); case 45: return GradientFilKhDC. vert. 4. Index.2. GRADIENTJILLJRIANGLE); case 90: return GradientFilKhDC. vert. 2. Index. 1. GRADIENT_FILL_RECT_V): case 135: vert[0].x = xl; vert[3].x = xO: vert[l].x = xO; vert[2].x = xl: return GradientFilKhDC. vert. 4. Index. 2. GRADIENTJILLJRIANGLE): } return FALSE: BOOL CornerGradientRectangle(HDC hDC. int xO. int yO. int xl. int yl. COLORREF cO. COLORREF cl. int corner) { TRIVERTEX vert[] = { { xO. yO. R16(cl). G16(cl). B16(cl). 0 }. { xl. yO. R16(cl). G16(cl). B16(cl). 0 }. { xl. yl. R16(cl). G16(cl). B16(cl). 0 }. { xO. yl. R16(cl). G16(cl). B16(cl). 0 } vert[corner].Red = R16(c0) vert[corner].Green = G16(c0) vert[corner].Blue = B16(c0) ULONG Index[] = { corner, (согпег+Ш4. (corner+2)X4. corner. (corner+3)U4, (corner+2U4 }: return GradientFilKhDC. vert. 4. Index. 2. GRADIENTJILLJRIANGLE): } Функция GradientRectangle рисует четыре заливки из первого ряда; в первом и третьем многоугольнике используется простая прямоугольная заливка.
Градиентные заливки 527 Второй и четвертый примеры нарисованы посредством треугольной заливки. Функция CornerGradientRectangle рисует четыре заливки в третьем ряду, во всех случаях используется комбинация двух треугольников. В приведенном фрагменте определено несколько подставляемых функций для преобразования 8-разрядных значений RGB в 16-разрядные, используемые в структуре TRI VERTEX, и вычисления усредненных цветов. Обратите внимание: в приведенном примере не задействованы структуры GRADIENTRECT и GRADIENTTRI ANGLE; вместо этого мы напрямую работаем с массивами длинных беззнаковых индексов. Применение градиентных заливок для создания объемных кнопок Комбинация нескольких градиентных заливок создает интересные эффекты. Благодаря механизму отсечения градиентные заливки можно применять и к непрямоугольным областям; например, это позволяет имитировать объемный вид кнопок. На рис. 9.20 изображены три объемные кнопки, созданные при помощи функции GradientRectangle. Рис. 9.20. Применение градиентных заливок для создания объемных кнопок Первая прямоугольная кнопка нарисована путем градиентной заливки от темного цвета к светлому и последующей закраски меньшего участка от светлого цвета к темному. В результате возникает впечатление искривленной поверхности. Следующие две кнопки созданы аналогично, но в них использовано отсечение по закругленным прямоугольникам и эллипсам. На самом деле все три кнопки отсекаются по регионам в виде закругленных прямоугольников с разной степенью закругления углов. Функция создания кнопок приведена ниже. void RoundRectButton(HDC hDC. int xO, int yO. int xl. int yl. int w, int d. COLORREF cl. COLORREF cO) { for (int i=0; i<2; i++) { POINT P[3] = { xO+d*i. yO+d*i, xl-d*i, yl-d*i. xO+d*i+w, yO+d*i+w }; LPtoDP(hDC, P. 3); HRGN hRgn = CreateRoundRectRgn(P[0].x. P[0].y,
528 Глава 9. Замкнутые области Р[1].х. Р[1].у. Р[2].х-Р[0].х. Р[2].у-Р[0].у); SelectClipRgn(hDC, hRgn): DeleteObject(hRgn); if ( i==0 ) GradientRectangle(hDC, xO. yO, xl. yl, cl. cO, 45); else GradientRectangleChDC, xO+d, yO+d, xl-d. yl-d. cO. cl, 45); } SelectClipRgnChDC. NULL); } Функция в цикле выполняет две градиентные заливки с разными размерами. Отсечение несколько усложняется тем, что для создания правильной области отсечения функция должна определять ее размеры и положение в системе координат устройства. Впрочем, эти небольшие дополнительные усилия позволяют использовать функцию в любой логической системе координат. Практическое использование заливок Заливки являются важным аспектом любых графических приложений, от простейших текстовых и графических редакторов до сложных пакетов профессиональной графики. В GDI поддерживаются три основных средства для создания заливок: О кисти и градиенты, определяющие цвет и узор заливки; О функции заливки, позволяющие непосредственно закрашивать простые геометрические фигуры; О механизм отсечения, обеспечивающий свободу выбора границ закрашиваемой области. Поддержка заливок в GDI достаточно близка к возможностям, поддерживаемым в современных графических пакетах: О одноцветные однородные заливки (включая полупрозрачные); О градиентные заливки; О текстурные заливки; О узорные заливки; О растровые заливки. Полупрозрачная заливка Одноцветные однородные заливки легко создаются при помощи однородных кистей GDI. При создании полупрозрачной заливки каждый второй пиксел закрашивается определенным цветом, а остальные пикселы приемника остаются без изменений. Для решения этой задачи можно воспользоваться шахматной узорной кистью и двумя бинарными растровыми операциями. Следующая функция создает полупрозрачную заливку в прямоугольнике.
Практическое использование заливок 529 void SemiFillRectCHOC hDC. int left, int top, int right, int bottom, COLORREF color) { int nSave = SaveDC(hDC); const unsigned short ChessBoard[] = { OxAA, 0x55, OxAA. 0x55. OxAA, 0x55, OxAA, 0x55 }; HBITMAP hBitmap = CreateBitmap(8. 8, 1, 1, ChessBoard); HBRUSH hBrush = CreatePatternBrush(hBitmap); DeleteObject(hBitmap); HGDIOBJ hOldBrush = SelectObject(hDC, hBrush); HGDIOBJ hOldPen = SelectObject(hDC, GetStockObject(NULL_PEN)); SetROP2(hDC, R2_MASKPEN); SetBkColor(hDC, RGB(0xFF, OxFF, OxFF)); // Без изменений цвета SetTextColor(hDC, RGB(0, 0, 0)); // Черный цвет Rectangle(hDC. left, top, right, bottom); SetROP2(hDC. R2_MERGEPEN); SetBkColor(hDC, RGB(0x0. 0x0. 0x0)); // Без изменений цвета SetTextColor(hDC, color); // Заданный цвет RectangleChDC. left, top, right, bottom); SelectObjectChDC. hOldBrush); SelectObjectChDC, hOldPen); DeleteObject(hBrush); RestoreDC(hDC, nSave); } Функция Semi Fill Rect создает узорную кисть с шахматным узором. При первом вызове функции Rectangle используется растровая операция R2MASKPEN, в результате чего основные пикселы окрашиваются в черный цвет (0), а фоновые пикселы остаются без изменений. При втором вызове функции Rectangl e операция R2_MERGEPEN окрашивает основные пикселы в заданный цвет, а фоновые пикселы по-прежнему остаются неизмененными. Тернарные растровые операции (см. следующую главу) позволяют обойтись всего одним вызовом функции при использовании шахматной кисти. Реализация градиентных заливок в цветовом пространстве HLS В Windows 98 и Windows 2000 на уровне GDI реализована неплохая поддержка градиентных заливок, и все же без проблем не обошлось. По имеющейся информации градиентные заливки в Windows 98 приводят к утечке ресурсов, поэтому часто пользоваться ими не рекомендуется. В Windows NT 4.0 и Windows 95 градиентные заливки на уровне GDI не поддерживаются, если не считать линейной интерполяции в пространстве RGB. По этим причинам в приложениях иногда возникает необходимость в самостоятельной реализации градиентных заливок. Заливки треугольников лучше выполняются при помощи операций с растрами, но градиентные заливки прямоугольников легко имитируются закраской проме-
530 Глава 9. Замкнутые области жуточных полос с постепенным изменением цвета. Функция HLSGradientRectangle демонстрирует создание градиентных заливок в цветовом пространстве HLS. Функция GradientFill GDI в ней не используется. void HLSGradientRectangle(HDC hDC, int xO, int yO. int xl. int yl, COLORREF crefO, COLORREF crefl. int nPart) { KColor cO(crefO); cO.ToHLSO: KColor cl(crefl): cl.ToHLSO: for (int i=0; i<nPart; i++) { KColor c; c.hue = ( cO.hue * (nPart-1-i) + cl.hue * i ) / (nPart-1); c.lightness = ( cO.lightness * (nPart-1-i) + cl.lightness * i ) / (nPart-1); c.saturation = ( cO.saturation* (nPart-1-i) + cl.saturation* i ) / (nPart-1): c.ToRGBO: HBRUSH hBrush = CreateSolidBrush(c.GetColorRefO); RECT rect = { xO+i*(xl-xO)/nPart, yO, xO+(i+l)*(xl-xO)/nPart, yl }; FillRect(hDC, & rect. hBrush); DeleteObject(hBrush); } } Преобразование цветов между пространствами RGB и HLS выполняется классом KColor. Интерполяция происходит в пространстве HLS, а не в пространстве RGB. Прямоугольник делится на заданное количество полос, закрашиваемых одноцветной кистью. Цветовое пространство HLS позволяет легко регулировать яркость цвета без изменения оттенка и насыщенности или же изменять оттенок при постоянной яркости и насыщенности. В цветовом пространстве RGB у этих операций не находится столь простого и естественного представления. Радиальные градиентные заливки В радиальных градиентных заливках цвет изменяется в зависимости от расстояния от заданной точки (как правило, от центра круга). Радиальные заливки часто применяются для имитации бликов на объемных сферических поверхностях. Хотя в GDI радиальные градиентные заливки не поддерживаются, они легко реализуются делением круга на треугольники и постепенным изменением цвета от центра к периметру многоугольника. Пример создания радиальных градиентных заливок приведен ниже. BOOL RadialGradientFilKHDC hDC, int xO. int yO. int xl. int yl, int r, COLORREF cO. COLORREF cl, int nPart) { const double PI2 « 3.1415927 * 2; TRIVERTEX * pVertex = new TRIVERTEX[nPart+l]: ULONG * pMesh = new UL0NG[(nPart+l)*3];
Практическое использование заливок 531 pVertex[0].x = xl; pVertex[0].y = yl; pVertex[0].Red = R16(c0) pVertex[0].Green = G16(c0) pVertex[0].Blue = G16(c0) pVertex[0].Alpha = 0; for (int i=0; i<nPart; i++) { pVertex[i+l].x = xO + (int) (r * cos(PI2 * i / nPart)): pVertex[i+l].y = yO + (int) (r * sin(PI2 * i / nPart)); pVertex[i+l].Red = R16(cl): pVertex[i+l].Green = G16(cl): pVertex[i+l].Blue = B16(cl): pVertex[i+l].Alpha - 0; pMesh[i*3+0] = 0; pMesh[i*3+l] - i+1: pMesh[i*3+2] = (i+1) % nPart+1: BOOL rslt - GradientFill(hDC. pVertex. nPart+1. pMesh. nPart. GRADIENT FILL TRIANGLE); delete [] pVertex; delete [] pMesh; return rslt; } Функция аппроксимирует круг многоугольником и делит его на несколько треугольников, закрашиваемых функцией GradientFill. Настоящий центр круга находится в точке (хО,уО), а точка (х1,г/1) является общей вершиной всех градиентных треугольников и имитирует различные углы зрения. На рис. 9.21 показаны примеры разбиения круга на 8, 16 и 256 треугольников. Рис. 9.21. Радиальные градиентные заливки Приведенная функция подойдет для рисования одной-двух кнопок с искривленной поверхностью, но она определенно не соответствует высокому качеству трехмерной графики, которое может быть получено средствами Direct3D или OpenGL. На периметре многоугольника используются одинаковые цвета, да и количество треугольников, вероятно, следовало бы увеличить.
532 Глава 9. Замкнутые области Текстурные и растровые заливки Текстурной заливкой (texture fill) называется заполнение области растром, изображающим текстуру конкретного материала — скажем, бумаги, мрамора, гранита, песка или дерева. Для реализации текстурных заливок можно было бы воспользоваться узорными кистями GDI, но при этом возникает пара проблем. Во-первых, в системах, не входящих в семейство NT, узорные кисти ограничиваются размерами 8x8 пикселов. Во-вторых, в GDI узоры определяются в координатах устройства и не масштабируются в соответствии с разрешением устройства. Растры 8x8 годятся разве что для очень мелких текстур, отображаемых на экране. Текстурный растр, который хорошо смотрится на экране с разрешением 96 dpi, будет практически неразличим на принтере с разрешением 1200 dpi. Например, текстура, имитирующая деревянную поверхность, сильно зависит от разрешения устройства. Под растровой заливкой (bitmap fill) понимается растяжение растра по размерам заполняемой области. Текстурные и растровые заливки связаны с растрами, подробно описанными в следующей главе, поэтому мы оставляем эту тему на будущее. Узорные заливки GDI предоставляет в распоряжение программиста несколько стандартных штриховых узоров для закраски замкнутых фигур двумя цветами. Штриховые кисти первоначально ориентировались на экранный вывод. Хотя в NT интерфейс DDI позволяет драйверам принтеров предоставлять собственные масштабированные растры для реализации штриховых кистей, эта возможность используется лишь немногими драйверами принтеров. Если приложение задействует штриховые кисти для вывода на экран, штриховой узор не масштабируется в режимах с разрешением 72, 96 или 120 dpi. При изменении масштаба изображения узор остается прежним. Если приложение задействует штриховые кисти при печати, разглядеть полученный узор удастся разве что под микроскопом. Узорные заливки довольно просто имитируются последовательностью линий, причем это дает возможность задавать переменную толщину пера и координаты в логическом координатном пространстве. Простая функция, приведенная ниже, с использованием линий строит наклонный узор в виде «кирпичной кладки». void BrickPatternFill(HDC hDC. int xO. int yO. int xl. int yl. int width, int height) { width = abs(width); height = abs(height); if ( xOxl ) { int t = xO: xO = xl; xl = t; } if ( yOyl ) { int t = yO; yO = yl; yl = t; } for (int y=yO; y<yl: у += height ) for (int x=xO; x<xl: x += width ) { MoveToEx(hDC, x. y, NULL); LineTo(hDC. x+width, y+height); MoveToEx(hDC, x+width, y, NULL);
Итоги 533 LineTo(hDC, x+width/2. y+height/2); } } Функция использует только логические координаты, поэтому построенный узор легко преобразуется. В приведенном примере не поддерживается отсечение по определяющему прямоугольнику, непрозрачная закраска фона и выравнивание базовой точки кисти. Пример вывода иллюстрирует рис. 9.22. Рис. 9.22. Смешение при рисовании однородной кистью на устройствах, использующих палитру Итоги В этой главе рассматриваются средства GDI, предназначенные для заполнения замкнутых областей — кисти, заливки, регионы и модные градиентные заливки. В отличие от перьев, обладающих собственными геометрическими размерами, кисть определяет только способ размещения цветового шаблона внутри замкнутой области. Мы достаточно подробно изучили ситуации, при которых ограниченные возможности кистей GDI не соответствуют требованиям современных приложений, разобрались в проблемах несовместимости между операционными системами и рассмотрели некоторые обходные пути, решения и общие рекомендации. Кисть, создаваемая средствами GDI, представляет собой логическую спецификацию реальной кисти, которая задействуется драйвером устройства при рисовании и обычно является аппаратно-зависимой. При первом использовании новой логической кисти графический механизм обращается к драйверу устройства с требованием реализовать логическую кисть, то есть создать физическую структуру данных на основании логической кисти. После этого реализованный объект кисти передается всем функциям драйвера, использующим кисть. Дополнительная информация о внутренней структуре данных кисти приведена при описании других объектов GDI в главе 3 (раздел «WinDbg и расширение отладчика GDI»). В GDI предусмотрено довольно много функций для рисования простых геометрических фигур (прямоугольников, закругленных прямоугольников, эллипсов и многоугольников). Контуры таких фигур обводятся пером, а внутренняя часть закрашивается кистью. Более сложные фигуры строятся с использова-
534 Глава 9. Замкнутые области нием траекторий, объединяющих разные типы кривых. На уровне DDI практически все контуры преобразуются в траектории, а большая часть вызовов заполнения замкнутых областей обрабатывается внутренней реализацией функции StrokeAndFi 11 Path. Даже многоугольники и совокупности многоугольников представляют собой траектории, состоящие только из прямых линий. Единственным исключением являются прямоугольники в совместимом графическом режиме; для них используется более простая точка входа DDL GDI относится к числу базовых интерфейсов графического программирования и поэтому не поддерживает достаточно полный набор геометрических операций. При усложнении фигур точное вычисление их контуров становится трудной, а то и вовсе невыполнимой задачей. Простейший выход заключается в использовании регионов. При помощи операций из теории множеств можно создавать новые регионы как комбинации существующих регионов и применять их для графического вывода или отсечения. С другой стороны, использование регионов требует значительных затрат памяти и процессорного времени, а при большом увеличении региона ухудшается качество изображения. В GDI существует пара специальных функций для получения данных внутреннего представления регионов и применения к ним преобразований. Это открывает немало интересных возможностей — например, применение преобразований перспективы к данным региона. В новых реализациях Win32 GDI средства API для закраски плоских фигур вышли в третье, цветовое измерение — появилась поддержка градиентных заливок. Градиентные заливки часто применяются для имитации бликов на различных поверхностях. Вероятно, в будущем они будут все чаще встречаться в приложениях. Итак, к настоящему времени мы познакомились с функциями рисования отдельных пикселов и линий/кривых, а также закраски замкнутых областей. Начиная со следующей главы, мы займемся изучением различных растров, поддерживаемых в GDI, и их постоянно расширяющейся областью применения. Пример программы К этой главе прилагается всего один пример программы Areas (табл. 9.3). Эта программа иллюстрирует все темы, рассмотренные в настоящей главе, и строит все рисунки, приведенные в тексте. Таблица 9.3. Программа главы 9 Каталог проекта Описание Samples\Chapt_09\Area Меню Test содержит больше десятка команд, иллюстрирующих смешение цветов, применение штриховых и узорных кистей, кистей системных цветов, рисования прямоугольников, эллипсов, секторов, сегментов, закругленных прямоугольников, многоугольников, наборов многоугольников, регионов и траекторий, а также градиентных заливок
Глава 10 Основные сведения о растрах Как было показано в трех последних главах, пикселы, линии и замкнутые области могут использоваться для построения финансовых диаграмм, инженерных чертежей, геометрических узоров и т. д. Геометрические объекты, входящие в изображение, описываются точными математическими формулами. Эта область программирования компьютерной графики обычно называется векторной графикой (vector graphics). В другой, не менее важной области компьютерной графики используются оцифрованные изображения, полученные из окружающего мира. Эта область называется растровой графикой (bitmap graphics). Растровое изображение представляет собой прямоугольный массив элементов (пикселов), каждый из которых имеет определенный цвет. Растровые изображения часто являются результатом обработки информации, введенной со сканера, цифрового фотоаппарата или видеокамеры. Растры — слишком обширная тема, поэтому в книге этот материал разделен на три главы. Эта глава посвящена форматам растровых изображений и их отображению на графическом устройстве. Мы рассмотрим три основных растровых формата, поддерживаемых в GDI, — DIB (Device-Independent Bitmap), DIB-сек- ции и DDB (Device-Dependent Bitmap). В следующих главах будут рассматриваться практические применения — прозрачный вывод растров, альфа-наложение на фоновый рисунок, постепенная «проявка» и исчезновение, повороты растров и т. д. Аппаратно-независимые растры При вводе оцифрованной графики с устройств данные изображения необходимо преобразовать в формат, подходящий для хранения на жестком диске компьютера или другом носителе, а также для передачи на удаленное устройство. В наши дни с форматами графических изображений возникает немало хлопот. Разные операционные системы, фирмы-разработчики аппаратуры и даже при-
536 Глава 10. Основные сведения о растрах ложения работают с графикой, хранящейся в разных форматах. К числу наиболее распространенных растровых форматов относятся следующие: О JPEG (разработчик —Joint Photographic Experts Group) — 24-разрядный цветной формат со сжатием и потерей данных; О TIFF (разработчик — Aldus ) — очень гибкий графический формат с поддержкой различных субформатов, порядка байтов MAC и PC, а также сжатия LZW; О GIF (разработчик — CompuServe) — графический формат для небольших изображений, содержащих не более 256 цветов, с поддержкой пошагового вывода и прозрачности; О PNG (www.cdrom.com/pub/png/), Portable Network Graphics — графический формат с поддержкой многочисленных экзотических возможностей; на сегодняшний день отличается от остальных форматов применением незапатентованных алгоритмов и свободным распространением исходных текстов. Традиционным графическим форматом операционной системы Microsoft Windows является формат BMP. По сравнению с другими графическими форматами это очень простой формат, который проектировался в основном для упрощения графического программирования в приложениях. Поддержка цветовой глубины в формате BMP достаточно универсальна, от 1-, 2-, 4- и 8-разрядных индексированных изображений до 16-, 24- и 32-разрядного цвета в модели RGB. Изображения в формате BMP обычно занимают очень много места, поскольку этот формат поддерживает лишь простейшую форму сжатия по алгоритму RLE в 4- и 8-разрядных индексированных форматах. Например, 24-разрядное изображение размером 1024 х 768 пикселов в формате BMP занимает 2,55 Мбайт, а в формате JPEG оно обычно сжимается примерно до 200 Кбайт. Сохранять такие большие изображения на диске или передавать их через Интернет не рекомендуется. Файловый формат BMP Растры в формате BMP обычно называются аппаратно-независимыми (Device- Independent Bitmap, DIB). Определение «аппаратно-независимый» означает, что формат содержит полную информацию об изображении и позволяет воспроизвести его на разных устройствах. Первоначально этот термин не означал, что растр кодируется в аппаратно-независимом цветовом пространстве, хотя новые версии операционных систем Microsoft включают в формат BMP данные цветовых профилей, чтобы компенсировать зависимость от цветовых устройств. Ап- паратно-независимым растрам противопоставляется другой формат изображений, используемых во внутренней работе графической системы Windows — аппарат - но-зависимые растры (Device-Dependent Bitmaps, DDB). В этом разделе мы сначала рассмотрим DIB как фундаментальный графический формат Windows, а затем перейдем к DDB и другому растровому формату — DIB-секциям. Аппаратно-независимый растр, или DIB, хранящийся в файле на диске, состоит из трех основных компонентов: заголовка растрового файла, блока описания растра и массива пикселов. Блок описания растра может дополнительно делиться на заголовок, массив масок и цветовую таблицу (в зависимости от цветовой глубины растра). На рис. 10.1 показана структура изображения формата DIB в дисковом файле.
Аппаратно-независимые растры 537 Заголовок растрового файла 1 (BITMAPFILEHEADER) 1 Bitmap Information Заголовок блока 1 с информацией о растре 1 (BITMAPCOLORHEADER, 1 BITMAPINFOHEADER, 1 BITMAPV4HEADER, 1 orBITMAPV5HEADER) 1 Битовая маска 1 (DWORD Ц) 1 Цветовая таблица 1 (RGBTRIPLE[ ], RGBQUAD [ ]) I Массив пикселов 1 (Pixel [][]) Рис. 10.1. Файловый формат BMP Заголовок растрового файла Заголовок растрового файла содержит простую информацию, по которой приложения идентифицируют BMP-файлы. Он состоит из трех основных компонентов: сигнатуры BMP-файла, поля длины файла и смещения массива пикселов. Заголовок определяется в структуре BITMAPFILEHEADER. typedef struct tagBITMAPFILEHEADER { WORD bfType; // Сигнатура BMP-файла DWORD bfSize; // Общий размер файла DWORD bfReservedl; // 0 DWORD bfReserved2; // 0 DWORD bfOffBits; // Смещение массива пикселов от начала файла } BITMAPFILEHEADER; Сигнатура bfType в BMP-файлах состоит из двух ASCII-символов, «В» и «М», поэтому файл всегда начинается со значения 0x4D42, или "М" * 256 + "В". В поле bfSize хранится общий размер графического файла (это удобно при загрузке файлов с удаленного компьютера). В последнем поле структуры хранится смещение массива пикселов от начала графического файла. Следует помнить о том, что структура BITMAPFILEHEADER изначально проектировалась для 16-разрядных версий Windows, поэтому она выравнивается по границе слов, а не двойных слов. Общий размер структуры равен 14 байтам, в результате чего заголовок блока описания растра тоже не выравнивается по границе двойного слова. Это обстоятельство может вызвать проблемы при попытке сохранения DIB-секции в растровом файле, отображаемом на память (memory- mapped).
538 Глава 10. Основные сведения о растрах Заголовок описания растра Заголовок растрового файла всего лишь сообщает приложениям, что файл содержит данные в формате BMP, а подробное описание хранится в следующем за ним информационном блоке, который также начинается с заголовка. Если заголовок растрового файла пережил несколько поколений операционных систем Windows без малейших изменений, заголовок блока описания с информацией о растре неоднократно изменялся в прошлом и продолжает изменяться. В нем содержатся сведения о формате растрового изображения, его размерах, схеме сжатия, размере цветовой таблицы, цветовых профилях и т. д. В настоящее время существует четыре разных версии этого заголовка. Самая простая из них — структура BITMAPCOREHEADER, первоначально спроектированная для операционной системы OS/2. typedef struct tagBITMAPCOREHEADER { DWORD bcSize; // sizeof(BITMAPCOREHEADER) WORD bcWidth; // ширина растра в пикселах WORD bcHeight: // высота растра в пикселах + ориентация WORD bcPlanes; // количество плоскостей, должно быть равно 1 WORD bcBitCount; // количество бит на пиксел } BITMAPCOREHEADER; Чаще всего в BMP-файлах используется заголовок в формате структуры BITMAPINFOHEADER, значительно расширенный по сравнению с OS/2-версией. typedef struct tagBITMAPCOREHEADER { DWORD bcSize; // sizeof(BITMAPCOREHEADER) WORD bcWidth; // ширина растра в пикселах WORD bcHeight; // высота растра в пикселах + ориентация WORD bcPlanes; // количество плоскостей, должно быть равно 1 WORD bcBitCount; // количество бит на пиксел DWORD biCompression: // алгоритм сжатия DWORD biSizelmage; // размер массива пикселов LONG biXPelsPerMeter; // горизонтальное разрешение LONG biYPelsPerMeter; // вертикальное разрешение DWORD biClrUsed; // общий размер цветовой таблицы DWORD bi CIrImportant; // количество цветов, необходимых для вывода } BITMAPCOREHEADER; Структура BITMAPINFOHEADER обычно называется «версией 3» описания растра. Все аспекты Win32 API, появившиеся во времена Windows 3.1, обычно относятся к «версии 3»; новые возможности, добавленные в Windows 95 и Windows NT, именуются «версией 4», а новые возможности Windows 98 и Windows 2000 относятся к «версии 5». В Windows 95 и Windows NT 4.0 появилась новая структура BITMAPV4HEADER, а в Windows 98 и Windows 2000 была добавлена структура BITMAPV5HEADER. Начало этих новых структур в точности совпадает с BITMAPINFOHEADER (если не считать того, что поле размера содержит соответственно sizeof(BITMAPV4HEADER) или sizeof(BITMAPV5HEADER)). В структуре версии 4 появились новые поля для цветовых масок RGBA, цветовых пространств, конечных точек и гамма-коррекции, что предназначалось для поддержки ICM 1.0. R структуре версии 5 добавились новые типы цветовых пространств, рекомендации по воспроизведению и данные
Аппаратно-независимые растры 539 цветового профиля, ориентированные на поддержку ICM 2.0. Подробные описания этих структур приведены в MSDN. По иронии судьбы тот факт, что графический компонент Win98 генерирует BMP-файлы с заголовком V5, считается недостатком, а не достоинством, поскольку даже Visual Basic не читает новые BMP-файлы. Однако хорошо написанное приложение должно по крайней мере учитывать возможность получения заголовков BMP-файлов в четырех разных форматах, даже если оно при этом игнорирует новые поля V4 и V5 и интерпретирует заголовок как структуру BITMAPINFOHEADER. В структурах заголовка первое поле определяет размер структуры и является единственным признаком, по которому можно определить, какая версия заголовка используется. Если поле равно sizeof (BITMAPCOREHEADER), приложение должно работать с DIB в формате OS/2. Поле размера также определяет смещение, по которому находится цветовая таблица DIB. В двух следующих полях хранится ширина и высота DIB в пикселах. Обратите внимание: в DIB формата OS/2 эти значения хранятся в виде 16-разрядных слов (WORD), тогда как в новых версиях используется 32-разрядный тип LONG. Высота DIB обычно является положительной величиной, но она может быть и отрицательной. Знак определяет порядок следования строк развертки в массиве пикселов. Положительная высота DIB соответствует обратному порядку строк (снизу вверх), при котором первый пиксел массива является первым пикселом последней строки развертки изображения; такие DIB-растры называются перевернутыми (bottom-up). Отрицательная высота DIB соответствует более привычному прямому порядку следования строк развертки (сверху вниз). В большинстве BMP-файлов используется обратный порядок следования строк развертки. Поля bcPlanes и bcBitCount определяют формат строк развертки массива пикселов. В двухцветных изображениях, частным случаем которых являются черно- белые изображения, для представления пиксела достаточно одного бита. В 256- цветных изображениях пиксел представляется 8 битами. В разных графических устройствах может использоваться разная структура строк развертки (с одной или несколькими цветовыми плоскостями), но формат DIB поддерживает изображения лишь с одной плоскостью, поэтому поле bcPlanes должно быть равно 1. Поле bcBitCount полностью определяет размер каждого пиксела и количество цветов, представляемых одним пикселом. Допустимые значения этого поля перечислены в табл. 10.1. Таблица 10.1. Допустимые значения поля bcBitCount в формате DIB Значение Максимальное количество Размер пик- Описание цветов села, байт 0 Зависит от внедренного изображения 1 2 (21) 1/8 Поддерживается только в Windows 98/2000; используется для внедрения изображений в формате JPEG или PNG Монохромное изображение Продолжение ^>
540 Глава 10. Основные сведения о растрах Таблица 10.1. Продолжение Значение Максимальное количество цветов Размер пик- Описание села, байт 4 8 16 24 32 4 (23) 1/4 16 (24) 1/2 256 (28) 1 32 768 (215) или 65 536 (216) 2 166 777 216 (224) 3 166 777 216 (224) 4 4-цветное изображение, используемое в WinCE 16-цветное изображение 256-цветное изображение High Color True Color True Color Если количество бит на пиксел меньше либо равно 8, после заголовка в ВМР- файле следует цветовая таблица. Используемая в OS/2 структура BITMAPCOREHEADER заканчивается полем bcBitCount, а в других структурах присутствуют дополнительные поля. Базовый формат DIB следует интерпретировать в приложении таким образом, чтобы отсутствующим полям присваивались значения по умолчанию. В поле biCompression хранится информация об алгоритме сжатия, применяемом к массиву пикселов. Допустимые значения перечислены в табл. 10.2. Таблица 10.2. Алгоритмы сжатия DIB Значение Описание BIRGB Несжатое изображение BIRLE8 Изображение с кодировкой 8 бит/пиксел, сжатое с использованием алгоритма RLE. Только для перевернутых DIB-растров BIRLE4 Изображение с кодировкой 4 бит/пиксел, сжатое с использованием алгоритма RLE. Только для перевернутых DIB-растров BIBITFIELDS Несжатые изображения с кодировкой 16 и 32 бит/пиксел. В изображение включаются три битовые маски, определяющие способ хранения компонентов RGB BIJPEG Массив пикселов содержит внедренное изображение в формате JPEG. Поддерживается только в Windows 98/2000 BIPNG Массив пикселов содержит внедренное изображение в формате PNG. Поддерживается только в Windows 98/2000 Чаще всего поле biCompression равно BIRGB (сжатие отсутствует). Visual Studio и графические редакторы от Microsoft генерируют BMP-файлы только в несжатом формате. При отсутствии сжатия каждая строка развертки DIB представляет собой упакованный массив пикселов. Пиксел с 1-битной кодировкой занимает 1/8 байта, пиксел с 2-битной кодировкой занимает 1/4 байта, а пикселы
Аппаратно-независимые растры 541 с 4-битной кодировкой занимают 1/2 байта. В этих трех случаях один байт может содержать данные нескольких пикселов, от старшего бита к младшему. Для изображений с большим количеством бит/пиксел каждый пиксел занимает biBitCount/8 байт. Строки развертки в аппаратно-независимых растрах всегда выравниваются по ближайшей границе двойного слова (при необходимости строка дополняется нулями). В DIB с кодировкой 4 и 8 бит/пиксел для уменьшения размеров растра может применяться необязательное сжатие но алгоритму RLE (Run-Length Encoding). Для 8-битных изображений этот алгоритм ищет последовательность смежных байтов с одинаковым значением и заменяет их двумя байтами: счетчиком повторений и кодом повторяющегося байта. Предусмотрены особые служебные последовательности для неповторяющихся байтов, конца строки и конца изображения. Алгоритм RLE обеспечивает наилучший результат в том случае, если каждая строка развертки состоит из одинаковых пикселов. В худшем случае результат занимает больше места, чем исходное несжатое изображение. Ниже приведено описание формата сжатого изображения с применением метода BIRLE8: <изображение> :: = <серия_пикселов> { <серия_пикселов> } <серия_пикселов> ::= <кодированные_данные> | <непосредственные_данные<@062> <кодированные_данные> ::= <конец_строки> | <конец_изображения> | <дельта> | <повторение> <конец_строки> : := О О <конец_изображения> ::= 0 1 <дельта> ::= 0 2 dx dy <непосредственные__данные> ::= 0 счетчик { байт } <повторение> ::= счетчик_повторений повторяемый_байт Сжатое изображение в формате RLE кодируется в виде последовательности серий пикселов, существующих в пяти формах. Если первый байт серии пикселов отличен от нуля, он является счетчиком повторений (от 1 до 255) и указывает, сколько раз повторяется следующий байт. Если первый байт равен нулю, следующий байт либо должен быть равен 0 (конец строки), 1 (конец изображения), 2 (дельта) или 3-255 (несжатые пикселы). Дельта-серия смещает текущую позицию в декодировашюм изображении с заданными смещениями по осям х и у, что позволяет быстро преодолеть несколько строк развертки. Признаки конца строки pi конца изображения также позволяют преждевременно обрывать строки развертки или изображения, однако значения пропущенных пикселов считаются неопределенными. Таким образом, на практике дельта-серии практически не встречаются; признаки конца строки и конца изображершя обычно размещаются за последним пикселом строки или всего изображенрря. Другими словами, каждая строка развертки обычно кодируется отдельно от остальных без пропуска пикселов, что позволяет 1рзбежать неопределенных значений пикселов. Непосредствершые серии описывают последовательности неповторяющихся пикселов в изображении. Серия начинается с 0 и счетчржа, принимающего значенрш из интервала 3-255, поскольку значершя 0-2 зарезервированы для признаков конца строки, корща ркзображения pi дельта-серий. За счетчиком следует точное колршество байт. Каждая непосредственная серрш должна состоять из четного количества байт, поэтому счетчрш должен быть четным. Это гарантирует, что каждая серрш пикселов в закодированном изображении всегда вырав-
542 Глава 10. Основные сведения о растрах нивается по границе слова, что заметно упрощает процессы кодирования/восстановления информации и повышает их эффективность. Если в изображении в кодировке RLE пикселы не пропускаются, его синтаксис представляется в следующем упрощенном виде: <изображение> :: = <строка_развертки> { <строка_развертки> } [ <конец_изображения<@062> ] <строка_развертки> ::= <серия_пикселов> { <серия_пикселов> } [ <конец_строки> ] <серия_пикселов> ::= <счетчик_повторений> | <непосредственная_серия> Четырехбитные изображения в кодировке BIRLE4 имеют такую же базовую структуру, как и изображения в формате BIRLE8. Однако в этом случае каждый пиксел представляется только 4 битами. Счетчик непосредственных данных содержит количество пикселов, поэтому следующие за ним данные должны состоять из значений (счетчик+1)/2 байт. Счетчик повторений тоже содержит количество пикселов; следующий за ним байт содержит два пиксела, которые используются попеременно для заполнения заданного количества пикселов. Для 16- и 32-разрядных DIB-растров поле biCompression должно быть равно BIBITFIELDS. Это значение не соответствует реально существующему методу сжатия; оно всего лишь позволяет задать размер и порядок следования красной, зеленой и синей составляющих в пикселе. Если поле bi Compression содержит BIFIELDS, после заголовка блока описания растра и перед цветовой таблицей добавляются три двойных слова — маски для извлечения красной, зеленой и синей составляющих из 16- или 32-разрядных упакованных пикселов. На первый взгляд кажется, что маски обладают чрезвычайной гибкостью и позволяют создавать всевозможные экзотические форматы DIB, но в действительности они должны подчиняться целому ряду ограничений. В системах семейства NT маски должны состоять из смежных пикселов и не должны перекрываться. Впрочем, вы все же можете создать DIB с нестандартным порядком каналов RGB. В системах, не входящих в семейство NT, поддерживается всего три разновидности масок. Для 16-разрядных изображений поддерживаются только форматы RGB 5-5-5 и 5-6-5. В формате 5-5-5 синяя маска равна OxlF, зеленая маска равна ОхЗЕО, а красная маска равна 0х7С00, и каждый канал занимает 5 бит. В формате 5-6-5 синяя маска равна OxlF, зеленая маска равна 0х7Е0, а красная маска равна 0xF800; из этих трех каналов только зеленый канал занимает 6 бит. Для 32-разрядных изображений поддерживается только формат 8-8-8, при этом синяя маска равна OxFF, зеленая маска равна OxFFOO, а красная маска равна OxFFOOOO. На самом деле битовые маски разрабатывались для решения проблем совместимости, связанных с различиями в реализациях режимов High Color. В «PC 99 System Design Guide» говорится, что видеоадаптер должен поддерживать 16-разрядный кадровый буфер в формате 5-5-5 или 5-6-5 или же оба формата одновременно. Если поддерживается только формат 5-5-5, видеоадаптер должен сообщать о нем как о 16-разрядном, а не 15-разрядном, поскольку в противном случае это может нарушить работу некоторых приложений. Для 32-разрядного кадрового буфера обязательна поддержка формата 8-8-8-8, где старшие 8 бит содержат данные альфа-канала. Как ни странно, альфа-канал не документируется для блока описания растра.
Аппаратно-независимые растры 543 При виде типов BIJPEG и BI_PNG создается впечатление, что GDI наконец-то пытается решить проблему больших несжатых DIB-растров в форматах High Color и True Color, однако предлагаемое решение — не более чем полумера. Эти два режима сжатия поддерживаются только в Windows 98 и Windows 2000 и с определенными ограничениями. GDI и базовый графический механизм не обеспечивают никакой поддержки декодирования изображений в форматах JPEG и PNG. Эта поддержка не обеспечивается и видеодрайверами; только драйверы принтеров по желанию могут реализовать ее. Чтобы проверить факт поддержки JPEG или PNG, приложение должно обратиться с запросом к контексту устройства принтера при помощи функции ExtEscape; только после получения положительного ответа приложение может передать драйверу устройства сжатый растр JPEG или PNG, «завернутый» в DIB. В этом случае GDI просто передает данные драйверу принтера. Это лишь отчасти решает проблему с огромными затратами памяти на хранение больших DIB-растров в форматах High Color или True Color. Тип BIJPEG рассматривается в главе 17. Давайте вернемся к структурам заголовка блока описания растра. За признаком сжатия следует поле biSizelmage, в котором хранится размер массива пикселов изображения. При использовании значения BIRGB поле biSizelmage может быть равно 0; GDI вычисляет размер изображения по ширине, высоте и количеству бит на пиксел. Но при сжатии RLE, JPEG или PNG это поле должно содержать фактический размер данных изображения. Два последних поля структуры ВITMAPINF0HEADER содержат информацию о цветовой таблице. В поле biClrllsed хранится количество элементов в цветовой таблице. Для DIB с количеством цветов, не превышающим 256, нулевое значение поля biClrUsed означает максимально возможное количество, то есть 2А(бит/пик- сел). Дисковый файл DIB должен содержать полную цветовую таблицу с максимальным количеством элементов. Неполная цветовая таблица может использоваться только в DIB-растрах, хранящихся в памяти. Поле biClrlmportant определяет количество элементов, реально необходимых для отображения растра. Как и прежде, нулевое значение означает, что значимыми являются все цвета в таблице. Каждый элемент цветовой таблицы обычно занимает 4 байта (кроме старого формата OS/2), что в общей сложности дает 1024 байта для DIB с кодировкой 8 бит/пиксел. Конечно, в мире Winl6 с 64-килобайтной кучей GDI такие затраты памяти приходилось оптимизировать. В программировании Win32 на 1024 байта никто не обращает внимания, если только ваша программа не работает с сотнями или тысячами изображений. Поле biClrlmportant всего лишь сообщает прикладной программе, сколько цветов реально используется в изображении. На основании этой информации программа может сгенерировать палитру в точности необходимого размера для вывода изображения в кадровом буфере. Для цветных DIB-растров в форматах High Color или True Color цветовая таблица не нужна, поскольку каждый пиксел содержит полную информацию обо всех цветовых составляющих RGB. С другой стороны, эти растры могут содержать ненулевые поля biClrUsed и biClrlmportant и цветовую таблицу. Цветовая таблица в изображениях High Color и True Color может использоваться для построения палитры для вывода DIB на устройствах с поддержкой палитры. Палитры рассматриваются в главе 13.
544 Глава 10. Основные сведения о растрах Битовые маски Если в 16- или 32-разрядном DIB-растре поле bi Compression равно BIBITFIELDS, за заголовком блока описания растра следуют битовые маски, хранящиеся в виде массива DWORD. Всегда используются три маски в традиционном порядке «красный — зеленый — синий». В GDI отсутствуют какие-либо структуры данных или функции API для работы с битовыми масками. Цветовая таблица В DIB-растрах, содержащих не более 256 цветов, каждый пиксел массива содержит индекс цветовой таблицы, по которой индексы преобразуются в значения RGB. DIB-растры в форматах True Color и High Color тоже могут содержать цветовые таблицы для построения логических палитр в системах, использующих палитру. Количество элементов в цветовой таблице задается в поле biClrUsed заголовка блока описания растра. Если это поле равно 0 (а также в DIB формата OS/2), предполагается максимальное количество элементов. Элементы цветовой таблицы делятся на три типа. В DIB формата OS/2 каждый элемент представляется структурой RGBTRIPLE, а в других форматах DIB — структурой RGBQUAD. Для DIB, хранящихся в памяти, каждый элемент может быть 16-разрядным словом, которое представляет собой индекс следующего уровня. И в RGBTRIPLE и в RGBQUAD цвет задается 8-разрядными значениями RGB. Как видно из приведенных ниже определений, эти структуры отличаются только наличием зарезервированного поля. typedef struct tagRGBTRIPLE { BYTE rgbtBlue; BYTE rgbtGreen; BYTE rgbtRed; } RGBTRIPLE; typedef struct tagRGBQUAD { BYTE rgbtBlue; BYTE rgbtGreen; BYTE rgbtRed; BYTE rgbtReserved; } RGBQUAD; GDI определяет две дополнительные структуры ВITMAPCOREINF0 и BITMAPINFO, в которых заголовок блока описания растра объединяется с цветовой таблицей. typedef struct tagCOREINFO { BITMAPCOREHEADER bmciHeader; RGBTRIPLE bmciColorsCl]; } BITMAPCOREINFO; typedef struct tagBITMAPINFO { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[l]; } BITMAPINFO; При использовании этих структур необходима осторожность, поскольку в обеих структурах резервируется место лишь для одного элемента цветовой таблицы.
Аппаратно-независимые растры 545 Следовательно, для хранения заголовка описания растра и цветовой таблицы приложение должно выделить дополнительную память за пределами этих структур. Поле bmiHeader структуры BITMAPINFO может относиться к типу BITMAPINFOHEADER, BITMAPV4HEADER или BITMAPV5HEADER, поэтому смещение поля bmi Colors не является фиксированной величиной. Даже если вы ограничиваетесь структурой BITMAPINFOHEADER, место для битовых масок в 16- и 32-разрядных DIB-растрах не резервируется. При поиске цветовой таблицы растра приложение не должно полагаться на содержимое структуры BITMAPINFO. Вместо этого смещение цветовой таблицы следует вычислять во время работы программы на основании данных из заголовка блока описания растра. Массив пикселов Пикселы изображения хранятся в массиве пикселов. Обычно изображение представляет собой последовательность строк развертки, дополненных до ближайшей 32-разрядной границы. По умолчанию строки развертки хранятся в обратном порядке, если только поле biHeight заголовка описания не является отрицательной величиной. Обратный порядок следования строк развертки означает, что первый пиксел массива в действительности соответствует первому пикселу последней строки развертки при выводе на экран в режиме ММ_ТЕХТ. Внутри строк развертки пикселы упаковываются для экономии места. Строки дополняются битами до границы двойного слова. Количество байт на строку развертки, одна из важных характеристик DIB, вычисляется следующей функцией: int inline Scan"! ineSize( int width, int bitcount) { return (width * bitcount + 31)/32; } Для DIB с режимом сжатия BI_RGB обращение к отдельным пикселам массива является простой операцией, которая реализуется достаточно эффективно. Прямой доступ к пикселам играет важную роль при реализации графических алгоритмов и при усовершенствовании средств вывода растров, поддерживаемых в GDI. Кроме того, эта методика чрезвычайно важна в программировании DirectDraw, где графическая поверхность фактически представляет собой DIB. Подробности будут рассмотрены ниже. Упакованный аппаратно-независимый растр Файловый формат BMP предназначен для хранения DIB-растров в виде файлов на диске. Как упоминалось выше, BMP-файл состоит из заголовка файла, блока описания растра и массива пикселов. Заголовок файла содержит информацию, используемую при загрузке DIB в память. Но после того, как файл окажется в памяти, необходимость в заголовке отпадает. Если требуется, заголовок файла можно восстановить по заголовку блока описания растра. DIB без заголовка файла называется упакованным (packed) DIB-растром. Термин «упакованный» в данном случае не имеет никакого отношения к упаковке пикселов в строке развертки. Он лишь указывает на то, что остальные компоненты DIB следуют друг за другом в смежных блоках памяти.
546 Глава 10. Основные сведения о растрах Упакованный DIB-растр начинается с заголовка блока описания растра, за которым следуют массив масок, цветовая таблица и массив пикселов. В качестве указателя на упакованный DIB-растр в Win32 API обычно используется указатель на структуру BITMAPINFO. Хотя эта структура не содержит ссылок на массивы масок и пикселов, из нее по крайней мере можно узнать о наличии цветовой таблицы. Достаточно большое количество функций API получает и возвращает упакованные DIB-растры. Если DIB входит в исполняемый файл в виде ресурса, для получения указателя на упакованный DIB-растр можно воспользоваться функциями FindResource, LoadResource и LockResource. Функция CreateDIBPatternBrushPt использует упакованный DIB-растр для создания узорной кисти. Кроме того, упакованные DIB-растры используются и в работе буфера обмена Windows. В упакованном DIB-растре, хранящемся в памяти, цветовая таблица может содержать индексы логической палитры. Например, если параметр i Usage функции CreateDIBPatternBrushPt равен DIBPALC0L0RS, цветовая таблица является массивом индексов. Однако такой DIB-растр не следует передавать другим приложениям или записывать на диск, если только другая сторона не осведомлена об этом факте. В файловом формате DIB не существует флага, который бы сообщал, что цветовая таблица содержит индексы неизвестной палитры. Разделенный аппаратно-независимый растр Содержимое упакованного DIB-растра можно разделить на две части: массив пикселов и информация о формате растра (заголовок блока описания растра, маски и цветовая таблица). Хранить эти две части вместе неудобно. Например, графический редактор для поддержки многоуровневой отмены операций может хранить несколько промежуточных DIB-растров; все они содержат абсолютно одинаковую форматную информацию и отличаются только массивом пикселов. Встречаются и другие ситуации — например, приложение может получать изображение в другом формате (PCX, TIFF или GIF), создавать структуру BITMAPINFO в памяти, строить массив пикселов по восстановленным данными и затем пытаться передавать эти два блока в виде DIB. Многие графические функции GDI обладают достаточной гибкостью и не требуют обязательной передачи упакованного DIB-растра. Вместо этого функции передаются два параметра — указатель на структуру BITMAPINFO и указатель на массив пикселов. Подобное решение обеспечивает необходимую гибкость при построении DIB в памяти. Иногда разделенный аппаратно-независимый растр (неупакованный DIB- растр) для экономии места содержит неполную цветовую таблицу. Например, при использовании DIB с 64 оттенками серого цвета можно выделить память для 64 элементов цветовой таблицы вместо 256. Класс для работы с DIB Хотя BMP считается одним из самых простых графических форматов, приведенное в предыдущем разделе описание не выглядит простым. Сложность
Класс для работы с DIB 547 BMP-файлов обусловлена постоянным развитием формата для поддержки новых возможностей и постоянными поисками компромисса между быстродействием и затратами памяти. Графическое программирование аппаратно-независимых растров — задача не из простых, а поддержка операций с растрами в GDI API весьма ограничена. Например, в GDI не существует функций, которые бы возвращали количество цветов в цветовой таблице упакованного DIB-растра или указатель на массив пикселов в упакованном DIB-растре. Программистам приходится самостоятельно писать код для анализа различных версий структур и получения нужной информации. На уровне GDI отсутствует поддержка и более сложных задач — например, вычисления адреса пиксела с координатами (х,у) в массиве пикселов или преобразования растра в оттенки серого. Операции с DIB хорошо инкапсулируются в классе C++, но даже библиотека Microsoft Foundation Classes не содержит специализированного класса для работы с DIB. В этом разделе мы начнем построение нетривиального класса, предназначенного для этого. Ниже перечислены основные цели проектирования. О Загрузка и отображение DIB во всех допустимых форматах DIB. Входные изображения могут поступать из разных источников, поэтому класс должен обеспечивать загрузку и отображение во всех форматах. О Эффективность работы с различными данными DIB. Большая часть информации DIB хранится в заголовке блока описания растра, который существует в четырех разных версиях с разными значениями по умолчанию. Хорошо спроектированный класс для работы с DIB должен как можно быстрее возвращать нужную информацию без многочисленных проверок. О Прямой доступ к пикселам в несжатом массиве пикселов — ключ к реализации многих графических алгоритмов. Короче говоря, мы хотим создать класс DIB, который загружает и отображает DIB во всех возможных форматах, а также обеспечивает эффективную работу с несжатыми изображениями в формате True Color. Объявление класса KDIB приведено в листинге 10.1. Листинг 10.1. Объявление класса KDIB typedef enum i DIB 1BPP. DIB 2BPP, DIB 4BPP, DIBJBPPRLE, DIB 8BPP. DIBJBPPRLE. DIBJ6RGB555. DIB 16RGB565. DIB 24RGB888, DIB 32RGB888. // // // // // // // // // // // // // // 2-цветное изображение с палитрой 4-цветное изображение с палитрой 16-цветное изображение с палитрой 16-цветное изображение с палитрой, сжатие RLE 256-цветное изображение с палитрой 2-цветное изображение с палитрой, сжатие RLE 15-разрядное цветное изображение RGB. 5-5-5, 1 бит не используется 16-разрядное цветное изображение RGB, 5-6-5 24-разрядное цветное изображение RGB. 8-8-8 32-разрядное цветное изображение RGB, 8-8-8. 8 бит не используются Продолжение &
548 Глава 10. Основные сведения о растрах Листинг 10.1. Продолжение DIB_32RGBA8888. // 32-разрядное цветное изображение RGBA. DIB 16RGBbitfields. DIB 32RGBbitfields. DIB_JPEG. DIB_PNG DIBFormat: // 16-разрядное цветное изображение RGB. // нестандартные маски, только в NT // 32-разрядное цветное изображение RGB. // нестандартные маски, только в NT // внедренное изображение в формате JPEG // внедренное изображение в формате PNG typedef enum { DIB_BMI_NEEDFREE = 1. DIB_BMI_READONLY = 2. DIB_BITS_NEEDFREE = 4. DIB BITS READONLY = 8 class KDIB { public: DIBFormat int BITMAPINFO BYTE m_nImageFormat; m_Flags; m_pBMI; m_pBits: // формат массива пикселов // DIB_BMI_NEEDFREE ' // BITMAPINFOHEADER + маска // цветовая таблица // массив пикселов RGBTRIPLE * m_pRGBTRIPLE; // цветовая таблица DIB OS/2 в m_pBMI RGBQUAD * m_pRGBQUAD: // цветовая таблица DIB V3.4.5 в m_pBMI int mjiClrUsed; // количество цветов в таблице int mjnClrlmpt: // количество используемых цветов DWORD * m_pBitFields; // маски для 16 и 32 бит/пиксел // в m_pBMI int m_nWidth; // ширина изображения в пикселах int mjnHeight; // высота изображения в пикселах // (положительная) int mjiPlanes: // количество плоскостей int m_nBitCount; // бит на плоскость int mjnColorDepth; // цветовая глубина int m_nImageSize; // размер массива пикселов int m nBPS; BYTE * int KDIBO; virtual m_pOrigin m_nDelta; -KDIBO: // Заранее вычисляемые значения: // размер строки развертки в байтах // (на каждую плоскость) // указатель на логическое начало растра // смещение следующей строки развертки // конструктор по умолчанию. // пустое изображение // виртуальный деструктор создает
Класс для работы с DIB 549 BOOL Creatednt width, int height, int bitcount); bool AttachDIB(BITMAPINFO * pDIB, BYTE * pBits. int flags); bool LoadFile(const TCHAR * pFileName); bool LoadBitmap(HMODULE hModlue. LPCTSTR pBitmapName); void ReleaseDIB(void): // освобождение памяти int GetWidth(void) const { return mjiWidth; } int GetHeight(void) const { return mjiHeight; } int GetDepth(void) const { return mjiColorDepth; } BITMAPINFO * GetBMI(void) const { return m_pBMI; } BYTE * GetBits(void) const { return m_pBits; } int GetBPS(void) const { return m_nBPS; } bool IsCompressed(void) const { return (m_nImageFormat == DIB_4BPPRLE) || (m_nImageFormat == DIBJBPPRLE) 11 (mjnlmageFormat == DIB_JPEG) || (mjnlmageFormat == DIB_PNG); } }: Первые четыре переменные класса KDIB содержат важнейшие сведения о растрах, по которым можно получить значения всех остальных переменных. DIB-растры существуют в разных растровых форматах, зависящих от количества бит/пиксел, признака сжатия и даже битовых масок. Наличие одного значения, однозначно определяющего растровый формат, заметно упростит реализацию графических алгоритмов. По этой причине мы и определяем перечисляемый тип DIBFormat. Из определения ясно видно, что формат BMP поддерживает 15 разновидностей растровых форматов. В Windows NT/2000 DDK используется аналогичный подход к определению растровых форматов. Например, функции EngCreateBitmap (создание поверхности, находящейся под управлением GDI) передаются такие константы, как BMF4RLE, BMF8BPP и BMF32BPP. Однако помимо структуры ВITMAPIN F0HEADER, класс KDIB содержит множество других полей. Экономия десятка-другого байтов в данном случае несущественна; быстродействие гораздо важнее. Экземпляры класса KDIB будут находиться в памяти компьютера, поэтому они представляют DIB-растр, хранящийся в памяти, а не на диске; следовательно, структура BITMAPFILEHEADER не понадобится. Переменная mpBMI содержит указатель на блок описания растра. В переменной mpBits хранится указатель на массив пикселов растра. Раздельное хранение указателей на блок описания растра и массив пикселов позволяет классу KDIB поддерживать как упакованные, так и неупакованные растры. Растры поступают из разных источников — это загрузка из файла или ресурса, вставка из буфера и даже построение на программном уровне. Класс растра должен знать, доступны ли эти два указателя только для чтения, или же данные, на которые они ссылаются, должны удаляться из памяти при удалении экземпляра класса KDIB. Для этого в класс была включена переменная m_Flags, значение которой представляет собой комбинацию четырех флагов: О DIBBMINEEDFREE — указатель m_pBMI ссылается на блок памяти, выделенный из кучи, который должен освобождаться в деструкторе;
550 Глава 10. Основные сведения о растрах О DIBBMIREADONLY — указатель mpBMI ссылается на данные, доступные только для чтения; О DIBBITSNEEDFREE — указатель mpBits ссылается на блок памяти, выделенный из кучи, который должен освобождаться в деструкторе; О DIB_BITS_READONLY — указатель mpBits ссылается на данные, доступные только для чтения. Во второй группе из пяти переменных хранятся указатели на цветовые таблицы в формате RGBTRIPLE или RGBQUAD, общее количество цветов и количество значащих цветов. Мы не поддерживаем цветовую таблицу с индексами палитры, которые не являются частью формата DIB. Только один из указателей mpRGBTRIPLE и m_pRGBQUAD может быть отличен от NULL. Переменная mpBitsFields указывает на битовые маски (если они используются). Первая часть класса KDIB содержит прямые указатели на заголовок блока описания растра, цветовую таблицу и битовые маски. Третья группа переменных класса подробно описывает формат изображения. Здесь хранится ширина и высота изображения (всегда положительная), количество плоскостей, количество бит/пиксел, цветовая глубина и размер изображения. Обратите внимание: в заголовке высота растра может быть отрицательной величиной, если растр хранится в памяти не в перевернутом виде. Из-за отрицательной высоты возникают проблемы во многих графических алгоритмах, поэтому в классе KDIB высота нормализуется, а ориентация растра в памяти отражается в одной из переменных следующей группы. Четвертая группа переменных содержит часто используемые значения, вычисленные на основании упоминавшихся выше переменных. Переменная n_nBPS содержит количество байт в строке развертки, дополненной до ближайшей границы DWORD. Переменная m_pOrigin указывает на логическое начало растра, то есть на пиксел (0,0), соответствующий первому байту массива пикселов для прямого (не перевернутого) растра. Переменная m_nDelta содержит смещение между строками развертки. Для растров с прямым порядком строк развертки переменная mjiDelta всегда положительна, в противном случае она отрицательна. По этим трем переменным всегда можно быстро вычислить адрес строки развертки, по которому легко найти адрес отдельного пиксела. Независимая переменная mjiDelta также может использоваться для хранения шага (pitch) поверхности DirectDraw, который может и не совпадать с m_nBPS. Класс KDIB содержит простой конструктор по умолчанию и виртуальный деструктор. Метод ReleaseDIB отвечает за освобождение ресурсов, выделенных для представления текущего изображения. Кроме того, мы определяем несколько функций, возвращающих геометрические характеристики изображения в виде констант. Метод AttachDIB выполняет основную работу по инициализации класса KDIB на основании данных упакованного или неупакованного DIB-растра. Метод LoadBitmap загружает ресурс BMP из модуля Win32 и инициализирует объект растра, доступного только для чтения, вызовом функции AttachDIB. Метод Load- File загружает BMP-файл с диска и инициализирует экземпляр KDIB вызовом AttachDIB. Эти три метода приведены в листинге 10.2.
Класс для работы с DIB 551 Листинг 10.2. Инициализация класса KDIB по BMP-файлу или ресурсу bool KDIB::AttachDIB(BITMAPINFO * pDIB. BYTE * pBits. int flags) { if ( IsBadReadPtr(pDIB. sizeof(BITMAPCOREHEADER)) ) return false; ReleaseDIBO; m_pBMI = pDIB; m_Flags = flags: DWORD size = * (DWORD *) pDIB; // Размер size всегда равен DWORD int compression; // Сбор информации из структур заголовка блока описания растра switch ( size ) { case sizeof(BITMAPCOREHEADER): { BITMAPCOREHEADER * pHeader - (BITMAPCOREHEADER *) pDIB; mjiWidth = pHeader->bcWidth; mjiHeight = pHeader->bcHeight; m_nPlanes = pHeader->bcPlanes; m_nBitCount - pHeader->bcBitCount; m_nImageSize= 0; compression - BI_RGB; if ( m_nBitCount <- 8 ) { mjiClrUsed - 1 « mjiBitCount; m_nClrImpt = m_nClrUsed; m_pRGBTRIPLE - (RGBTRIPLE *) ((BYTE *) m_pBMI + size); m_pBits - (BYTE *) & m_pRGBTRIPLE[m_nClrUsed]; } else m_pBits - (BYTE *) m_pBMI + size; break; } case sizeof(BITMAPINFOHEADER): case sizeof(BITMAPV4HEADER): case sizeof(BITMAPV5HEADER): { BITMAPINFOHEADER * pHeader = & m_pBMI->bmiHeader; m_nWidth = pHeader->biWidth; m_nHeight = pHeader->biHeight; m_nPlanes = pHeader->biPlanes; m_nBitCount - pHeader->biBitCount; m_nImageSize= pHeader->biSizeImage; compression = pHeader->biCompression; Продолжение &
552 Глава 10. Основные сведения о растрах Листинг 10.2. Продолжение m_nClrUsed = pHeader->biClrUsed; mjiClrlmpt = pHeader->bi CIrImportant: if ( m_nBitCount<=8 ) if ( mjiClrUsed==0 ) /7 0- полная цветовая таблица mjiClrUsed = 1 « mjiBitCount; if ( m_nClrUsed ) // Имеется цветовая таблица { if ( m_nClrImpt==0 ) // 0 - все цвета являются значимыми m_nClrlmpt = m_nClrUsed; if ( compression==BI_BITFIELDS ) { m_pBitFields = (DWORD *) ((BYTE *)pDIB+size); m_pRGBQUAD - (RGBQUAD *) ((BYTE *)pDIB+size + 3*sizeof(DWORD)); } else m_pRGBQUAD - (RGBQUAD *) ((BYTE *)pDIB+size); m_pBits = (BYTE *) & m_pRGBQUAD[m_nClrUsed]: } else { if ( compression==BI_BITFIELDS ) { m_pBitFields = (DWORD *) ((BYTE *)pDIB+size); m_pBits = (BYTE *) m_pBMI + size + 3 * sizeof(DWORD); } else m_pBits = (BYTE *) m_pBMI + size; } break; } default: return false; } if ( pBits ) m_pBits = pBits; // Вычисление основных параметров DIB mjiColorDepth = mjiPlanes * m_nBitCount; m_nBPS = (mjiWidth * m_nBitCount + 31) / 32 * 4; if (mjiHeight < 0 ) // Прямой порядок строк развертки { mjiHeight = - mjiHeight; // Перейти к положительной величине mjiDelta = m_nBPS; // Смещение вперед m_pOrigin = m_pBits; // scanO .. scanN-1 } else
Класс для работы с DIB 553 { mjiDelta = - mjiBPS: // Смещение назад m_pOrigin = m_pBits + (m_nHeight-l) * m_nBPS * mjiPlanes; // scanN-1..scanO } if ( mjiImageSize==0 ) mjiImageSize = m_nBPS * m_nPlanes * mjiHeight: // Определить формат изображения по режиму сжатия switch ( mjiBitCount ) { case 0: if ( compression==BI_JPEG ) mjiImageFormat = DIB_JPEG; else if ( compression==BI_PNG ) m_nImageFormat = DIB_PNG; else return false; case 1: mjiImageFormat = DIB_1BPP; break; case 2: mjiImageFormat - DIB_2BPP; break; case 4: if ( compression==BI_RLE4 ) mjiImageFormat = DIB_4BPPRLE; else mjiImageFormat = DIB_4BPP; break; case 8: if ( compression==BI_RLE8 ) mjiImageFormat = DIBJBPPRLE: else m_nImageFormat = DIB_8BPP: break; case 16: if ( compression==BI_BITFIELDS ) mjiImageFormat - DIB_16RGBbitfields: else m_nImageFormat = DIB_16RGB555; // См. ниже break; case 24: mjiImageFormat = DIB_24RGB888; break; case 32: if ( compression — BI_BITFIELDS ) Продолжение^
554 Глава 10. Основные сведения о растрах Листинг 10.2. Продолжение mjiImageFormat else mjiImageFormat break; default: return false; } // Разобраться с битовыми полями if ( compression==BI_BITFIELDS ) { DWORD red - m_pBitFields[0]; DWORD green = m_pBitFields[l]; DWORD blue = m_pBitFields[2]: if ( (blue—OxOOlF) && (green==0x03E0) && (red==0x7C00) ) mjiImageFormat = DIB_16RGB555; else if ( (blue==0x001F) && (green==0x07E0) && (red==0xF800) ) mjiImageFormat - DIB_16RGB565; else if ( (blue==OxOOFF) && (green==OxFFOO) && (red==OxFFOOOO) ) mjiImageFormat - DIB_32RGB888; } return true: bool KDIB::LoadBitmap(HMODULE hModule. LPCTSTR pBitmapName) { HRSRC hRes - FindResource(hModule. pBitmapName. RTJITMAP); if ( hRes—NULL ) return false; HGLOBAL hGlb - LoadResource(hModule. hRes); if ( hGlb—NULL ) return false; BITMAPINFO * pDIB - (BITMAPINFO *) LockResource(hGlb): if ( pDIB—NULL ) return false; return AttachDIBCpDIB. NULL. DIB_BMIREADONLY | DIB_BITS_READONLY); bool KDIB::LoadFile(const TCHAR * pFileName) { if ( pFileName—NULL ) return false; HANDLE handle - CreateFile(pFileName. GENERIC_READ. FILE_SHARE_READ. NULL. OPEN EXISTING. FILE ATTRIBUTEJORMAL. NULL): - DIB_32RGBbitfields; = DIB 32RGB888; // См. ниже
Класс для работы с DIB 555 if ( handle == INVALID_HANDLE_VALUE ) return false; BITMAPFILEHEADER bmFH; DWORD dwRead = 0: ReadFile(handle, & bmFH. sizeof(bmFH). & dwRead. NULL); if ( (bmFH.bfType == 0x4D42) && (bmFH.bfSize<=GetFileSize(handle. NULL)) ) { BITMAPINFO * pDIB = (BITMAPINFO *) new BYTE[bmFH.bfSize]; if ( pDIB ) { ReadFile(handle. pDIB. bmFH.bfSize. & dwRead, NULL); CloseHandle(handle); return AttachDIBCpDIB, NULL. DIB_BMI_NEEDFREE); } } CloseHandle(handle); return false; } Метод LoadBitmap получает манипулятор модуля и имя ресурса растра. Он ищет ресурс функцией FindResource, получает манипулятор ресурса функцией LoadResource и вызывает функцию LockResource для получения указателя на упакованный DIB-растр. Учтите, что в среде Win32 последовательность вызовов FindResource, LoadResource и LockResource не приводит к фактическому выделению ресурсов, за исключением того, что соответствующие страницы загружаются с диска в память. Значение, возвращаемое функцией LockResource, представляет собой указатель на образ модуля, содержащего ресурс. Следовательно, данные, на которые ссылается этот указатель, доступны только для чтения и освобождать их после использования необязательно. Функция LoadBitmap вызывает AttachDIB для инициализации экземпляра класса KDIB данными упакованного растра, доступного только для чтения. При этом функции AttachDIB передаются флаги DIB_ BMI_READONLY | DIB_BITS | READONLY, а удаление экземпляра класса KDIB не требует освобождения памяти в куче. Функция LoadFile открывает файл, читает структуру BITMAPFILEHEADER и проверяет сигнатуру BMP-файла с размером файла. Если проверка прошла успешно, функция выделяет блок памяти для упакованного DIB-растра и загружает в него оставшуюся часть файла. Указатель на блок передается AttachDIB для дополнительной проверки и инициализации переменных класса KDIB. Функция AttachDIB вызывается с флагом DIBJ3MI_NEDFREE, поэтому выделенный блок будет освобожден в деструкторе. Функция AttachDIB использует всю информацию, упоминавшуюся при описании формата DIB в предыдущем разделе, для инициализации десятка переменных класса. Сначала мы выбираем нужную версию заголовка блока описания растра из четырех возможных, а затем декодируем формат изображения.
556 Глава 10. Основные сведения о растрах В конце кода функции мы проверяем, соответствует ли изображение, находящееся в режиме сжатия BIBITFIELD, трем группам битовых масок, поддерживаемых в Windows 95/98. Функция работает как с упакованными, так и неупакованными DIB-растрами. Для неупакованных DIB-растров функции AttachDIB передаются два указателя. Несмотря на всю простоту, этот класс позволит нам загрузить любой ВМР- файл и начать эксперименты с растровыми изображениями. Отображение DIB в контексте устройства В Win32 GDI предусмотрено две функции для вывода аппаратно-независимого растра в контексте устройства: int StretchDIBitsCHDC hdc. int xDest, int yDest, int nDestWidth. int nDestHeight, in XSrc, int YSrc, int nSrcWidth, int nSrcHeight. CONST VOID * lpBits. CONST BITMAPINFO * IpBitsInfo, UINT iUsage, DWORD dwRop); int SetDIBitsToDevice(HDC hdc, int xDest. int yDest. DWORD dwWidth. DWORD dwHeight, in XSrc. int YSrc. UINT nStartScan, UINT cScanLines. CONST VOID * IpvBits. CONST BITMAPINFO * Ipbmi. UINT fuColorUse); StretchDIBits Функция StretchDIBits занимает в GDI исключительно важное место, поэтому мы начнем именно с нее. В первом параметре, конечно, передается манипулятор контекста устройства. Параметры XDest, YDest, nDestWidth и nDestHeight определяют (в логических координатах) прямоугольник, который будет выводиться на поверхности устройства. Затем следует еще одна четверка XSrc, YSrc, nSrcWidth и nSrcHeight, определяющая прямоугольный участок DIB-растра. Указатель lpBits ссылается на массив пикселов DIB, а указатель IpBitsInfo — на одну из версий структуры BITMAPINFO. В совокупности они определяют DIB-растр, упакованный или неупакованный. Параметр iUsage обычно равен DIBRGBC0L0RS (для цветовой таблицы RGB) или DIBPALC0L0RS (для цветовой таблицы, содержащей индексы логической палитры). Последний параметр содержит признак растровой операции, который заслуживает особого разговора. Пока мы будем использовать простейшую растровую операцию SRCC0PY. Функция StretchDIBits выделяет участок изображения DIB, выполняет отсечение, масштабирует выделенный участок по размерам приемного прямоугольника, преобразует к цветовому формату приемной поверхности и записывает полученные данные в приемную поверхность (при использовании растровой операции SRCC0PY). С концептуальной точки зрения процесс состоит из шести этапов: выбор источника, преобразование, отсечение, масштабирование, преобразование цветового формата и растровая операция. Исходный прямоугольник Первый этап (выбор источника) достаточно прост — источник определяется соответствующей четверкой параметров. Если вы хотите отобразить весь растр,
Отображение DIB в контексте устройства 557 передайте значения [0, 0, ширина изображения, высота изображения]. Помните, что для растров с прямым порядком строк развертки поле biHeight структуры BITMAPINFOHEADER отрицательно; используйте абсолютную величину (модуль). В GDI исходный прямоугольник определяется четверкой [XSrc,YSrc,XSrc+nSrcWidth, YSrc+nSrcHeight] с исключением правой и нижней сторон. Если приложение передает при вызове StretchDIBits значения [ширина изображения, высота изображения, -ширина изображения, -высота изображения], используется прямоугольник [ширина изображения, высота изображения, 0, 0]. Чем же этот прямоугольник отличается от прямоугольника [0, 0, ширина изображения, высота изображения]? GDI интерпретирует его как отображение системы координат и выводит весь растр, но с зеркальным отражением по вертикали и горизонтали. Используя параметры исходного прямоугольника, можно выделить фрагмент изображения. Если какая-либо часть заданного прямоугольника выходит за границы растра, она считается отсеченной. Некоторые комбинации параметров исходного прямоугольника приведены в табл. 10.3. Таблица 10.3. Параметры XSrc, YSrc, nSrcWidth и nSrcHeight функции StretchDIBits Значения Исходный прямоугольник 0, 0, ширина, высота Все изображение, исходная ориентация ширина, 0, -ширина, высота Все изображение, зеркальное отражение по горизонтали 0, высота, ширина, -высота Все изображение, зеркальное отражение по вертикали ширина, высота, -ширина, -высота Все изображение, зеркальное отражение по горизонтали и вертикали 0, 0, ширина, 1 Первая строка развертки 0, 0, 1, высота Первый столбец пикселов Обратите внимание: параметры исходного изображения интерпретируются как логические координаты, а не физические. Вертикальная координата 0 означает первую логическую строку развертки изображения, а не первую физическую строку. В растрах с прямым порядком строк развертки первая логическая строка развертки соответствует первой физической строке, но при обратном порядке строк первая логическая строка соответствует последней физической строке развертки в массиве пикселов. Приемный прямоугольник и режимы масштабирования Приемник можно определить аналогичным образом — отображением [XDst, YDst, Xdst+nDestWidth, YDst+nDestHeight] из логических координат в физические с исключением правой и нижней сторон. Если прямоугольник не нормализован,
558 Глава 10. Основные сведения о растрах выполняется зеркальное отражение. Таким образом, окончательная ориентация изображения зависит как от исходного, так и от приемного прямоугольников. Выбранный фрагмент исходного растра масштабируется по размерам приемного прямоугольника. Возможны три принципиально различающихся случая: сохранение исходного масштаба, увеличение или уменьшение. При сохранении масштаба все просто — один пиксел исходного изображения соответствует ровно одному пикселу приемной поверхности, не больше и не меньше. При увеличении один исходный пиксел может соответствовать нескольким приемным пикселам, причем масштабный коэффициент может быть дробным. При уменьшении несколько исходных пикселов преобразуются в один пиксел приемника (масштабный коэффициент тоже может быть дробным). Существует много способов масштабирования, причем ни одному из них нельзя отдать однозначного предпочтения. Способ масштабирования определяется одним из атрибутов контекста устройства — режимом масштабирования (stretch mode). Управление режимом масштабирования в GDI осуществляется двумя функциями: int SetStretchBltModeCHDC hDC. int iStretchMode); int GetStretchBltMode(HDC hDC); Функция SetStretchBltMode присваивает значение атрибуту режима масштабирования в контексте устройства, а функция GetStretchBUMode читает его. Допустимые значения перечислены в табл. 10.4. Таблица 10.4. Режимы масштабирования Режим масштабирования Описание (прежнее название) STRETCH_ANDSCANS (BLACK0NWHITE) Пикселы комбинируются поразрядной логической операцией И. В режиме RGB сохраняются черные пикселы STRETCH_ORSCANS (WHITE0NBLACK) Пикселы комбинируются поразрядной логической операцией ИЛИ. В режиме RGB сохраняются черные пикселы STRETCH_DELETESCAN (C0L0R0NC0L0R) Сохраняется один пиксел, остальные удаляются STRETCH_HALFTONE (HALFTONE) Вычислить средний цвет по нескольким пикселам. Поддерживается только в системах семейства NT. После установки этого режима следует выровнять базовую точку кисти функцией SetBrushOrgEx Режим масштабирования учитывается только при уменьшении, то есть при выводе большого исходного изображения в маленьком приемном прямоугольнике. Увеличение реализуется простым повторением пикселов. Если вы не хотите, чтобы в увеличенном растре возникали «зазубрины» на контурах, реализуйте собственный алгоритм масштабирования. Режимы STRETCHANDSCANS и STRETCH_ORSCANS предназначены для черно-белых изображений. Если вы хотите, чтобы тонкие черные линии на белом фоне не исчезали и не прерывались в результате масштабирования, воспользуйтесь режимом STRETCH_ANDSCANS, поскольку
Отображение DIB в контексте устройства 559 логическая операция И отдает предпочтение черному цвету (0) перед белым (1). Если вы хотите сохранить белые линии на черном фоне, используй- те операцию STRETCH_ORSCANS, поскольку она отдает предпочтение белому цвету. В цветных изображениях эти два режима приводят к искажению цветов, поэтому в этом случае используют следующие два режима. При выборе режима STRETCH_ DELETESCAN лишние данные попросту игнорируются. Этот способ работает быстро, но не всегда дает хорошие результаты, поскольку при уменьшении изображения лишние пикселы просто отбрасываются, что приводит к потере информации. В режиме STRETCH_HALFTONE вычисляется средний цвет группы пикселов, что улучшает восприятие уменьшенных объектов человеческим глазом. С другой стороны, этот режим работает гораздо медленнее. Другая проблема заключается в том, что режим STRETCH_HALFTONE реализован только в системах семейства NT. Преобразование цветового формата Цветное изображение может выводиться на черно-белом принтере, а черно-белое изображение может отображаться на цветном экране. В таких случаях GDI приходится преобразовывать пикселы из формата исходного изображения в формат приемного контекста устройства. Если форматы источника и приемника совпадают, преобразование касается только формата хранения данных. Изображения DIB всегда являются цветными. Даже двуцветный растр должен содержать цветовую таблицу с двумя элементами. Цветовая таблица преобразует индексы пикселов в цветовые значения. Если палитра не используется, пикселы могут содержать непосредственные значения RGB. При выводе на устройство с поддержкой палитры в отображении значений RGB на индексы палитры участвуют две палитры: логическая и системная. Операции с палитрой рассматриваются в главе 13. Чтобы вывести DIB с палитрой на RGB-устройстве, индексы, хранящиеся в растре, приходится преобразовывать в значения RGB по цветовой таблице растра. Задача вывода цветного изображения на монохромном устройстве не имеет однозначного решения. В режиме STRETCHHALFTONE функция StretchDIBits осуществляет полутоновое преобразование цветных изображений в черно-белый формат; в других режимах StretchDIBits подбирает ближайшие цвета. Для сравнения стоит заметить, что это поведение отличается от отображения аппаратно- зависимого растра в черно-белом контексте устройства. Растровая операция Итак, после преобразования цветового формата мы имеем преобразованный массив пикселов, готовый к записи на приемную поверхность. По аналогии с бинарными растровыми операциями, определяющими способ объединения цвета пера/кисти с цветом приемника, в GDI предусмотрены растровые операции, определяющие окончательное значение приемного пиксела. Растровые операции подробно рассматриваются в следующей главе. А пока мы ограничимся простейшей растровой операцией SRCCOPY, которая сводится к простому копированию пиксела исходного растра на приемную поверхность.
560 Глава 10. Основные сведения о растрах Пример использования функции StretchDIBits Ниже приведен небольшой пример, демонстрирующий применение функции StretchDIBits. Для начала необходимо добавить в класс KDIB функцию для отображения DIB. int DrawDIBCHDC hDC. int dx, int dy. int dw, int dh, int sx, int sy, int sw, int sh, DWORD гор) { if ( m_pBMI ) return ::StretchDIBits(hDC. dx, dy, dw, dh, sx, sy, sw. sh, m_pBits. m_pBMI. DIB_RGB_COLORS, гор); else return GDIJRROR; } После добавления этой функции мы можем воспользоваться классом KDIB для загрузки и отображения DIB. Код следующего фрагмента загружает растровый рисунок со львом и отображает его в четырех разных ориентациях. KDIB lion; if ( lion.LoadFile(_T("1ion.bmp")) ) { int w - DIB.GetWidthO; int h = DIB.GetHeightO: DIB.DrawDIB(hDC, 5, 5. w, h, 0, 0. w, h, SRCCOPY) DIB.DrawDIB(hDC. 10+w. 5, w. h. 0. 0. -w. h, SRCCOPY) DIB.DrawDIB(hDC, 5. 10+h. w. h. 0, 0, w. -h. SRCCOPY) DIB.DrawDIB(hDC. 10+w. 10+h. w. h. 0. 0. -w. -h. SRCCOPY) } Рис. 10.2. Зеркальное отражение рисунка с использованием функции StretchDIBits Этот пример приведен только для демонстрационных целей. Изображения в программе должны загружаться один раз, а не каждый раз, когда их потребуется
Отображение DIB в контексте устройства 561 вывести. Программа Bitmap, описываемая в этой главе, позволяет выбрать изображения DIB в диалоговом окне и отобразить их в дочерних окнах MDL С каждым дочерним окном связан экземпляр класса KDIB, который один раз загружает изображение и многократно отображает его. Одно из этих дочерних окон показано на рис. 10.2. SetDIBitsToDevice На фоне других функций GDI API функция SetDIBitsToDevice выглядит одиноко. Вероятно, компании Microsoft следовало бы исключить эту функцию из Win32 API. Если приложение импортирует ее, то, скорее всего, это делается косвенно через класс CDC MFC. Функция выводит DIB (полностью или частично) с сохранением исходной ориентации и масштаба, независимо от текущего мирового преобразования или режима отображения окна на область просмотра. Из всех параметров в логической системе координат задается лишь позиция приемника (xDest,yDest). Параметры XSrc, YSrc, dwWidth и dwHeight определяют часть DIB. He пытайтесь проделывать такие же фокусы со знаком параметров, как при вызове StretchDIBits — высота и ширина передаются в виде беззнаковых целых чисел. Передавать размеры приемного прямоугольника не нужно; в системе координат устройства они всегда соответствуют размерам источника. Но самое интересное в функции SetDIBitsToDevice — это способ передачи DIB- растра (или его части). В параметре lpbmi, как и прежде, передается указатель на заголовок блока описания растра. Параметр lpvBits указывает на буфер, содержащий несколько строк развертки или весь растр. Функция спроектирована таким образом, чтобы в lpvBits можно было передавать указатель на часть, а не на весь DIB-растр. Расположение данных в буфере определяется двумя дополнительными параметрами. Параметр uStartScan содержит последовательный номер строки развертки в изображении, на первую строку которого ссылается lpvBits, а параметр cScanLines содержит количество строк развертки в буфере. Функция SetDIBitsToDevice всегда копирует исходные пикселы в приемник, поэтому указывать растровую операцию не нужно. Последний параметр, fuColorUse, идентифицирует способ интерпретации цветовой таблицы. Функция SetDIBitsToDevice копирует cScanLines из буфера на приемную поверхность, начиная с (xDest, yDest+uStartScan), с учетом отсечения исходного прямоугольника и отсечения, действующего в приемном контексте устройства. Учтите, что координаты приемника должны задаваться в системе координат устройства. В следующем фрагменте показано, как при помощи функции SetDIBitsToDevice вывести все изображение, начиная с точки приемника (х,у). SetDIBitsToDevice(hDC. х, у, mjiWidth. abs(m_nHeight), О, О, О, abs(mjiHeight). m_pBits, (const BITMAPINFO *) m_pDIB, DIB_RGB_C0L0RS); Единственным преимуществом SetDIBitsToDevice перед StretchDIBits является снижение затрат памяти. Если приложение Win 16, работающее на портативном компьютере с 4 Мбайт памяти, должно вывести 1-мегабайтный растр в формате BMP, оно не сможет полностью загрузить его в память. При использовании функции SetDIBitsToDevice можно загрузить в память заголовок блока
562 Глава 10. Основные сведения о растрах описания растра и цветовую таблицу, получить все параметры, а затем в цикле читать из буфера строки развертки и вызывать SetDIBitsToDevice для каждой строки. В этом случае можно обойтись буфером, в котором помещается всего одна строка развертки, а при наличии свободной памяти можно одновременно читать несколько строк развертки. Функция StretchDIBits тоже позволяет реализовать принцип последовательной загрузки, но вам придется модифицировать поле высоты в заголовке блока описания растра в соответствии с количеством строк развертки в буфере, а также изменять параметры исходного и приемного прямоугольника при каждом вызове. В приложениях Win32 затраты памяти уже не столь критичны, как в приложениях Win 16. Если возникает необходимость вывести большой графический файл, приложение может воспользоваться файлами, отображаемыми на память (memory-mapped files). При этом графика отображается в виртуальную память, находящуюся под управлением диспетчера памяти операционной системы. В системах Windows 95/98 вывод больших изображений одним вызовом StretchDIBits нередко вызывает проблемы с быстродействием системы. Реализация GDI в этих системах фактически состоит из 16-разрядного кода. При каждом графическом выводе происходит переход от 32-разрядного GDI к 16-разрядному, при этом доступ к GDI со стороны других программных потоков блокируется, поскольку 16-разрядная реализация не является безопасной в отношении многопоточного доступа. GDI может потратить несколько секунд на обработку одной функции с огромным объемом данных, и на это время даже курсор мыши застывает в одном положении. Приложения, работающие в Windows 95/98, очень часто делят большие изображения на несколько меньших фрагментов. Функция SetDIBitsToDevice не масштабирует изображение. Следовательно, когда возникает необходимость в масштабировании, приложению приходится реа- лизовывать собственный алгоритм. Конечно, это существенный недостаток функции SetDIBitsToDevice. Если приложение работает в режиме отображения ММ_ТЕХТ, функция SetDIBitsToDevice может использоваться для преобразования фрагментов изображения в уменьшенном буфере. В следующем примере растр инвертируется во время отображения. if ( ! m_DIB.Compressed() ) { int bps - m_DIB.GetBPS(); BYTE * buffer = new BYTE[bps]; for (int i=0; i<m_DIB.GetHeight(); i++) { memcpy(buffer, m_DIB.GetBits() + bps*i. bps); for (int j=0; j<bps; j++) buffer[j] = - buffer[j]; SetDIBitsToDevice(hDC. 10, 10. m_DIB.GetWidth(). m_DIB.GetHeight(). 0. 0, i, 1. buffer. m_DIВ.GeBMI(), DIB_RGB_C0L0RS); } delete [] buffer; }
Совместимые контексты устройств 563 Для сжатых изображений этот код не работает, поскольку сжатие существенно затрудняет переход между строками развертки. Каждая строка развертки копируется в буфер, инвертируется и отображается на экране вызовом Set- DIBitsToDevice. Строки обрабатываются последовательно; в параметрах изменяется только значение uStartScan. Совместимые контексты устройств Контексты устройств, рассматривавшиеся до настоящего момента, всегда соответствовали реальному физическому устройству, которое обслуживалось специальным драйвером. Контекст устройства предоставляет в распоряжение программ, использующих GDI, абстрактное графическое устройство. В своей внутренней реализации GDI поддерживает для каждого контекста устройства таблицу функций драйвера графического устройства, вызываемых через интерфейс DDL Такое описание напоминает виртуальные функции C++ или методы СОМ; действительно, в работе этих механизмов имеется определенное сходство. Абстрактный подход позволяет GDI полностью имитировать графическое устройство в памяти — в том же смысле, в каком виртуальный диск имитирует жесткий диск. Для работы с графическими устройствами, имитируемыми в памяти, применяются совместимые контексты устройств (memory device context). По историческим причинам совместимые контексты устройств не являются полностью независимыми от физических графических устройств. В действительности совместимый контекст устройства всегда связывается с физическим графическим устройством. Выражаясь точнее, в GDI поддерживается всего одна функция для создания контекста устройства, совместимого с существующим контекстом: HDC CreateCompatibleDC(HDC hDC); Эталонный контекст устройства, передаваемый этой функции, должен поддерживать растровые операции, иначе совместимый контекст не принесет особой пользы. Для создания совместимых контекстов обычно используется экранный контекст, поскольку современные видеоадаптеры обеспечивают полную и правильную реализацию растровых операций. Напротив, контекст устройства принтера вряд ли является хорошим кандидатом для создания совместимого контекста устройства. Принтеры обладают различным уровнем поддержки цветов и растровых операций. Например, в драйвере принтера PostScript поддержка растровых операций обычно ограничена. GDI даже позволяет передавать манипулятор NULL; в этом случае создается контекст устройства, совместимый с текущим экраном. Работа совместимого контекста устройства основана на использовании растра. Все графические команды реализуются как вывод на растре, а не на физическом устройстве. Между этим растром и совместимым контекстом устройства не существует жесткой связи. Базовый растр является атрибутом контекста; его можно выбирать и исключать, как манипулятор объекта пера или кисти. Для получения этого атрибута можно вызвать функцию GetCurrentObject с типом OBJBITMAP. При создании совместимого контекста устройства GDI присваивает
564 Глава 10. Основные сведения о растрах этому атрибуту монохромный растр, состоящий из одного пиксела. По крайней мере, вы можете получить и задать цвет этого пиксела. Чтобы использовать совместимый контекст устройства, необходимо создать и выбрать в нем базовый растр. Аппаратно-независимые растры, описанные в предыдущем разделе, для этого не подходят. В качестве поверхности для совместимого контекста устройства GDI разрешает использовать только аппарат- но-зависимые растры и DIB-секции. Эти два типа растров рассматриваются в следующих двух разделах. Совместимые контексты устройств чрезвычайно важны для графического программирования Windows, но мы временно оставим эту тему и вернемся к ней после знакомства с аппаратно-зависимыми растрами и DIB-секциями. Аппаратно-зависимые растры Аппаратно-независимые растры (DIB) позволяют легко получать изображения из внешних источников, выполнять с ними программные операции, отображать или передавать графические данные другим приложениям или компьютерам. Основная проблема заключается в том, что GDI не поддерживает прямую запись в DIB. Для этой цели в GDI предусмотрен другой класс растров — аппаратно-зависимые растры. Аппаратно-зависимый растр (Device-Dependent Bitmap, DDB) представляет собой объект GDI, который находится под управлением GDI и драйверов устройств и обладает тем же статусом, что и объект логического пера, логической кисти или региона. При создании DDB-растра GDI и драйвер графического устройства определяют его внутренний формат данных и выделяют память из области памяти GDI. После этого все операции с DDB выполняются через манипулятор объекта GDI. Манипулятору аппаратно-зависимого растра в GDI присваивается тип HBITMAP. DDB-растры также часто называют «растровыми объектами GDI». В GDI предусмотрен богатый набор функций для работы с аппаратно-зависимыми растрами, поскольку они широко используются самой операционной системой. В частности, DDB-растры могут применяться в операциях с геометрическими перьями, узорными кистями, каретками, меню и стандартными элементами управления. Существует несколько способов создания растровых объектов GDI: HBITMAP CreateBitmap(int nWidth, int nHeight. UINT cPlanes, UINT cBitsPerPel. CONST VOID * IpvBits); HBITMAP CreateBitmapIndirect(CONST BITMAP * lpbm): HBITMAP CreateCompatibleBitmap(HDC hDC. int nWidth, int nHeight); HBITMAP CreateDiscardableBitmap(HDC hDC. int nWidth, int nHeight); HBITMAP CreateDIBitmap(HDC hdc. CONST BITMAPINFOHEADER * Ipbmih, DWORD fdwlnit. CONST VOID * Ipblnit. CONST BITMAPINFO * lpbmi. UINT fuUsage); HBITMAP LoadBitmapCHINSTANCE hlnstance, LPCTSTR IpBitmapName); Между DDB и DIB существует несколько принципиальных различий. По своей исходной архитектуре DDB зависит от устройства. Это означает, что любое гра-
Аппаратно-зависимые растры 565 фическое устройство может выбрать для представления DDB свой собственный внутренний растровый формат. Реальный формат DDB может изменяться при работе приложения на разных компьютерах и даже на одном компьютере в разных видеорежимах. Аппаратно-зависимый растр, как и DIB, содержит массив пикселов, но при передаче или чтении данных DDB строки развертки всегда следуют в прямом порядке (сверху вниз), поэтому отдельно обрабатывать отрицательную высоту для перевернутых растров не нужно. В отличие от DIB-раст- ров, всегда использующих строки развертки с одной цветовой плоскостью, DDB- растры могут использовать несколько цветовых плоскостей, чтобы обеспечить совместимость с конкретным графическим устройством для получения оптимального быстродействия. Массивы пикселов, передаваемые функциям создания DDB, должны выравниваться по 16-разрядной границе слов. DDB не содержит цветовой таблицы, поэтому реальный цвет каждого пиксела изображения зависит от устройства, на котором оно выводится. CreateBitmap DDB определяется шириной, высотой, количеством плоскостей, количеством бит на пиксел и массивом цветов (или индексов) пикселов. Эти пять характеристик передаются при вызове функции CreateBitmap. Функция создает растр nWidth* nHeight, с числом цветовых плоскостей, равным cPlanes, и кодировкой cBitsPerPel бит/пиксел. Параметр IpvBits содержит указатель на исходный массив пикселов; предполагается, что размер этого массива равен (nWidth*cBitsPerPel+15)/16*2* cP1anes*nWidth*nHeight. GDI выделяет блок памяти соответствующего размера и копирует в него данные инициализации. Если параметр IpvBits равен NULL, созданный растр не инициализируется. С точки зрения системы между DIB и DDB существуют значительные различия. Упакованный DIB-растр определяется одним указателем, а неупакованный — двумя указателями. Эти указатели относятся к адресному пространству пользовательского режима, базирующемуся на файле, отображаемом в память, либо на системном файле подкачки. Максимальный размер DIB ограничивается только объемом дискового пространства и 2-гигабайтным объемом адресного пространства пользовательского режима. В Windows 95/95 DDB-растры хранятся в 32-разрядной куче GDI, хотя реализация GDI в этих системах фактически полностью состоит из 16-разрядного кода. Максимальный размер DDB в этих системах равен 16 Мбайт. Размер строки развертки не может превышать 64 Кбайт. В системах семейства NT, начиная с Windows NT 4.0, память для DDB выделяется из выгружаемого пула, находящегося в адресном пространстве ядра. В выгружаемом пуле хранятся многие объекты GDI (в том числе регионы, контексты устройств, траектории) и другие объекты, с GDI не связанные. Максимальный размер DDB равен 48 Мбайт, тогда как объем всего выгружаемого пула не превышает 192 Мбайт. Кроме того, на DDB тратится еще один потенциально ограниченный системный ресурс — манипуляторы объектов GDI. Короче говоря, вместо ресурсов уровня процесса DDB поглощает общесистемные ресурсы, поэтому при создании больших DDB-растров (или большого количества DDB- растров), а также утечке ресурсов необходимо действовать очень осторожно.
566 Глава 10. Основные сведения о растрах У DDB существует всего один стандартный формат — одноплоскостной монохромный формат с кодировкой 1 бит/пиксел. В других форматах параметры nPlanes и cBitsPerPel всего лишь определяют минимальные требования к растру. Во внутренней работе современных графических устройств используется стандартный формат DIB с одной цветовой плоскостью. GDI поручает драйверу устройства выбор ближайшего доступного формата с кодировкой по крайней мере nPlanes*cBitsPerPel бит/пиксел. Например, на запрос формата с 3 плоскостями и 8 битами/пиксел предоставляется DDB с одной плоскостью 24 бит/пиксел, а на запрос с 3 плоскостями и 10 битами/пиксел — DDB с одной плоскостью и 32 бит/пиксел. Параметры nPlanes и cBitsPerPel также определяют интерпретацию исходного содержимого массива пикселов. В текущей реализации GDI, если произведение nPlanes*cBitsPerPel больше 32, попытка создания растра завершается неудачей. Следующий фрагмент показывает, как создать DDB-растр с кодировкой 1 бит/пиксел, инициализированный шахматным узором 4x4, и неинициализированный DDB-растр с кодировкой 24 бита/пиксел: const WORD Data88_lpp[] = { OxCC, OxCC. 0x33. 0x33, OxCC. OxCC. 0x33, 0x33 }; HBITMAP hBmplbpp - CreateBitmap(8, 8. 1, 1. Data88_lpp); HBITMAP hBmp24bpp = CreateBitmap(8. 8. 3, 8. NULL); CreateBitmapIndirect Функция CreateBitmapIndirect позволяет создать DDB через указатель на структуру BITMAP, которая определяется следующим образом: typedef struct tagBITMAP { LONG bmType; // Тип растра, должен быть равен 0 LONG bmWidth; LONG bmHeight; LONG bmWidthBytes; WORD bmPlanes; WORD bmBitsPixel; LPV0ID bmBits; } BITMAP; При сравнении полей структуры со списком параметров CreateBitmap обнаруживаются три основных различия. Хотя в структуре появилось новое поле bmType, оно должно быть равно 0. В поле bmWidthBytes хранится размер строки развертки массива пикселов в байтах. Как и в случае с функцией CreateBitmap, оно должно быть четным числом. Поле bmBits содержит указатель на массив пикселов, но этот указатель не определяется как константный. В документации Microsoft ошибочно утверждается, что размер строки развертки должен быть кратен 32 битам. На самом деле это не обязательно. На практике GDI всегда стремится объединять вызовы разных функций в один системный вызов; функция CreateBitmapIndirect также реализуется вызовом CreateBitmap. Но если значение поля bmWidthBytes выходит за 16-разрядные границы, то GDI перед вызовом CreateBitmap выделяет временный блок памяти из системной кучи и копирует исходный массив пикселов.
Аппаратно-зависимые растры 567 GetObject и DDB При вызове CreateBitmap или CreateBitmapIndirect параметры всего лишь определяют требования к формату массива пикселов. GDI или драйвер графического устройства могут выбрать необходимость сохранения растра в формате, поддерживаемом устройством. Это вполне допустимо, поскольку формат является ап- паратно-зависимым. По манипулятору объекта DDB функция GetObject дает некоторое представление о реальном формате, используемом для представления растра. Приложение не имеет прямого доступа к аппаратно-зависимому растру, поэтому эта информация неполна. Например, в структуре, возвращаемой GetObject, поле bmBits всегда равно NULL, потому что GDI не хочет сообщать приложению, где хранятся графические данные растра. Поле bmWidthBytes всегда округляется до четного числа, но во внутреннем представлении растр может храниться в формате DIB (с выравниванием по границе DWORD), поддерживаемом графическим механизмом систем семейства NT. Пример использования CreateBitmapIndirect и GetObject: DWORD Chess44[] = { OxCC. OxCC, 0x33, 0x33, OxCC. OxCC, 0x33. 0x33 }; BITMAP bmp - { 0. 8. 8, sizeof(Chess44[0]). 1. 1, Chess44 }; HBITMAP hBmp - CreateBitmapIndirect(&bmp); GetObject(hBmp, sizeof(bmp), & bmp); DeleteObject(hBMP): Приведенный фрагмент создает DDB функцией CreateBitmapIndirect, используя массив пикселов, выровненный по границе DWORD, а затем получает информацию объекта DDB при помощи функции GetObject. В структуре BITMAP, заполняемой функцией GetObject, поле bmWidthBytes равно 2 вместо 4, а поле bmBits равно NULL. Если переопределить Chess44 как массив типа WORD, результат будет тем же. CreateCompatibieBitmap и CreateDiscardableBitmap Цветной растр, созданный функцией CreateBitmap или CreateBitmapIndirect, может оказаться несовместимым с контекстом устройства, в котором вы собираетесь его отобразить. Вполне нормальный DDB-растр, несовместимый с контекстом устройства, будет отвергнут при попытке использования его в данном контексте. Чтобы создаваемый растр был заведомо совместим с устройством, воспользуйтесь функцией CreateCompatibieBitmap. Функция CreateCompatibieBitmap выглядит гораздо проще — ей передается только манипулятор эталонного контекста устройства, а также требуемая ширина и высота растра. CreateCompatibieBitmap не нужно знать количество цветовых плоскостей и количество бит на пиксел, поскольку эти характеристики вычисляются на основании данных эталонного контекста. Если контекст устройства соответствует физическому графическому устройству, GDI задействует его характеристики для создания растра. Например, при использовании экранного контекста устройства режиме с 256 цветами CreateCompatibleDC создает DDB с кодировкой 8 бит/пиксел, а в 32-разрядном режиме True Color создается DDB с кодировкой 32 бит/пиксел. Следовательно, если приложение запрашивает DDB с размерами
568 Глава 10. Основные сведения о растрах 1024 х 1024, то необходимый объем памяти будет равен 1 Мбайт для режима с 256 цветами и 4 Мбайт для 32-разрядного режима True Color. Для совместимых контекстов устройств GDI создает растр с таким же форматом пикселов, как и у текущего растра, выбранного в контексте устройства. Как было сказано в предыдущем разделе, при создании совместимого контекста устройства в нем выбирается монохромный растр, состоящий из одного пиксела, поэтому функция CreateCompatibleBitmap для этого контекста устройства создает монохромный растр. Следующая функция отвечает на некоторые часто возникающие вопросы — почему функция CreateCompatibleBitmap отказывается создавать DDB и каков максимальный размер DDB-растра? Функция LargestDDB получает манипулятор контекста устройства и использует алгоритм бинарного поиска для определения размеров наибольшего DDB-растра, совместимого с этим контекстом. HBITMAP LargestDDBCHDC hDC) { HBITMAP hBmp; int mins = 1; int maxs = 1024 * 128; while ( true ) // Бинарный поиск наибольшего DDB { int mid = (mins + maxs)/2; hBmp = CreateCompatibleBitmap(hDC, mid. mid); if ( hBmp ) { HBITMAP h = CreateCompatibleBitmap(hDC, mid+1. mid+1); if ( h==NULL ) return hBmp; DeleteObject(h); DeleteObject(hBmp); mins = mid+1; } else maxs = mid; } return NULL; } При передаче только что созданного совместимого контекста устройства функция может генерировать довольно большой монохромный DDB-растр; если передается экранный контекст, наибольший DIB-растр имеет существенно меньшие размеры в пикселах из-за увеличившейся цветовой глубины. В табл. 10.5 приведены результаты, полученные при вызове функции LargestDDB для контекстов устройств с разной цветовой глубиной. На первый взгляд эта статистика выглядит просто, однако она может сильно повлиять на архитектуру ваших программ. Если приложение работает с DDB, то размер одного DDB-растра фактически ограничивается величиной в 3,96 мегапиксела для худшего случая — системы Windows 95/98 в экранном режиме с
Аппаратно-зависимые растры 569 32-разрядной кодировкой пикселов. Современные цифровые фотоаппараты нередко создают изображения, содержащие свыше 2 мегапикселов. Следовательно, приложение даже не сможет сохранить в DDB изображение, полученное с цифрового фотоаппарата, в масштабе 2:1, поскольку оно будет занимать 16 Мбайт. Таблица 10.5. Максимальные размеры совместимого DDB-растра Цветовая глубина DC Windows NT/2000 Windows 95/98 1 17 408x17 408; 36,125 Мбайт 11474x11474; 15,71 Мбайт 8 6144x6144; 36 Мбайт 4079x4079; 15,87 Мбайт 16 4352x4352; 36,125 Мбайт 2880x2880; 15,82 Мбайт 24 3584x3584; 36,76 Мбайт 2352x2352; 15,82 Мбайт 32 3072x3072; 36 Мбайт 2039x2039; 15,86 Мбайт Во времена Winl6 памяти вечно не хватало, поэтому в GDI была включена функция CreateDiscardableBitmap для создания освобождаемых растров. Идея состояла в том, чтобы интерфейс GDI мог освобождать ресурсы растра в случае их нехватки. Каждый раз, когда приложение хотело воспользоваться освобождаемым растром, оно должно было проверить его и воссоздать заново, если растр стал недействительным. Хотя функция CreateDiscardableBitmap входит и в Win32 GDI, она не создает освобождаемый растр. Вместо этого она просто вызывает CreateCompatibleBitmap. В 32-разрядных операционных системах затраты памяти на хранение аппаратно-зависимых растров по-прежнему остаются серьезной проблемой, особенно в экранных режимах True Color при высоком разрешении. Впрочем, программы Win32 могут использовать DIB-секции и тем самым переместить затраты памяти из системных ресурсов GDI на уровень приложения. CreateDIBitmap Описанные выше функции не позволяют легко создать инициализированный цветной DDB-растр. Хотя при вызове CreateBitmap и CreateBitmapIndirect можно передать указатель на массив пикселов, сложность цветного изображения при этом не учитывается. С другой стороны, аппаратно-независимый растр обладает хорошими средствами для описания стандартных цветовых форматов. По этой причине в GDI была предусмотрена функция CreateDIBitmap, которая создает инициализированный DDB-растр на базе DIB, то есть в каком-то смысле преобразует DIB в DDB. Функция CreateDIBitmap работает в два этапа: сначала она создает DDB, а затем преобразует DIB в DDB. В параметре hdc передается манипулятор эталонного контекста устройства, с которым должен быть совместим созданный DDB- растр. При передаче NULL создается монохромный DIB-растр. Параметр Ipbmih содержит указатель на структуру заголовка блока описания растра, но в ней используются только поля ширины и высоты. Если высота отрицательна, исполь-
570 Глава 10. Основные сведения о растрах зуется абсолютное значение. Другие поля (такие, как количество бит на пиксел и режим сжатия) не используются. Инициализация созданного DDB-растра необязательна и зависит от параметра fdwinit. Если параметр равен CBMINIT, следующие три параметра полностью описывают неупакованный DIB-растр. Параметр lpblnit указывает на массив пикселов, параметр 1 pbmi — на заголовок блока описания растра, а параметр fuUsage сообщает, содержит ли цветовая таблица индексы палитры или цвета RGB. Следующая функция класса KDIB преобразует DIB в DDB: HBITMAP ConvertToDDB(HDC hDC) { return CreateDIBitmap(hDC. & m_pBMI->bmiHeader, CBMJNIT, m_pBits. m_pBMI, DIB_RGB_C0L0RS); } Если в процессе преобразования DIB в DDB задействована палитра, то используется текущая палитра, выбранная в контексте устройства. LoadBitmap В Windows-программировании растры обычно присоединяются к модулю в виде ресурса и затем загружаются в виде DDB функцией LoadBitmap. Функция LoadBitmap получает два параметра: hlnstance — манипулятор модуля, содержащего растровый ресурс, и IpBitmapName — имя растрового ресурса. Если параметр hlnstance равен NULL, во втором параметре передаются константы 0BM_BTNC0RNERS, 0BM_CHECK и т. д., определяющие десятки стандартных системных растров. Эти растры либо берутся непосредственно из модуля USER32.dll, либо синтезируются этим модулем. Если для идентификации растра в ресурсном файле используется целочисленный идентификатор, преобразование целого числа в указатель на символьную строку выполняется с помощью макроса MAKEINTRESOURCE. Растровые ресурсы хранятся в модулях Win32 в формате упакованного DIB- растра. Функция LoadBitmap находит растровый ресурс, фиксирует его в памяти для получения манипулятора упакованного DIB-растра и затем создает DDB- растр, совместимый с текущим экранным режимом. Для монохромных растров (то есть DIB-растров, у которых цветовая таблица содержит только черный и белый цвет) GDI использует монохромный формат DDB вместо цветного формата, увеличивающего затраты памяти. При работе в 256-цветном экранном режиме с палитрой загрузка изображений True Color и High Color приводит к ухудшению качества изображения, поскольку приложение не может управлять процессом преобразования цветов. Код следующего фрагмента загружает растровое изображение панели инструментов из библиотеки BROWSEUI.dll: HINSTANCE hMod - LoadLi brary Cbrowseui.dll"); HBITMAP hBmp = LoadBitmap(hMod. MAKEINTRES0URCE(26D); FreeLibrary(hMod); В документации Microsoft сказано, что в Windows 95 при использовании функции LoadBitmap возникают проблемы с загрузкой растров объемом более 64 Кбайт из-за базовой 16-разрядной реализации. Если размер ресурса DIB пре-
Аппаратно-зависимые растры 571 вышает 64 Кбайт, внутренняя реализация LoadBitmap преобразует его в 16-разрядное значение со сдвигом влево, что может привести к потере младших битов размера. Обходное решение заключается в дополнении ресурса нулями для округления размера. В стандартной схеме применения LoadBitmap растр загружается и выводится один раз, после чего объект удаляется. Если вас беспокоит быстродействие программы, в эту схему можно внести изменения. Преобразование DIB в DDB выполняется медленно, a DDB тратит лишние системные ресурсы. В таких ситуациях быстрее и «дешевле» напрямую работать с DIB. Но если растр загружается один раз и используется многократно, использование DDB может сэкономить время, затрачиваемое на преобразование формата растра. Копирование растров между форматами DIB и DDB Кроме функций для создания новых DDB-растров в GDI предусмотрены две функции для копирования пикселов между DDB и DIB. int SetDIBitsCHDC hdc, HBITMAP hbmp. UINT uStartScan. UINT cScanLines. CONST VOID * lpvBits. CONST BITMAPINFO * lpbmi. UINT fuColorUse); int GetDIBitsCHDC hdc. HBITMAP hbmp. UINT uStartScan. UINT cScanLines. CONST VOID * lpvBits. CONST BITMAPINFO * lpbmi. UINT fuColorUse); Списки параметров этих двух функций совпадают, хотя в документации MSDN имена слегка различаются. Первый параметр определяет эталонный контекст устройства, палитра которого должна использоваться при преобразовании формата пикселов. Второй параметр содержит манипулятор существующего объекта аппаратно-зависимого растра. Пять оставшихся параметров определяют фрагмент неупакованного DIB-растра и интерпретацию его цветовой таблицы. Фрагмент может представлять собой как полный DIB-растр, так и группу смежных строк развертки. Подобное решение в основном предназначено для экономии памяти и для постепенного вывода фрагментов растра, загружаемого по медленному соединению. Параметр lpvBits содержит указатель на строки развертки, параметр uStartScan определяет номер первой строки развертки в буфере, а параметр cScanLines — количество строк развертки в буфере. Функция SetDIBits преобразует пикселы заданного фрагмента DIB в формат DDB и копирует результат в DDB. Функция GetDIBits преобразует пикселы из формата DDB в формат DIB и копирует результат в буфер фрагмента DIB. Функция SetDIBits в действительности реализуется функцией SetDIBitsToDevice с указанием в качестве приемника совместимого контекста устройства, в котором выбран DDB-растр. В процессе преобразования в совместимом контексте устройства выбирается палитра контекста, определяемого параметром hdc. Существует несколько способов преобразования DIB в DDB. DIB-растр, хранящийся в виде ресурса, можно загрузить в DDB функцией LoadBitmap. К сожалению, при этом вы не управляете процессом преобразования. Функция CreateDIBitmap создает на базе DIB абсолютно новый DDB-растр, совместимый с заданным контекстом устройства. Таким образом, эта функция позволяет в
572 Глава 10. Основные сведения о растрах определенной степени управлять преобразованием цветов и в то же время с ней легко работать. По сравнению с LoadBitmap и CreateDIBitmap функция SetDIBits обладает более широкими возможностями. Поскольку в DDB копируется не весь DIB-растр, а его фрагмент, вызывающая сторона может использовать буфер меньшего размера и выполнять преобразование постепенно; кроме того, она может объединить несколько маленьких DIB-растров в один большой DDB-растр и управлять форматом DDB. Как считается, самый распространенный способ преобразования DDB в DIB предлагает функция GetDIBits. Процесс преобразования DDB в DIB непрост, поскольку при этом приходится обеспечивать поддержку разных форматов DIB. Функция GetDIBits поддерживает все допустимые для DIB комбинации цветовых кодировок, форматов (RGB/палитра), наличия и отсутствия сжатия RLE в массиве пикселов, а также битовых полей. В параметре lpbmi передается указатель на информационный заголовок DIB-растра, определяющий его формат. При сжатии RLE приложение не может легко определить размер сжатого изображения. Функцию GetDIBits приходится вызывать дважды. При первом вызове в указателе на массив пикселов передается NULL, на что GDI возвращает необходимый размер буфера. При втором вызове выделенный буфер указанного размера заполняется данными изображения. В листинге 10.3 приведена функция BitmapToDIB, которая является удобной оболочкой для вызова функции GetDIBits. Функция получает манипулятор объекта палитры GDI, используемого для построения цветовой таблицы, манипулятор объекта DDB, количество байт на пиксел и флаг сжатия DIB. Функция вычисляет размер DIB, выделяет буфер нужного размера, записывает в него данные DDB и возвращает указатель на буфер. Листинг 10.3. Функция BitmapToDIB: преобразование DDB в DIB BITMAPINFO * BitmapToDIBCHPALETTE hPal, // Палитра для // преобразования цвета HBITMAP hBmp, // Преобразуемый DDB-растр int nBitCount, int nCompression) // Нужный формат { typedef struct { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[256+3]; } DIBINFO; BITMAP ddbinfo; DIBINFO dibinfo; // Получение данных DDB if ( GetObject(hBmp, sizeof(BITMAP), & ddbinfo)==0 ) return NULL; // Заполнение структуры BITMAPINFOHEADER // по данным размера и запрашиваемому формату memsetC&dibinfo, 0, sizeof(dibinfo)); di bi nfо.bmi Header.bi Si ze = sizeof(BITMAPINFOHEADER);
Аппаратно-зависимые растры 573 dibinfo.bmiHeader.bi Width = ddbi nfo.bmWi dth: dibinfo.bmiHeader.biHeight = ddbinfo.bmHeight: dibinfo.bmiHeader.biPlanes = 1: dibinfo.bmiHeader.biBitCount = nBitCount; dibinfo.bmiHeader.biCompression = nCompression; HDC hDC = GetDC(NULL); // Экранный контекст устройства HGDIOBJ hpalOld; if ( hPal ) hpalOld = SelectPalette(hDC, hPal. FALSE); else hpalOld = NULL; // Запросить у GDI размер изображения GetDIBitsChDC. hBmp. 0. ddbinfo.bmHeight. NULL. (BITMAPINFO *) & dibinfo. DIB_RGB_COLORS); int nlnfoSize = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * GetDIBColorCount(dibinfo.bmiHeader); int nTotalSize = nlnfoSize + GetDIBPixelSize(dibinfo.bmiHeader); BYTE * pDIB = new BYTE[nTotalSize]; if ( pDIB ) { memcpy(pDIB. & dibinfo. nlnfoSize); if ( ddbinfo.bmHeight != GetDIBitsChDC. hBmp. 0. ddbinfo.bmHeight. pDIB + nlnfoSize. (BITMAPINFO *) pDIB. DIB_RGB_COLORS) ) { delete [] pDIB; pDIB = NULL; if ( hpalOld ) SelectObject(hDC. hpalOld); ReleaseDC(NULL. hDC); return (BITMAPINFO *) pDIB; } Для упрощения вызова функция BitmapToDIB не требует, чтобы вызывающая сторона передавала структуру BITMAPINFO. Она создает в стеке структуру DIBINFO с цветовой таблицей из 259 элементов; этого вполне достаточно для DIB-растра, использующего битовые поля и 256 цветов полной палитры. Ширина и высота DIB вычисляются по размерам DDB. После того как первый вызов GetDIBits вернет фактический размер изображения, функция выделяет память для буфера, копирует заголовок описания растра, а затем вызывает GetDIBits для загрузки всего DIB-растра.
574 Глава 10. Основные сведения о растрах Функция GetDIBits обладает и другой неочевидной особенностью. Если присвоить значение только полю biSize и оставить остальные поля равными 0, GDI заполнит их данными о количестве бит/пиксел и режиме сжатия, используемом контекстом устройства. Таким образом, приложение может точно определить формат пикселов, используемый контекстом устройства. Этот прием особенно полезен в том случае, если приложение должно напрямую работать с пикселами в видеорежиме с кодировкой 16 бит/пиксел. У 16-разрядной кодировки существует два стандартных подтипа: 5-5-5 и 5-6-5. В некоторых ситуациях приложение должно знать точный формат пикселов; задача решается функцией Pixel Format, приведенной в листинге 10.4. Листинг 10.4. Функция PixelFormat: определение формата пикселов контекста устройства int PixelFormatCHDC hdc) { typedef struct { BITMAPINFOHEADER bmiHeader: RGBQUAD bmiColors[256+3]: } DIBINFO: DIBINFO dibinfo; HBITMAP hBmp = CreateCompatibleBitmap(hdc, 1. 1): if ( hBmp==NULL ) return -1: memsetC&dibinfo. 0, sizeof(dibinfo)): dibinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // Первый вызов для получения значения biBitCount для hdc GetDIBits(hdc. hBmp, 0, 1. NULL, (BITMAPINF0*) & dibinfo, DIB_RGB_C0L0RS): // Второй вызов для получения цветовой таблицы или битовых полей GetDIBits(hdc, hBmp. 0. 1. NULL, (BITMAPINFO*) & dibinfo. DIB_RGB_C0L0RS): DeleteObject(hBmp): // Попытаться интерпретировать битовые поля if ( di bi nfo.bmi Header.bi BitCount==BI_BITFIELDS ) { DWORD * pBitFields = (DWORD *) dibinfo.bmiColors: DWORD red - pBitFields[0]: DWORD green = pBitFields[l]: DWORD blue = pBitFields[2]; if ( (blue—OxOOlF) && (green==0x03E0) && (red==0x7C00) ) return DIB_16RGB555: else if ( (blue—OxOOlF) && (green==0x007E) && (red==0xF800) ) return DIBJ6RGB565: else if ( (blue==0x00FF) && (green==0xFF00) && (red==0xFF0000) )
Аппаратно-зависимые растры 575 return DIBJ2RGB888; else return -1; } switch ( dibinfo.bmiHeader.biBitCount ) { case 1:return DIB_1BPP; case 2: return DIB_2BPP: case 4:return DIB_4BPP; case 8: return DIB_8BPP; case 24: return DIB_24RGB888: case 16: return DIBJ.6RGB555; case 32: return DIB_32RGB888: default: return -1: } } Функции SetDIBits и GetDIBits поддерживают и другой формат растров GDI, о которых речь пойдет в разделе «DIB-секции». В документации (KB Q230499) сказано, что при использовании GetDIBits для преобразования DIB-секций с кодировкой 1 или 4 бит/пиксел в DIB с кодировкой 8 бит/пиксел цветовая таблица DIB настраивается неправильно. Прямой доступ к массиву пикселов DDB Одна из главных особенностей аппаратно-зависимых растров заключается в том, что DDB-растры не имеют цветовой таблицы и могут иметь уникальный внутренний формат пикселов, определяемый производителем оборудования. Единственным стандартным форматом DDB считается монохромный формат. Из-за этого прямой доступ к графическим данным DDB не имеет особого смысла (особенно для цветных DDB-растров). Однако в GDI предусмотрена пара функций, при помощи которых приложение может работать с массивами пикселов DDB: LONG GetBitmapBits(HBITMAP hBmp, LONG cbBuffer, LPVOID IpvBits): LONG SetBitmapBits(HBITMAP hBmp. LONG cBytes, LPVOID IpBits): Функция GetBitmapBits копирует массив пикселов DIB в буфер, заданный параметрами cbBuffer и IpvBits. Но как узнать размер выделяемого буфера? В документации Microsoft не упоминается о том, что функция GetBitmapBits возвращает необходимый размер буфера, если размер равен 0, а указатель на буфер содержит NULL. Массив пикселов копируется без преобразования формата и цвета. Функция SetBitmapBits выполняет обратную операцию: она копирует содержимое буфера в массив пикселов растра. Найти разумное применение для этих двух функций нелегко. Один из возможных вариантов — реализация эффективных алгоритмов работы с DDB без применения совместимых контекстов. Например, вы можете легко реализовать инверсию каждого пиксела, зеркальное отражение и повороты строк развертки. Другое возможное применение — исследование внутреннего формата DDB. Приведенная ниже функция выводит содержимое массива пикселов DDB в текстовом окне (предполагается, что у нас имеется функция вывода шестнадцатерич-
576 Глава 10. Основные сведения о растрах ного дампа HexDump). В системах семейства Windows NT почти все видеоадаптеры используют кадровые буферы с одноплоскостным форматом DIB, поэтому у них массив пикселов DDB достаточно близок к массиву DIB. С другой стороны, в стандартных режимах VGA или в системах семейства Windows 95 формат DDB усложняется. void DumpBitmapCHWND hWnd. HBITMAP hBMP) { int size = GetBitmapBits(hBmp, 0, NULL); BYTE * pBuffer = new BYTE[size]; if ( pBuffer ) { GetBitmapBits(hBmp, size, pBuffer); HexDumpChWnd, pBuffer, size): delete [] pBuffer; } } Использование DDB-растров Аппаратно-зависимые растры широко используются в Windows-программировании. Предыдущий раздел был посвящен разным способам создания DDB и преобразования между DDB, DIB и непосредственным содержимым массива пикселов. В этом разделе рассматривается отображение DDB-растров и их использование в меню, панелях инструментов и т. д. Отображение DDB-растров Хотя DDB-растры играют очень важную роль в Windows-программировании, вы не найдете в GDI функции для непосредственного отображения DIB. Чтобы вывести DIB, приложение должно создать совместимый контекст устройства, выбрать в нем DDB и скопировать данные пикселов из совместимого контекста устройства в приемный контекст. Эта контекстно-ориентированная схема хороша тем, что DDB-растр может быть как источником, так и приемником для операции вывода. Более того, вы даже можете скопировать одну часть DDB в другую часть того же DDB-растра! Для сравнения стоит заметить, что GDI поддерживает только функции, отображающие содержимое DIB, — и ни одной функции для вывода в DIB. В GDI задача отображения DDB решается обобщенно, как задача пересылки прямоугольного массива пикселов с одного графического устройства на другое графическое устройство, каждое из которых представлено манипулятором контекста устройства. Такие операции пересылки традиционно обозначаются сокращением «BitBlt» от слов «Bit boundary Block Transfer»1. B GDI существует две основные функции блиттинга: 1 В русском языке обычно используется термин «блиттинг». — Примеч. перев.
Использование DDB-растров 577 BOOL BitBlKHDC hdcDst. int nXDst. int nYDst. int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop); BOOL BitBltCHDC hdcDst, int nXDst, int nYDst. int nWDst. int nHDst, HDC hdcSrc. int nXSrc. int nYSrc. nWSrc. int nHSrc. DWORD dwRop); Функция BitBlt передает прямоугольный блок пикселов с исходного устройства в прямоугольник приемного устройства. Исходный прямоугольник определяется параметрами nXSrc, nYSrc, nWidth и nHeight в логической системе координат исходного контекста устройства. Приемный прямоугольник определяется параметрами nXDst, nYDst, nWidth pi nHeight в логической системе координат приемного контекста устройства. Оба контекста устройства должны поддерживать растровые операции RCBITBLT. Например, если исходный контекст устройства является метафайловым контекстом, попытка вызова BitBlt завершится неудачей, поскольку метафайловый контекст не имеет кадрового буфера, содержимое которого можно прочитать. GDI также не справляется со случаями, когда в исходном контексте устройства действует преобразование поворота или сдвига, способное превратить исходный прямоугольник в параллелограмм или прямоугольник, стороны которого не параллельны осям. Если исходный и приемный прямоугольники имеют разные размеры в системах координат устройства, исходное изображение масштабируется по размерам приемного многоугольника. В приемном контексте устройства могут действовать любые преобразования, хотя в системах семейства Windows 95 мировые преобразования не поддерживаются. Как и в случае с функцией StretchDIBits, изменение знака в параметрах исходного и приемного прямоугольника позволяет выполнить зеркальное отражение растра по вертикальной и/или горизонтальной оси. Последний параметр dwRop определяет тернарную растровую операцию, то есть способ объединения исходного пиксела, приемного пиксела и кисти для формирования нового значения исходного пиксела. Пока мы ограничимся тернарной операцией SRCC0PY, при которой пикселы приемника попросту заменяются пикселами источника. Функция StretchBlt делает практически то же, что и функция BitBlt. Единственное различие заключается в независимом определении размеров исходного и приемного прямоугольников, поэтому функция StretchBlt обычно используется в ситуациях, когда исходный и приемный прямоугольники имеют разные размеры в логических координатах. Понадобится ли реальное масштабирование или нет — зависит от настройки логических систем координат. Случайная перестановка фрагментов экрана Давайте рассмотрим использование функций BitBlt/StretchBlt на конкретном примере. Мы напишем программу, которая копирует случайно выбранный прямоугольник экрана в другой случайно выбранный прямоугольник. Чтобы изображение выглядело поярче, мы задействуем случайно выбранную растровую операцию с однородной кистью случайно выбранного цвета. Приемный прямоугольник определяется с отрицательной шириной и высотой, поэтому исходный прямоугольник поворачивается на 180°. В результате масштабирования приемный прямоугольник увеличивается вдвое по сравнению с источником. После нескольких итераций экран начинает выглядеть довольно оригинально. Мы не собираемся писать полноценную экранную заставку (screen saver), поэтому код
578 Глава 10. Основные сведения о растрах просто выполняется в цикле 200 раз, а затем программа завершается запросом на перерисовку экрана. int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE. PSTR, int) { HDC hDC = GetDC(NULL); int width = GetSystemMetrics(SM_CXSCREEN); int height = GetSystemMetrics(SM_CYSCREEN); for (int i=0; i<2000; i++) { HBRUSH hBrush = CreateSolidBrush(RGB(rand()*256. rand()*256.rand()a!256)): SelectObject(hDC. hBrush); BOOL rslt = StretchBlt(hDC, randO % width, randO % height, -64, -64, hDC. randO % width, randO % height. 32, 32, (randO % 256) « 16); SelectObject(hDC, GetStockObject(WHITE_BRUSH)): DeleteObject(hBrush); Sleep(l): ReleaseDC(NULL. hDC); RedrawWindow(NULL. NULL, NULL, RDWJNVALIDATE | RDW_ALLCHILDREN); return 0; Различные способы отображения DDB-растров Функции BitBlt и StretchBlt обычно применяются при выводе аппаратно-зави- симых растров. При правильном использовании эти две функции способны создавать разнообразные графические эффекты. В листинге 10.5 приведен простой класс для работы с DDB. Функция KDDB: :Draw позволяет рисовать обычные растры, растры, выровненные по центру и масштабированные по размерам приемника, растры с сохранением пропорций, а также мозаичные растры. Листинг 10.5. class KDDB { protected: HBITMAP mJiBitmap; HBITMAP mJiQldBmp; void ReleaseDDB(void); public: HDC mJiMemDC: bool Preparednt & width, int & height); typedef enum { drawjnormal. draw_center, draw_tile,draw_stretch, draw_stretchprop }; HBITMAP GetBitmap(void) const { return mJiBitmap; }
Использование DDB-растров 579 KDDBO { mJiBitmap = NULL: m_hMemDC = NULL: mJiOldBmp = NULL: } virtual -KDDBO { ReleaseDDBO: } BOOL Attach(HBITMAP hBmp): BOOL LoadBitmap(HINSTANCE hlnst. int id) { return Attach ( ::LoadBitmap(hInst. MAKEINTRESOURCE(id)) ): } BOOL DrawCHDC hDC. int xO. int yO. int w. int h. DWORD гор. int opt=draw_normal): // Запросить размер, подготовить совместимый контекст устройства // и выбрать в нем растр boo! KDDB::Prepare(int & width, int & height) { BITMAP bmp: if ( ! GetObject(m_hBitmap. sizeof(bmp). & bmp) ) return false: width = bmp.bmWidth; height = bmp.bmHeight; if ( m_hMemDC==NULL ) // Убедиться в том. что создание { // растра прошло успешно HDC hDC = GetDC(NULL): mJiMemDC = CreateCompatibleDC(hDC); ReleaseDCCNULL. hDC): m_h01dBmp = (HBITMAP) SelectObject(m_hMemDC. mJiBitmap): } return true: // Освобождение ресурсов void KDDB::ReleaseDDB(void) { if ( mJiMemDC ) { SelectObject(m_hMemDC. mJiOldBmp): DeleteObject(m_hMemDC): mJiMemDC = NULL: } Продолжение ^
580 Глава 10. Основные сведения о растрах Листинг 10.5. Продолжение if ( mJiBitmap ) { DeleteObject(m_hBitmap); mJiBitmap = NULL: } mJiOldBmp = NULL; BOOL KDDB::Attach(HBITMAP hBmp) { if ( hBmp==NULL ) return FALSE; if ( m_h01dBmp ) // Исключить mJiBitmap { SelectObject(m_hMemDC. m_h01dBmp); mJiOldBmp = NULL; } if ( mJiBitmap ) // Удалить текущий растр DeleteObject(mJiBitmap); m_hBitmap = hBmp; // Заменить новым растром if ( mJiMemDC ) // Выбрать в совместимом контексте устройства. { // если он есть mJiOldBmp = (HBITMAP) SelectObject(m_hMemDC. mJiBitmap): return mJiOldBmp != NULL; } else return TRUE; } BOOL KDDB::Draw(HDC hDC, int xO. int yO. int w. int h. DWORD гор. int opt) { int bmpwidth. bmpheight; if ( ! Prepare(bmpwidth. bmpheight) ) return FALSE; switch ( opt ) { case drawjiormal: return BitBlt(hDC. xO. yO. bmpwidth. bmpheight. mJiMemDC. 0. 0. гор); case draw_center: return BitBltChDC. xO + (w-bmpwidth)/2. yO + ( h-bmpheight)/2. bmpwidth. bmpheight. mJiMemDC. 0. 0. гор); break: case draw_tile: {
Использование DDB-растров 581 for (int j=0; j<h; j+= bmpheight) for (int i=0; i<w; i+= bmpwidth) if ( ! BitBltChDC. xO+i. yO+j. bmpwidth. bmpheight, m_hMemDC, 0. О, гор) ) return FALSE; return TRUE; } break; case draw_stretch; return StretchBlt(hDC. xO. yO, w. h. mJiMemDC. 0, 0, bmpwidth, bmpheight. гор); case draw_stretchprop: { int ww = w; int hh = h; if ( w * bmpheight < h * bmpwidth ) // Выбор оси hh = bmpheight * w / bmpwidth; // для масштабирования else ww = bmpwidth * h / bmpheight; // Пропорциональные масштабирование и центровка return StretchBlt(hDC, xO + (w-ww)/2. yO + (h-hh)/2. ww. hh. m_hMemDC. 0. 0. bmpwidth. bmpheight. гор); } default: return FALSE; } } Класс KDDB содержит три переменные: манипулятор совместимого контекста устройства и два манипулятора HBITMAP для нового DDB-растра и для старого DDB-растра, исключаемого из совместимого контекста. DDB-растры чаще всего создаются загрузкой из ресурсов. Экземпляры класса KDDB следует размещать вне обработчика сообщения WMPAINT, чтобы свести к минимуму затраты на преобразование ресурса из формата DIB в DDB и на создание совместимого контекста устройства. В методе KDDB:: Draw поддерживаются различные варианты рисования растра, определяемые последним параметром. В приведенной версии реализованы нормальный вывод, центровка, мозаичная раскладка, простое и пропорциональное масштабирование. В пользовательском интерфейсе растры все чаще используются для оформления заставочных окон (splash screens) и фона. Для небольших текстур часто применяется мозаичное повторение; растры, изображающие самостоятельные объекты, часто выводятся с центровкой и пропорциональным масштабированием. Сохранение окна/экрана После того как DDB-растр будет выбран в совместимом контексте устройства, вы можете выполнять с ним различные операции с помощью функций GDI.
582 Глава 10. Основные сведения о растрах Простейшей операцией является сохранение содержимого окна, и как частный случай — сохранение всего экрана. Задача решается приведенной ниже функцией CaptureWindow. HBITMAP CaptureWindow(HWND hWnd) { RECT wnd; if ( ! GetWindowRectChWnd. & wnd) ) return NULL; HDC hDC = GetWindowDC(hWnd); HBITMAP hBmp = CreateCompatibleBitmap(hDC, wnd.right-wnd.left, wnd.bottom - wnd.top); if (hBmp) { HDC hMemDC = CreateCompatibleDC(hDC); HGDIOBJ hOld = SelectObject(hMemDC. hBmp): BitBlt(hMemDC. 0. 0, wnd.right - wnd.left. wnd.bottom - wnd.top. hDC. 0. 0. SRCCOPY): SelectObject(hMemDC. hOld); DeleteObject(hMemDC); } ReleaseDC(hWnd. hDC); return hBmp; } Функция CaptureWindow возвращает манипулятор DDB, который затем можно преобразовать в DIB, сохранить в дисковом файле или скопировать в буфер обмена. Преобразование цветов DDB Два контекста устройств, исходный и приемный, могут иметь разный формат кадрового буфера или характеристики палитры. Например, монохромный исходный растр может копироваться на приемную поверхность с 32-разрядной кодировкой цвета, или наоборот — исходное изображение в формате True Color может копироваться на монохромную поверхность. В этом случае функция BitBlt/ StretchBlt преобразует пикселы из цветового формата источника к формату приемника. Если один из контекстов устройств является совместимым контекстом с выбранным DDB-растром, несовпадение возможно лишь в том случае, если один из контекстов является монохромным. Например, для экрана в режиме с 24-разрядным цветом совместимый контекст устройства обычно создается в соответствующем цветовом формате. Функции LoadBitmap и CreateCompatibleBitmap генерируют только 24-разрядные или монохромные растры. GDI позволяет выбрать в контексте устройства только 24-разрядный или монохромный DDB-растр. Если приложение создает растр с 8-разрядным цветом, создание растра пройдет
Использование DDB-растров 583 успешно, но попытка выбрать его в контексте устройства, совместимом с экраном, завершится неудачей. При выводе монохромного растра на цветной поверхности GDI не ограничивается простым отображением черного и белого цвета; вместо этого GDI позволяет раскрасить растр с использованием атрибутов основного и фонового цвета контекста устройства. По умолчанию фоновым цветом контекста устройства является белый цвет, а основным цветом (цветом текста) — черный, но вместо них можно выбрать любые другие цвета функциями SetBkColor и SetTextCol or. В монохромном растре значения пикселов равны 0 и 1. Считается, что пикселы со значением 0 относятся к основному цвету, а пикселы со значением 1 — к фоновому. При выводе пикселов основного цвета (0) GDI использует основной цвет приемного контекста устройства, а при выводе пикселов фонового цвета (1) — фоновый цвет приемного контекста. В следующем фрагменте показано, как создать мозаичную раскладку с использованием разных основных и фоновых цветов. const C0L0RREF ColorTable[] = { RGBCOxFF. 0. 0). RGB(0. OxFF. 0). RGB(0. 0. OxFF). RGB(0xFF, OxFF, 0). RGB(0. OxFF. OxFF). RGB(OxFF. 0. OxFF) }: for (int y=0: y<clientheight; y+= bmpheight ) for (int x=0: x<clientwidth; x+= bmpwidth ) { SetTextColor(hDC. ColorTable[y/bmpheight]); SetBkColor(hDC. ColorTable[x/bmpwidth] | RGB(0xC0, OxCO. 0xC0)); BitBlt(hDC. x. y. bmpwidth. bmpheight. hMemDC. 0. 0. SRCCOPY): } При выводе цветного растра в монохромном контексте устройства каждому цветному пикселу необходимо поставить в соответствие либо 0, либо 1. Мы знаем, что при выводе DIB в монохромном контексте устройства GDI пытается подобрать для каждого пиксела ближайший цвет, однако при выводе DDB интерфейс GDI действует совершенно иначе. Цвет каждого пиксела сравнивается с фоновым цветом исходного контекста устройства. Значения пикселов, совпадающих с фоновым цветом, преобразуются в 1 (белый), а остальные пикселы преобразуются в 0 (черный). Обратите внимание: в процессе преобразования учитывается только фоновый цвет, а основной цвет не используется. Этот на первый взгляд «наивный» способ преобразования цветных растров в монохромные на самом деле оказывается очень полезным. Он обеспечивает простые средства для деления пикселов растра на фоновые и не относящиеся к фону; сгенерированный при этом растр может использоваться в качестве маски. Маска может пригодиться в тернарных растровых операциях для отображения растров с прозрачными участками (спрайтов). Подробное описание вывода прозрачных растров вы найдете в главе 11. Ниже приведена новая функция класса KDDB, которая генерирует монохромный растр по заданному цвету фона. Функция CreateMask использует вызов Create- Bitmap для создания монохромного растра, устанавливает заданный цвет в качестве фонового в исходном совместимом контексте устройства, после чего преобразует цветной растр в монохромный функцией BitBlt.
584 Глава 10. Основные сведения о растрах HBITMAP KDDB::CreateMaskCCOLORREF crBackGround. HDC hMaskDC) { int width, height; if ( ! Prepare(width, height) ) return NULL; HBITMAP hMask - CreateBitmap(width, height, 1. 1. NULL); HBITMAP hOld = (HBITMAP) SelectObject(hMaskDC, hMask); SetBkColor(m_hMemDC, crBackGround); BitBltChMaskDC, 0. 0, width, height. mJiMemDC. 0, 0, SRCCOPY); return hOld: } На рис. 10.3 изображены 9 масок, созданных функцией KDDB: :CreateMask для каждого из цветов, задействованных в цветном изображении. На первом месте показан цветной растр, а затем следуют монохромные маски. При отображении масок используется основной и фоновый цвет по умолчанию; 1 соответствует белому цвету, а 0 — черному. Рис. 10.3. Разложение цветного DDB-растра на монохромные маски Использование растров в меню В программировании для Windows с каждой командой меню можно связать два маленьких растра. Эти растры выводятся рядом с командой; первый — когда команда активизирована (checked), а второй — когда команда пассивна (unchecked). По умолчанию Windows не выводит растры для пассивных команд, а активные команды помечаются стандартным растровым рисунком в виде «галочки». Впрочем, эти растры вовсе не обязаны соответствовать активному или пассивному состоянию команды. Скажем, маленький значок в виде принтера рядом с командой Print определенно делает меню более наглядным. Для изменения этих растров-меток используются функции SetMenuItemBitmaps и SetMenuItemlnfo. Хотя сами по себе эти функции просты, с подготовкой и обработкой растров дело обстоит сложнее. Чтобы растр сливался с фоном меню, фо-
Использование DDB-растров 585 новые пикселы растра должны быть окрашены в фоновый цвет меню. Кроме того, растры необходимо масштабировать по высоте команд меню. Растр, манипулятор которого передается функциям SetMenuItemBitmap и SetMenuItemlnfo, нельзя удалять до тех пор, пока меню не перестает использоваться. В листинге 10.6 приведен класс для работы с растрами-метками команд меню. Листинг 10.6. Использование растров в качестве меток для команд меню class KCheckMark { protected: typedef enum { MAXSUBIMAGES = 50 }; HBITMAP mJiBmp; int m_nSubImageId[MAXSUBIMAGES]; HBITMAP mJiSublmage [MAXSUBIMAGES]; int mjiUsed; public: KCheckMark () { m_hBmp = NULL; m nUsed = 0; -KCheckMarkO: void AddBitmapdnt id. HBITMAP hBmp); void LoadToo1bar(HMODULE hModule. int resid, bool transparent=false); HBITMAP GetSubImage(int id); BOOL SetCheckMarks(HMENU hMenu, UINT uPos. UINT uFlags, int unchecked, int checked); void KCheckMark::AddBitmap(int id. HBITMAP hBmp) { if ( m_nUsed < MAXSUBIMAGES ) { m_nSubImageId[m_nUsed] = id; mJiSublmage [m_nUsed++] = hBmp; } } void KCheckMark::LoadToolbar(HMODULE hModule. int resid. bool transparent) { mJiBmp - (HBITMAP) ::LoadImage(hModule. MAKEINTRES0URCE(resid). IMAGE_BITMAP. 0. 0. transparent ? LRJ.0ADTRANSPARENT ; 0); AddBitmap((int) hModule + resid. mJiBmp); KCheckMark:-KCheckMarkO Продолжение ё>
586 Глава 10. Основные сведения о растрах Листинг 10.6. Продолжение { for (int i=0: i<m_nUsed; i++) DeleteObject(m_hSubImage[i]); } HBITMAP KCheckMark::GetSubImage(int id) { if ( id < 0 ) return NULL; for (int i=0: i<m_nUsed; i++) if ( m_nSubImageId[i]==id ) return m_hSubImage[i]; BITMAP bmp; if ( ! GetObject(m_hBmp. sizeof(bmp). & bmp) ) return NULL; if ( id *bmp.bmHeight >= bmp.bmWidth ) return NULL; HDC hMemDCS = CreateCompatibleDC(NULL); HDC hMemDCD - CreateCompatibleDC(NULL); SelectObject(hMemDCS. m_hBmp); int w - GetSystemMetrics(SM_CXMENUCHECK); int h = GetSystemMetrics(SM_CYMENUCHECK); HBITMAP hRslt = CreateCompatibleBitmap(hMemDCS. w. h); if ( hRslt ) { HGDIOBJ hOld = SelectObject(hMemDCD. hRslt); StretchBltChMemDCD, 0. 0. w. h. hMemDCS. id*bmp.bmHeight. 0. bmp.bmHeight, bmp.bmHeight. SRCCOPY); SelectObject(hMemDCD. hOld); AddBitmap(id. hRslt); } DeleteObject(hMemDCS); DeleteObject(hMemDCD); return hRslt; } BOOL KCheckMark::SetCheckMarks(HMENU hMenu. UINT uPos. UINT uFlags. int unchecked, int checked) { return SetMenuItemBitmaps(hMenu. uPos. uFlags. GetSublmage(unchecked). GetSublmage(checked)); }
Использование DDB-растров 587 Класс KCheckMark загружает один растровый рисунок, состоящий из нескольких растров меньшего размера, расположенных рядом (наподобие растров, используемых при работе с панелями инструментов). Растр загружается функцией Loadlmage, которая обеспечивает замену фоновых пикселов стандартным цветом окна (C0L0R_WIND0W) при помощи флага LRJ.OADTRANSPARENT. Цвет окна по умолчанию обычно совпадает с фоновым цветом меню, и это помогает нам решить проблему слияния растра с фоном меню. Большая часть полезной работы в этом классе выполняется функцией GetSublmage, которая «вырезает» из растра небольшой фрагмент. Предполагается, что фрагменты расположены в одну строку, поэтому их размеры вычисляются по высоте общего растра. Функция получает размеры растров, используемых в качестве меток для команд меню, и масштабирует по ним фрагменты. Идентификатор и манипулятор растра-фрагмента сохраняются в таблице, чтобы их можно было задействовать в будущем. Выбор новых меток вместо назначенных ранее или используемых по умолчанию выполняет функция SetMenuImageltems. Поскольку операционная система Windows не создает копий растров, таблица манипуляторов используется в деструкторе класса для удаления растровых объектов. Экземпляры класса KCheckMark должны существовать на уровне окна или приложения, чтобы их деструкторы вызывались лишь тогда, когда растр перестает использоваться. На рис. 10.4 изображен результат применения растров для оформления некоторых стандартных команд меню. Исходный растр загружен из модуля browserui.dll, идентификатор ресурса 275. * frask | ФНттй %Ы ЦСору | paBedo QNew фОреп SSave fj|PjintePr&vtew jgfPmpeifes ИГНФ *,'''' 'фЩфт*. ' ''."// Рис. 10.4. Оформление стандартных команд меню растровыми метками
588 Глава 10. Основные сведения о растрах Растры также позволяют заменить обычный текст в командах в меню. Для этой цели используются функции AppendMenu, InsertMenuItem и SetMenuItemlnfo. В следующей программе показано, как создать подменю с растровыми командами. void KBitmapMenu::AddToMenu(HMENU hMenu. int nCount, HMODULE hModule, const int nID[]. int nFirstCommand) { m_hMenu = hMenu; mjiBitmap = nCount; mjiChecked = 0; mjiFirstCommand = nFirstCommand; for (int i=0; i<nCount; i++) { m_hBitmap[i] = LoadBitmap(hModule. MAKEINTRESOURCE(nID[i])); if ( m_hBitmap[i] ) AppendMenu(hMenu, MF_BITMAP, nFirstCommand + i, (LPCTSTR) m_hBitmap[i]); CheckMenuItem(m_hMenu, m_nChecked + nFirstCommand. MF_BYC0MMAND | MF_CHECKED); } На рис. 10.5 изображено растровое меню с вариантами текстур и окно, заполненное выбранной текстурой с помощью функции KDDB: :Draw. File Color View Window Oieqk Matk* j Textyres Шй j г —_ ^| i ,i i.-i..i i.i i rr i:.'i.:г.. :r y~r ыш- *'*3£*§ 'ь'-'Л', З&Ф'' - .ffl.lgjxlj I ] 1 pi- I pl„ j rV pr Li "41 Рис. 10.5. Меню с растровыми командами
Использование DDB-растров 589 В Win32 API применение растров в меню должно подчиняться некоторым ограничениям. Например, в экранном режиме с 256 цветами цветопередача растров-меток с большим количеством цветов искажается. Размер меток обычно ограничивается величиной 13 х 13 пикселов, что меньше растров 16 х 16 или 20 х 20, используемых на панелях инструментов. Многим также не нравится то, как система выделяет команды меню. Если вы хотите в полной мере управлять отображением растров в меню: воспользуйтесь меню, прорисовка которых выполняется владельцем (owner-drawn). Впрочем, эта тема выходит за рамки настоящей книги. Использование растра в качестве фона окна При работе с растрами часто возникает вопрос — как вывести растр в качестве фона окна (например, клиентского окна MDI, диалогового окна, страницы свойств или статического элемента управления)? Операции с фоном окна в Windows обычно выполняются при обработке сообщения WM_ERASEBKGND. Обработчик этого сообщения может нарисовать в фоне окна все, что сочтет нужным. Если сообщение не обработано, стандартный обработчик закрашивает фон фоновой кистью, указанной в определении класса окна. Итак, ключевой проблемой является обработка сообщения WM_ERASEBKGND. Как правило, обработчики сообщений для клиентских окон MDI, диалоговых окон, страниц свойств и статических элементов управления не предоставляются приложением, а реализуются операционной системой в модуле user32.dll или commctrl.dll. Следовательно, для вмешательства в процесс прорисовки фона придется воспользоваться методикой субклассирования. Главное — правильно установить перехватчик (hook), а вывод растра — задача несложная. В листинге 10.7 приведен родовой класс, обеспечивающий нестандартную прорисовку фона путем субклассирования. Листинг 10.7. Родовой класс прорисовки фона class KBackground { WNDPROC m_01dProc; virtual LRESULT EraseBackground(HWND hWnd, UINT uMsg. WPARAM wParam, LPARAM lParam); virtual LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM lParam); static LRESULT CALLBACK BackGroundWindowProc(HWND hWnd. UINT uMsg, WPARAM wParam. LPARAM lParam); public: KBackgroundO { m_01dProc = NULL; } virtual -KBackgroundO Продолжение &
590 Глава 10. Основные сведения о растрах Листинг 10.7. Продолжение { } BOOL Attach(HWND hWnd): BOOL Detatch(HWND hWnd); // Реализация KBackground const TCHAR Prop_KBackground[] - J("KBackground Instance"): LRESULT KBackground::EraseBackground(HWND hWnd. UINT uMsg, WPARAM wParam, LPARAM IParam) { return DefWindowProc(hWnd. uMsg. wParam, IParam); } LRESULT KBackground::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM IParam) { if ( uMsg - WMJRASEBKGND ) return EraseBackgroundChWnd, uMsg. wParam, IParam); else return CaHWindowProc(m_01dProc, hWnd. uMsg. wParam, IParam); } LRESULT KBackground::BackGroundWindowProc(HWND hWnd. UINT uMsg, WPARAM wParam, LPARAM IParam) { KBackground * pThis - (KBackground *) GetProp(hWnd, Prop_KBackground): if ( pThis ) return pThis->WndProc(hWnd, uMsg, wParam, IParam); else return DefWindowProc(hWnd. uMsg. wParam. IParam); BOOL KBackground::Attach(HWND hWnd) { SetProp(hWnd, Prop_KBackground, this); m_01dProc - (WNDPROC) SetWindowLong(hWnd. GWL_WNDPROC, (LONG) BackGroundWindowProc); return m 01dProc!=NULL; BOOL KBackground::Detatch(HWND hWnd) { RemoveProp(hWnd, Prop_KBackground); if ( m_01dProc ) return SetWindowLong(hWnd, GWL_WNDPROC. (LONG) m_01dProc) — (LONG) BackGroundWindowProc; else return FALSE; }
Использование DDB-растров 591 В классе KBackground определяются два виртуальных метода (не считая виртуального деструктора). Метод EraseBackground рисует фон окна; реализация по умолчанию просто вызывает DefWindowProc. Метод WndProc обрабатывает все сообщения, хотя в данном случае нас интересует только сообщение WM_ERASEBKGND. Субклассирование существующего окна выполняется вызовом метода Attach. С окном ассоциируется свойство, значение которого представляет собой указатель на экземпляр класса KBackground, а функция окна переопределяется статической функцией BackGroundWindowProc. Получив сообщение, эта функция запрашивает значение свойства, чтобы получить указатель this для экземпляра KBackground, а затем передает вызов его методу WndProc. Обратите внимание — мы не можем сохранить указатель this в поле GWLJJSERDATA, как в классе KWindow, поскольку субклассируемое окно может быть создано другой стороной, использующей поле GWLJJSERDATA. He годятся и глобальные переменные, поскольку мы хотим использовать наш класс для одновременной поддержки нескольких окон, но при этом обойтись без создания глобальных диспетчерских таблиц, как в MFC. Класс KBackground решает общую задачу субклассирования и перехвата сообщения WM_ERASEBKGND. Однако существуют различные варианты прорисовки фона — линиями, образующими решетчатый узор, заливкой замкнутых областей, функциями вывода DIB или DDB. Эти варианты реализуются в специализированных классах, производных от родового класса KBackground. Реализация, ориентированная на вывод DDB, приведена в листинге 10.8. Листинг 10.8. Класс для прорисовки фона выводом DDB class KDDBBackground : public KBackground { KDDB m_DDB; int mjiStyle: virtual LRESULT EraseBackgroundCHWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); public: KDDBBackgroundO mjiStyle = KDDB::draw_tile; void SetStyleCint style) mjnStyle = style; void SetBitmapCHMODULE hModule. int nRes) m_DDB.LoadBitmap(hModule, nRes); }: LRESULT KDDBBackground::EraseBackgroundCHWND hWnd. UINT uMsg, WPARAM wParam, LPARAM 1 Param) { Продолжение &
592 Глава 10. Основные сведения о растрах Листинг 10.8. Продолжение if ( m_DDB.GetBitmap() ) { RECT rect; HDC hDC - (HDC) wParam; GetClientRectChWnd. & rect); HRGN hRgn = CreateRectRgnIndirect(&rect); SaveDC(hDC); SelectClipRgn(hDC, hRgn); DeleteObject(hRgn); m_DDB.Draw(hDC. rect.left. rect.top. rect.right - rect.left. rect.bottom - rect.top. SRCCOPY. mjiStyle): RestoreDC(hDC. -1): return 1; // Обработано } else return DefWindowProc(hWnd. uMsg. wParam. IParam); } Для загрузки и вывода DDB класс KDDBBackground использует класс KDDB. Он переопределяет метод EraseBackground и реализует в нем вывод фона. Использовать этот класс несложно. Все, что от вас потребуется, — создать экземпляр класса KDDBBackground, задать растр и стиль вывода, а затем субклассировать окно вызовом метода Attach. На рис. 10.6 показаны результаты субклассирования для диалогового окна, группирующей рамки и статической рамки (frame). Диалоговбе окно заполняется деревянной текстурой, в группирующей рамке кирпичная текстура выравнивается по центру, а в статической рамке та же текстура подвергается пропорциональному масштабированию. А самое замечательное — то, что для каждого окна задача решается всего тремя строками кода при обработке сообщения WMJNITDIAL0G. // KDDBBackground whole; // KDDBBackground groupbox; // KDDBBackground frame; whole.SetBitmap(m_hInstance. IDB_PAPER01); whole.SetStyle(KDDB::draw_tile): whole.Attach(hwnd); groupbox.SetBitmap(m_hInstance. IDB_BRICK02); groupbox.SetStyleCKDDB::draw_center); groupbox.Attach(GetDlgItem(hWnd. IDC_GR0UPB0X)); frame.SetBitmap(m_hInstance. IDB_BRICK02); frame.SetStyle(KDDB::draw_stretchprop); frame.Attach(GetDlgItem(hWnd. IDCJRAME));
Использование DDB-растров 593 Рис. 10.6. Использование класса KDDBBackground в диалоговом окне DIB-секции Мы рассмотрели два основных растровых формата, поддерживаемых в GDI: ап- паратно-независимые растры (DIB) и аппаратно-зависимые растры (DDB). DIB- растры могут существовать в различных стандартных цветовых форматах, выбор которых зависит от ситуации. DDB-растры по практическим соображениям либо являются монохромными, либо их цветовой формат совпадает с форматом устройства. Средства GDI позволяют выводить как DIB, так и DDB, однако GDI поддерживает рисование только на DDB-растре, выбранном в совместимом контексте устройства, и не поддерживает рисование на DIB. DIB-растры хороши тем, что их хранение организуется на уровне приложения, поэтому приложение может напрямую работать с цветовой таблицей и массивом пикселов, однако рассчитывать на помощь GDI в создании DIB не приходится. Преимущества DDB-растров — в том, что в них можно рисовать средствами GDI; с другой стороны, вы не имеете прямого доступа к внутреннему представлению DDB, поскольку оно находится под управлением GDI. Поскольку DDB-растры хранятся в системной памяти, существуют ограничения как для максимального размера одного DDB-растра, так и для общего размера всех DDB-растров в системе. С другой стороны, хранение DIB организуется приложением, поэтому размер DIB ограничивается только объемом виртуального адресного пространства процесса и свободным пространством на диске, выделенным для системного файла подкачки. Возникает естественный вопрос: существует ли тип растров, обладающий всеми достоинствами DIB и DDB? Да, существует. Это новый тип растров, поддерживаемый в Win32 GDI API — DIB-секции (DIB sections). Термин «DIB-секция» выглядит довольно странно. Вероятно, программист, работавший над реализацией, не удосужился подобрать нормальное имя, а специалисты по подготовке документации вообще не представляли, о чем идет речь. В документации Microsoft DIB-секция определяется как DIB-растр, в который приложение может напрямую записывать данные. Но приложения еще со времен Windows 3.1 напрямую записывают данные в DIB и без DIB-секций. Также
594 Глава 10. Основные сведения о растрах в документации утверждается, будто DIB-секция является частью DIB, но на самом деле DIB-секция всегда содержит полный DIB-растр. Во избежание дальнейших недоразумений стоит привести нормальное определение. DIB-секцией называется DIB-растр, обеспечивающий непосредственное чтение/запись со стороны как приложения, так и GDI. Вы спросите, при чем здесь «секция»? Дело в том, что массив пикселов DIB-секции может храниться в файле, отображаемом на память, который в среде разработчиков операционной системы Windows называется «секцией». Даже если DIB-секция и не находится в файле, отображаемом на память, ее массив пикселов хранится в виртуальной памяти, которая может выгружаться в системный файл подкачки. Вероятно, DIB- секции правильнее было бы назвать «DIB-растрами с двойным доступом» (dual access DIB). DIB-секция, как и аппаратно-зависимый растр, является объектом GDI. При создании DIB-секции GDI возвращает манипулятор объекта DIB-секции, относящийся к знакомому типу HBITMAP. Но в отличие от DDB, GDI также возвращает адрес массива пикселов DIB-секции, чтобы приложение могло напрямую работать с графическими данными. Завершив работу с DIB-секцией, приложение должно вызвать функцию DeleteObject, чтобы освободить связанные с ней ресурсы. При работе с DIB-секциями используются те же функции API, как и при работе с DDB, поэтому для поддержки DIB-секций на уровне API появились всего три новые функции: HBITMAP CreateDIBSection(HDC hDC, CONST BITMAPINFO *pbmi, UINT iUsage. PVOID * ppvBits, HANDLE hSection, DWORD dwOffset); UINT GetDIBColorTableCHDC hDC, UINT uStartlndex. UINT cEntries. RGBQUAD * pColors): UINT SetDIBColorTableCHDC hDC. UINT uStartlndex, UINT cEntries, CONST RGBQUAD * pColors): typedef struct tabDIBSection { BITMAP dsBm: BITMAPINFOHEADER dsBmih; DWORD dsBitfields[3]; HANDLE dshSection; DWORD dsOffset; } DIBSECTION; CreateDIBSection Функция CreateDIBSection создает объект DIB-секции. Из параметров этой функции самыми важными являются первые три. В первом параметре передается указатель на эталонный контекст устройства. Параметр pbmi указывает на структуру BITMAPINFO, содержащую манипулятор блока описания растра, битовые маски и цветовую таблицу. Параметр i Usage сообщает, содержит ли цветовая таблица значения в формате RGB или индексы палитры. Если значение равно DIB_PAL_ COLORS, используется логическая палитра, в данный момент выбранная в hdc. Итак, первые три параметра полностью определяют размеры, формат пикселов и цветовую таблицу DIB-секции. Четвертый параметр, ppvBits, содержит адрес пере- ш шюй-указателя, в которую GDI заносит адрес массива пикселов DIB-секции.
Использование DDB-растров 595 Два последних параметра обеспечивают выделение памяти и инициализацию массива пикселов при помощи блока из объекта файла, отображаемого на память. Параметр hSection содержит манипулятор объекта файла, отображаемого на память, полученный при вызове CreateFileMapping. Обратите внимание на имя параметра: как говорилось выше, объект файла, отображаемого на память, также называется «объектом секции». Вероятно, это и стало одной из причин появления странного термина «DIB-секция». В параметре dwOffset передается смещение массива пикселов внутри отображаемого файла. Функция CreateDIBSection возвращает два значения — манипулятор объекта DIB-секции (возвращаемое значение функции) и указатель на массив пикселов (параметр ppvBits). Хотя параметры функции CreateDIBSection выглядят довольно сложно, основное внимание в приложениях обычно уделяется второму параметру — указателю на структуру BITMAPINFO. Иначе говоря, для создания DIB-секции вы должны указать ширину, высоту, количество бит/пиксел, тип сжатия, битовые маски и цветовую таблицу. GDI не поддерживает для DIB-секций все допустимые форматы DIB, поскольку DIB-секция должна быть доступна как для чтения, так и для записи (вывода). По этой причине GDI поддерживает для DIB-секций лишь формат DIB без сжатия. Невозможно создать DIB-секцию с типом сжатия BIJU.E4, BI__RLE8, BI__PNG или BIJPEG. Для управления DIB-секцией GDI выделяет блок памяти, в котором хранятся заголовок блока описания растра, битовые маски и цветовая таблица. Эти данные находятся под управлением GDI, и приложение не может работать с ними напрямую. Разумеется, GDI резервирует в таблице объектов GDI элемент, связывающий внутреннюю структуру данных GDI с DIB-секцией. Между манипулятором объекта GDI и записью таблицы объектов GDI существует однозначное соответствие. В этом отношении DIB-секции похожи на DDB-растры, но отличаются от DIB-растров, которые не находятся под управлением GDI. Если DIB-секция создается не в объекте файла, отображаемого на память, GDI выделяет память под массив пикселов из виртуальной памяти приложения и возвращает указатель на нее вызывающей стороне. Обратите внимание на различия в схемах выделения памяти для DDB-растров и DIB-секций. В системах семейства Windows 9x память для массива пикселов DDB выделяется из кучи GDI, а в системах семейства Windows NT — из выгружаемого пула режима ядра. В обоих случаях используются общесистемные ограниченные ресурсы и приложение не имеет прямого доступа к массиву пикселов. С другой стороны, массив пикселов DIB-секции создается в виртуальном пространстве памяти текущего приложения, объем которого ограничивается только объемом виртуальной памяти приложений и свободным местом на диске, причем прикладные программы могут напрямую обращаться к этой памяти. Пикселы в выделенном массиве находятся в неопределенном состоянии, как в неинициализированном DDB- растре. Также следует обратить внимание на то, что память выделяется из виртуального пространства приложения, а не из системной кучи. Хотя системная куча создается в виртуальном адресном пространстве, при работе с ней используется механизм вторичного выделения памяти, повышающий эффективность создания большого количества мелких объектов. Память в виртуальном пространстве выделяется блоками, размер которых кратен размеру страницы; на процессорах
596 Глава 10. Основные сведения о растрах Intel эта величина равна 4 Кбайт. Как показали эксперименты, GDI выделяет память для DIB-секций 64-килобайтными блоками. При передаче действительного манипулятора объекта файла, отображаемого на память, параметр dwOffset должен быть кратен DWORD. По данным структуры BITMAPINFO GDI может вычислить размер массива пикселов. Зная манипулятор объекта отображаемого файла, смещение и длину, GDI может вызвать функцию MapViewOfFIle для отображения блока данных файла на виртуальное пространство приложения. Если данные в файле соответствуют формату массива пикселов, DIB-секция полностью инициализируется без выделения памяти под массив пикселов и копирования данных, связанного с потенциальными затратами. Напрашивается предположение, что DIB-секцию можно создать на основе BMP-файла, отображенного на память. К сожалению, данная возможность не поддерживается, поскольку функции CreateDIBSection должен передаваться указатель на блок памяти, выровненный по границе DWORD. Размер структуры BITMAPFILEHEADER равен 14 байтам, а размер структуры BITMAPINFO всегда кратен DWORD; таким образом, массив пикселов в BMP-файле не всегда выровнен по границе DWORD. Если формат файла, отображенного на память, не соответствует формату массива пикселов, последние два параметра всего лишь обеспечивают альтернативное средство управления памятью. Зачем Microsoft предоставляет такую возможность? Ведь каждый байт памяти все равно хранится на диске — если не в файле, указанном приложением, то в системном файле подкачки? Передавая функции CreateDIBSection объект файла, отображаемого на память, приложение может указать, где должен храниться этот файл и допускается ли его совместное использование несколькими процессами. Допустим, на вашем компьютере системный файл подкачки хранится на жестком диске С:, на котором имеется всего 100 Мбайт свободного места. Графический редактор обрабатывает изображение с 32-разрядным цветом, разрешением 600 dpi и размером в полную страницу; для этого он должен создать 128-мегабайтную DIB-секцию. Если редактор достаточно сообразителен, он увидит, что на жестком диске D: имеется 500 Мбайт свободного места, поэтому отображаемый файл следует создать на диске D: и передать его при вызове CreateDIBSection. Теперь редактор справится с четырьмя большими изображениями. Класс для работы с DIB-секциями Для удобства работы с DIB-секциями их стоит оформить в виде отдельного класса. К счастью, большую часть кода можно позаимствовать из классов KDIB и KDDB (более того, наш класс DIB-секции будет создан как производный от этих классов). Класс для работы с DIB-секциями приведен в листинге 10.9. Листинг 10.9. Класс для работы с DIB-секциями class KDIBSection : public KDIB, public KDDB { public: KDIBSectionO {
Использование DDB-растров 597 } virtual -KDIBSectionO { } BOOL CreateDIBSection(HDC hDC, CONST BITMAPINFO * pBMI. UINT iUsage, HANDLE hSection. DWORD dwOffset); UINT GetColorTable(void); UINT SetColorTable(void); void DecodeDIBSectionFormatCTCHAR desp[]); void KDIBSection::DecodeDIBSectionFormat(TCHAR desp[]) { DIBSECTION dibsec; if ( GetObjectCmJiBitmap. sizeof(DIBSECTION). & dibsec) ) { KDIB::DecodeDIBFormat(desp); _tcscat(desp. _T(" ")); DecodeDDB(GetBitmap(). desp + _tcslen(desp)); } else _tcscpy(desp. _T("Invalid DIB Section")); } BOOL KDIBSection::CreateDIBSection(HDC hDC. CONST BITMAPINFO * pBMI. UINT iUsage. HANDLE hSection. DWORD dwOffset) { PVOID pBits = NULL; HBITMAP hBmp - ::CreateDIBSection(hDC. pBMI. iUsage. & pBits. hSection. dwOffset); if ( hBmp ) { ReleaseDDBO; // Освободить предыдущий объект ReleaseDIBO; mJiBitmap = hBmp; int nColor = GetDIBColorCount(pBMI->bmiHeader); int nSize = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColor; BITMAPINFO * pDIB = (BITMAPINFO *) new BYTE[nS1ze]: if ( pDIB==NULL ) return FALSE; memcpy(pDIB. pBMI. nSize); // Скопировать заголовок // и цветовую таблицу AttachDIB(pDIB. (PBYTE) pBits. DIB_BMI_NEEDFREE); Продолжение^
598 Глава 10. Основные сведения о растрах Листинг 10.9. Продолжение GetColorTableO: return TRUE; } else return FALSE; } Класс KDIBSection не содержит ни одной собственной переменной, поскольку он использует переменные классов KDDB и KDIB. Экземпляр класса KDIBSection обладает возможностями как экземпляра класса KDDB, так и экземпляра KDIB. Таким образом, в инициализированной DIB-секции можно рисовать средствами GDI, используя методы класса KDDB, и напрямую работать с ее массивом пикселов методами класса KDIB. Основной код этого класса сосредоточен в функции CreateDIBSection, создающей DIB-секцию. Эта функция вызывает одноименную функцию GDI. Если вызов был успешным, функция копирует структуру BITMAP- INFO и заполняет новую цветовую таблицу; затем вызывается функция DIB:: AttachDIB, инициализирующая переменные класса KDIB. Обратите внимание — мы ограничиваемся освобождением новой структуры BITMAPINFO; деструктор класса KDDB вызывает DeleteObject с манипулятором DIB-секции, что приводит к освобождению массива пикселов, выделенного GDI. Функции GetObjectType и GetObject для DIB-секции DDB-растры и DIB-секции относятся к общей категории объектов GDI, но между ними, конечно, существуют принципиальные различия. Располагая только манипулятором объекта GDI, трудно сказать, к чему он относится — к DDB- растру или DIB-секции. Для DIB-секций функция GetObjectType возвращает то же значение 0BJBITMAP (7); функция GetObject (hBitmap, О, NULL) всегда возвращает sizeof (BITMAP), а функция GetObject (hBitmap, sizeof (BITMAP), &bmp) всегда завершается успешно и заполняет структуру BITMAP. DIB-секцию можно отличить от DDB двумя способами. Во-первых, структура BITMAP, возвращаемая GetObject, содержит действительный указатель на массив пикселов. Как говорилось выше, для DDB адрес массива пикселов не передается приложению, поэтому поле bmBits всегда равно NULL. Для DIB-секций это поле совпадает со значением, возвращаемым CreateDIBSection в параметре ppvBits. Во-вторых, GetObject может возвращать структуру DIBSection, если параметр cbBuffer равен sizeof(DISBSECTI0N), а размер буфера, на который указывает lpvObjects, достаточен для хранения структуры DIBSection. Структура DIBSection содержит информацию о DIB-секции, которую GDI предоставляет приложениям. В первом поле хранится структура BITMAP, которая описывает DIB-секцию со стороны DDB. Во втором поле хранится структура BITMAPINFOHEADER, описывающая размеры и цветовой формат DIB. Помните, что вместо нее также может использоваться структура BITMAPV4HEADER или BITMAPV5HEADER. Вероятно, на уровне GDI следовало бы определить структуры DIBSECTI0NV4 и DIB- SECTI0NV5. Поле dsBitFields содержит массив из трех битовых масок, используемых в 16- и 32-разрядных режимах (в режимах BIJRGB и BI_BITFIELDS). По ним
Использование DDB-растров 599 можно определить текущий формат пикселов в режиме с кодировкой 16 бит/пиксел. Если вы создаете 16-разрядную DIB-секцию в формате BIRGB, проверьте поле dsBitFields структуры DIBSection, и вы узнаете, какой формат пикселов используется — 5-5-5 или 5-6-5. Два последних поля предназначены для создания DIB- секций на базе объекта файла, отображаемого на память. Их значения совпадают с параметрами, передаваемыми при вызове CreateDIBSection. GetDIBColorTable и SetDIBColorTable DIB-секция не является полноценным DIB-растром, поскольку приложение не имеет прямого доступа к цветовой таблице, как при работе с цветовой таблицей DIB. Цветовая таблица DIB-секции находится под управлением GDI, и приложение работает с ней только через функции GetDIBColorTable/SetDIBColorTablе. Напрашивается вопрос: зачем приложению обращаться к цветовой таблице DIB-секции, если оно уже предоставило ее при создании DIB-секции функцией CreateDIBSection? Существует минимум две веские причины. Во-первых, если при вызове функции CreateDIBSection параметр i Usage был равен DIBPALC0L0RS, приложение работает только с индексами логической палитры, а не с цветовой таблицей RGB. Если приложение захочет сохранить DIB-секцию в ВМР-файле, ему понадобится нормальная цветовая таблица RGB. Во-вторых, многие графические алгоритмы (например, регулировка оттенка, яркости и насыщенности растра или преобразование его к оттенкам серого) реализуются операциями с цветовой таблицей изображений. Для этого приложение выполняет необходимые манипуляции с цветовой таблицей RGB и возвращает ее новое состояние. Функция GetDIBColorTable возвращает цветовую таблицу RGB для DIB-секции. В первом параметре функции передается совместимый контекст устройства, в котором должна быть выбрана DIB-секция. Параметры uStartlndex и cEntries сообщают начальную позицию и количество копируемых элементов. Параметр pColors указывает на буфер для записи цветовой таблицы в виде массива структур RGBQUAD. Возвращаемое значение функции определяет количество скопированных элементов; 0 является признаком ошибки. Функция SetDIBColor заполняет цветовую таблицу DIB-секции данными из таблицы, предоставленной приложением. Она вызывается с теми же параметрами и возвращает количество скопированных элементов. Ниже приведены соответствующие функции класса KDIBSection. Обратите внимание: мы используем тот же совместимый контекст устройства, который использовался при выводе растра, а цветовая таблица DIB-секции хранится в переменной KDIB: :m_pRGBQUAD. // Копирование цветовой таблицы DIB-секции в цветовую таблицу DIB UINT KDIBSection::GetColorTable(void) { int width, height; if ( (GetDepth()>8) || ! Prepare(width, height) ) // Создать совместимый // контекст устройства return 0: return GetDIBColorTable(m_hMemDC, 0, mjiClrUsed. m_pRGBQUAD); }
600 Глава 10. Основные сведения о растрах // Копирование цветовой таблицы DIB в цветовую таблицу DIB-секции UINT KDIBSection::SetColorTable(void) { int width, height; if ( (GetDepth()>8) || ! PrepareCwidth, height) ) // Создать совместимый // контекст устройства return 0; return SetDIBColorTable(m_hMemDC. 0. mjiClrUsed. m_pRGBQUAD); } I Применение DIB-секций: аппаратно- независимый вывод DIB-секции обладают рядом преимуществ по сравнению с DIB и DDB. О Аппаратно-зависимый вывод средствами GDI. GDI не оказывает особой помощи в построении DIB. Для DDB поддерживается всего один цветовой формат, совместимый с текущим режимом экрана. Если графическое приложение хочет реализовать 24-разрядный вывод в экранном режиме с кодировкой 8 бит/пиксел средствами GDI, это можно сделать только с использованием DIB-секции. О Объединение вывода средствами GDI с прямым доступом к массиву пикселов. Только DIB-секции поддерживают одновременный вывод средствами GDI с прямым доступом к массиву пикселов в приложениях. Без DIB-секций вам придется создавать DIBhDDBh передавать пикселы между ними функциями GetDIBits и SetDIBits. О Гибкая схема управления памятью. DDB-растры создаются в системной памяти, а память DIB-секций выделяется в виртуальном адресном пространстве приложения или в файле, отображаемом на память. Размер DIB-секции ограничивается только объемом виртуального адресного пространства и свободным пространством на диске. Например, если позволяет место на диске, вы можете создать DIB-секцию 8192 х 8192 с 32-разрядной кодировкой цвета объемом 256 Мбайт. Создать DDB такого размера невозможно, поскольку в системах семейства NT максимальный размер DDB равен 48 Мбайт, а в системах семейства Windows 95—16 Мбайт. DIB-секции также позволяют создать большее количество растров одновременно. Управление памятью для DIB отличается большей гибкостью. Например, DIB-растры могут находиться в секции ресурсов исполняемого файла, доступной только для чтения. Давайте рассмотрим реализацию аппаратно-независимого вывода на конкретном примере и вернемся к примеру с сохранением экрана. На этот раз мы хотим сохранить содержимое окна в 24-разрядном DIB-растре. Конечно, содержимое окна можно сохранить в DDB-растре, а затем преобразовать его в 24-разрядный DIB-растр, но мы хотим сделать кое-что еще — а именно, нарисовать объемную рамку и снабдить изображение подписью. При работе с DDB в экранном режиме с 8-разрядным цветом сделать это было бы очень сложно — плавные переходы цветов объемной рамки плохо представляются в 8-разрядном DDB-растре.
Использование DDB-растров 601 С другой стороны, если вы решите создать рамку в 24-разрядном DIB-растре, GDI вам в этом не поможет. С другой стороны, можно обойтись одной 24-разрядной DIB-секцией. Весь вывод будет осуществляться средствами GDI, а потом полученное изображение можно будет сохранить в ВМР-файле. Функции SaveWindow (сохранение содержимого окна) и Frame3D (рисование объемной рамки) приведены в листинге 10.10. Листинг 10.10. Сохранение окна и построение рамки в DIB-секции BOOL SaveWindow(HWND hWnd. boo! bClient. int nFrame, COLORREF crFrame) { RECT wnd; if ( bClient ) { if ( ! GetClientRect(hWnd. & wnd) ) return FALSE; } else { if ( ! GetWindowRect(hWnd. & wnd) ) return FALSE; } KBitmapInfo bmi; KDIBSection dibsec; bmi.SetFormaUwnd.right - wnd.left + nFrame * 2. wnd.bottom - wnd.top + nFrame *2. 24, BI_RGB); if ( dibsec.CreateDIBSection(NULL. bmi.GetBMIO. DIB_RGB_C0L0RS. NULL, NULL) ) { int width, height; dibsec.Prepare(width, height); // Создать совместимый // контекст устройства, // выбрать в нем dibsec if ( nFrame ) { Frame(dibsec.mJiMemDC. nFrame, crFrame. 0, 0, width, height); TCHAR Tit!e[128]; GetWindowText(hWnd. Title. sizeof(Title)/sizeof(Title[0])): SetBkMode(dibsec.m_hMemDC. TRANSPARENT); SetTextColor(dibsec.m_hMemDC. RGB(0xFF. OxFF. OxFF)); TextOutCdibsec.mJiMemDC. nFrame, (nFrame-20)/2. Title. _tcslen(Title)); } HDC hDC; if ( bClient ) hDC = GetDC(hWnd);
602 Глава 10. Основные сведения о растрах Листинг 10.10. Продолжение else hDC - GetWindowDC(hWnd); // Скопировать содержимое экрана в DIB-секцию BitBlt(dibsec.m_hMemDC, nFrame, nFrame. width height - nFrame * 2. hDC. 0. 0. SRCCOPY); ReleaseDCChWnd. hDC): nFrame return dibsec.SaveFile(NULL); } return FALSE; void Frame3D(HDC hDC. int nFrame. COLORREF crFrame. int left, int top. int right, int bottom) { int red = GetRValue(crFrame); int green = GetGValue(crFrame); int blue = GetBValue(crFrame); RECT rect = { left. top. right, bottom }; for (int i=0; i<nFrame; i++) HBRUSH hBrush - CreateSolidBrush(RGB(red. green, blue)); FrameRect(hDC. & rect, hBrush); // Один пиксел DeleteObject(hBrush); if ( i<nFrame/2 ) { red - red * 19/20 green = green * 19/20 blue - blue * 19/20 // Первая половина // Темнее else red = red green = green blue = blue 19/18 19/18 19/18 // Вторая половина // Светлее if ( red>255 ) red = 255 if ( green>255 ) green - 255 if ( blue>255) blue - 255 InflateRect (&rect. -1. -1); // Меньше Функция SaveWi ndow получает четыре параметра. Параметр hWnd содержит манипулятор окна, параметр bClient определяет сохраняемую часть (все окно или клиентская область), параметр nFrame содержит толщину добавляемой рамки, а параметр crFrame определяет цвет рамки. Функция готовит структуру BITMAPINFO для 24-разрядной DIB-секции, используя несложный класс KBitmapInfo, предназначенный для инициализации BITMAPINFO. Экземпляр KDIBSection создается в стеке. После создания DIB-секции функция SaveWi ndow создает совместимый контекст и выбирает в нем DIB-секцию. Теперь можно вызвать функцию Frame3D для рисования рамки, вывести надпись с текстом из заголовка окна и, наконец,
Использование DDB-растров 603 скопировать пикселы из экранного контекста устройства в центр DIB-секции. Затем программа сохраняет DIB-секцию в BMP-файле вызовом метода KDIB:: SaveFile. Объемная рамка строится из прямоугольников высотой в один пиксел, цвет которых постепенно темнеет, а начиная с середины рамки — светлеет, что создает иллюзию полукруглой рамки. Пример изображен на рис. 10.7. Рис. 10.7. Сохраненная клиентская область в рамке Приведенный пример показывает, как использовать GDI для вывода в DIB- секции. Прямой доступ к массиву пикселов организуется так же, как это делается в DIB. Дополнительная информация приведена в следующей главе. Применение DIB-секции: вывод в высоком разрешении Выделение памяти для хранения DIB-секций в адресном пространстве пользовательского режима позволяет работать с изображениями значительно больших размеров, чем при использовании DDB. Возможность создания DIB-секций на базе манипулятора файла, отображаемого на память, упрощает контроль над размещением данных на диске и в памяти. Эти две особенности часто используются в графических редакторах и программных процессорах растровых изображений (Raster Image Processor, RIP). Процессор растровых изображений получает документ, написанный на языке описания страниц, и преобразует его в растровое изображение высокого разрешения, который после полутоновой обработки передается на принтер высокого разрешения. Например, программу Ghostscript можно рассматривать как программный процессор RIP — Ghost- script получает документ в формате PostScript и воспроизводит его в растровом виде в разных вариантах разрешения. В этом разделе мы напишем несложный программный процессор RIP, работа которого основана на использовании DIB-секций. Конечно, документы PostScript слишком сложны для подобных примеров, но мы прибегнем к помощи GDI
604 Глава 10. Основные сведения о растрах и воспользуемся другим выразительным, полезным и общедоступным средством — расширенными метафайлами Windows (EMF). Наша задача — создать файл, отображаемый в память, на базе которого будет создана DIB-секция высокого разрешения, и затем воспроизвести EMF-файл в этой DIB-секции. Завершив воспроизведение, мы удаляем DIB-секцию, закрываем файл и получаем растровое представление EMF-файла в высоком разрешении. Основные трудности связаны с тем, что функция CreateDIBSection требует, чтобы смещение массива пикселов было кратно DWORD. BMP-файлы не удовлетворяют этому требованию, поскольку их массивы пикселов никогда не начинаются на границе DWORD. Мы пойдем обходным путем и воспользуемся 24-разрядным графическим форматом Targa, который содержит очень простой заголовок регулируемой длины и поддерживает несжатые массивы пикселов RGB в 24-разрядном формате. В листинге 10.11 приведен класс КТагда24, предназначенный для работы с DIB-секциями в 24-разрядном графическом формате Targa. Листинг 10.11. Работа с DIB-секциями с использованием файлов, отображаемых на память class KTarga24 : public KDIBSection { #pragma pack(push.l) typedef struct { BYTE IDLength; BYTE CoMapType; BYTE ImgType; WORD Index; WORD Length; BYTE CoSize; WORD X_0rg; WORD Y_0rg; WORD Width: WORD Height; BYTE Pixel Size; BYTE AttBits; char ID[14]; } ImageHeader; #pragma pack(pop) HANDLE m_hFile HANDLE mJiFileMapping; public: KTarga24() { mJiFile = INVALID_HANDLE_VALUE; mJiFileMapping - INVALID_HANDLE_VALUE; }: virtual ~KTarga24() { // 00: Длина строки-идентификатора //01 0 = таблица отсутствует //02 2 = TGA_RGB // 03 индекс первого элемента цветовой таблицы // 05 количество элементов в цветовой таблице // 07 размер элемента цветовой таблицы // 08 О // 0А О // ОС ширина // 0Е высота // 10 размер пиксела // 11 0 // 12 заполнитель, обеспечивающий // выравнивание ImageHeader по границе DWORD
Использование DDB-растров 605 ReleaseDDBO; ReleaseDIBO; if ( m_hFileMapping!=INVALID_HANDLE_VALUE ) CI oseHandle(m_hFi1eMappi ng); if ( mJiFile !« INVALID_HANDLE_VALUE ) CloseHandle(m_hFile); }: BOOL Create(int width, int height, const TCHAR * filename); BOOL KTarga24::Create(int width, int height, const TCHAR * pFileName) { if ( width & 3 ) // Обойти проблемы совместимости с TGA return FALSE; ImageHeader tgaheader; memset(& tgaheader. 0. sizeof(tgaheader)); tgaheader.IDLength = si zeof(tgaheader.ID); tgaheader.ImgType = 2: tgaheader.Width = width; tgaheader.Height = height; tgaheader.Pixel Size = 24; strcpy(tgaheader.ID. "BitmapShop"); m_hFile = CreateFile(pFileName. GENERIC_WRITE | GENERIC_READ. FILE_SHARE_READ | FILE_SHARE_WRITE. NULL. CREATE_ALWAYS. FILE_ATTRIBUTE_NORMAL. NULL); if ( m_hFile==INVALID_HANDLE_VALUE ) return FALSE; int imagesize = (width*3+3)/4*4 * height; m_hFileMapping = CreateFileMapping(m_hFile. NULL. PAGE_READWRITE. 0. sizeof(tgaheader) + imagesize, NULL); if ( m_hFileMapping==INVALID_HANDLE_VALUE ) return FALSE; DWORD dwWritten = NULL; WriteFile(m_hFile. & tgaheader. sizeof(tgaheader). &dwWritten. NULL); SetFilePointer(m_hFile. sizeof(tgaheader) + imagesize. 0. FILE_BEGIN); SetEndOfFile(m_hFile); KBitmapInfo bmi: bmi.SetFormat(width, height. 24. BI_RGB); return CreateDIBSection(NULL. bmi.GetBMIO. DIB_RGB_C0L0RS. m_hFi1eMappi ng. si zeof(tgaheader));
606 Глава 10. Основные сведения о растрах Класс КТагда24 определен как производный от класса KDIBSection. Он содержит две новые переменные для хранения манипуляторов файла и файлового отображения. Главным методом класса является метод Create, при вызове которого передается ширина, высота и имя файла. Функция создает объекты файла и файлового отображения, заполняет 32-байтовый заголовок графического формата Targa и создает DIB-секцию с использованием объекта файлового отображения. Поскольку длина заголовка равна 32 байтам, смещение массива пикселов равно 32 — величине, кратной DWORD. Обратите внимание: файл должен быть создан с общим доступом для чтения и записи, и файловое отображение также должно иметь права доступа для чтения и записи; в противном случае попытки создания DIB-секции или вывода в файл завершатся неудачей. Объекты файла и файлового отображения закрываются в деструкторе после удаления объекта DIB-секции (в ReleaseDDB). Расширенные метафайлы (EMF) описаны в главе 16. Пока достаточно запомнить, что EMF-файл представляет собой записанную последовательность команд GDI, которая легко обрабатывается и воспроизводится. Приведенная ниже функция использует класс КТагда24 для воспроизведения EMF-файла. BOOL RenderEMFCHENHHETAFILE hemf. int width, int height, const TCHAR * tgaFileName) { KTarga24 targa; int w = (width+3)/4*4; // Убедиться, что значение кратно 4 if ( targa.Create(w. height, tgaFileName) ) { targa.Prepare(w, height); BitBlt(targa.mJiMemDC. 0. 0, width, height, NULL. 0. 0. WHITENESS); // Очистить DIB-секцию RECT rect = { 0. 0. width, height }; return PlayEnhMetaFile(targa.m_hMemDC. hemf. &rect); } return FALSE; } Функция RenderEMF получает манипулятор EMF, ширину и высоту воспроизводимого изображения и имя графического файла. Она создает экземпляр класса КТагда24 в стеке, инициализирует его, заполняет DIB-секцию белым цветом и вызывает функцию PlayEnhMetaFile для воспроизведения объекта EMF в DIB- секции. Остается написать интерфейсный код для выбора входного EMF-файла, имени выходного файла Targa и масштаба воспроизведения. EMF-файлы воспроизводятся пропорционально с одинаковым масштабом по осям х и у. Для проверки мы воспользовались 600-килобайтным EMF-файлом, содержащим сложный рисунок, в масштабе 900 %. Исходный размер изображения составлял 625 х 777 пикселов; в масштабе 900 % он достиг 5625 х 6993 пикселов. DIB-секция с 24-разрядной кодировкой цвета занимает 112 Мбайт. По размерам изображения это задание печати близко к полностраничному документу с разрешением 600 dpi.
Итоги 607 Итоги В этой главе рассматривались три типа растров, поддерживаемых в GDI, — ап- паратно-независимые растры (DIB), аппаратно-зависимые растры (DDB) и DIB- секции. Основное внимание уделялось вопросам создания, преобразования, отображения и простейшего применения этих типов растров, а также различиям между ними. Растры — настолько серьезная тема, что ее описание разделено на три главы. В главе 11 рассматриваются нетривиальные и интересные аспекты применения растров: растровые операции, прозрачность, прямой доступ к пикселам и альфа- наложение. Глава 12 посвящена обработке изображения посредством прямого доступа к пикселам. Кроме того, в главе 17 рассматривается декодирование и печать изображений в формате JPEG. Примеры программ В этой главе рассматриваются два примера программ (табл. 10.6). Важно то, что работа этих двух программ основана на нескольких полезных классах, которые в усовершенствованном виде будут использоваться в других главах, посвященных растровым изображениям. Таблица 10.6. Программы главы 10 Каталог проекта Описание Samples\Chapt_10\Bitmaps Демонстрация загрузки и сохранения ВМР-файлов, сохранения экрана, различных способов отображения растров, применения растровых меток и команд меню, растровых фонов, DIB-секций, аппаратно-независимо- го вывода с использованием DIB-секций и вывода растров в шсстнадцатеричном формате Samples\Chapt_10\Scrambler Применение функции StretchBlt для случайной перестановки фрагментов экрана
Глава 11 Нетривиальное использование растров Растры играют чрезвычайно важную роль в программировании для Windows, поэтому эту тему невозможно охватить в одной главе. В предыдущей главе описаны три типа растров, поддерживаемых в GDI: аппаратно-независимые растры (DIB), аппаратно-зависимые растры (DDB) и DIB-секции. Мы рассмотрели основные принципы работы с растрами, в том числе различные способы их отображения, применение растровых изображений в пользовательском интерфейсе и даже программную реализацию вывода с высоким разрешением. Однако предыдущая глава далеко не исчерпывает темы растровых изображений. В этой главе мы будем изучать растровые операции, прозрачность и альфа- наложение. Глава 12 посвящена работе с растровыми изображениями на уровне прямого доступа к пикселам, а палитры рассматриваются в главе 13. Тернарные растровые операции При рисовании линий или заливке областей GDI использует бинарные растровые операции, которые определяют способ объединения пиксела пера или кисти с пикселом приемника и получения нового пиксела приемника. В GDI поддерживается шестнадцать бинарных растровых операций, для работы с ними используется пара функций SetR0P2 и GetR0P2. Вполне логично предположить, что при работе с растрами существуют похожие растровые операции, позволяющие создавать всевозможные специальные эффекты. При этом возникает новый фактор — пикселы изображения-источника. Таким образом, при работе с растрами приходится учитывать три фактора: цвет пиксела пера или кисти, цвет пиксела приемника и цвет пиксела источни-
Тернарные растровые операции 609 ка. Растровые операции, объединяющие эти три цвета, обычно называются тернарными растровыми операциями. В GDI поддерживаются только поразрядные логические операции, которые выполняются с каждым битом пиксела независимо от других пикселов и ограничиваются булевыми операциями AND (&), OR (|), NOT (~) и XOR (А). При таких ограничениях существует 256 (2А(2А3), или 28) возможных тернарных растровых операций. Коды растровых операций Для представления 256 разных растровых операций достаточно одного байта. Учитывая, что каждый бит интерпретируется независимо от остальных, механизм кодировки выглядит весьма простым. Предположим, Р — бит пера или кисти, S — бит источника, a D — бит приемника. Если результат растровой операции всегда равен Р, операции присваивается код OxFO. Если результат операции всегда совпадает с S, код равен ОхСС, а если он всегда совпадает с D, то код равен ОхАА. Ниже приведены определения этих кодов растровых операций на языке C/C++: const BYTE rop_P = OxFO; //11110000 const BYTE rop_S = OxCC; //11001100 const BYTE rop_D = OxAA; //10101010 Все остальные растровые операции определяются на основании этих трех констант посредством булевых операций. Например, если в результате растровой операции S и Р объединяются логической операцией AND, достаточно вычислить rop_S&rop_P — получается ОхСО. Если вам нужна растровая операция, которая возвращает Р, если S = 1, и D в противном случае, вычислите (rop_S&rop_P) | (~rop_S&rop_D); получается -хЕ2. Для каждой растровой операции существует минимум одна соответствующая формула булевой алгебры, точно описывающая растровую операцию по величинам Р, S и D. Операции может соответствовать несколько формул, но все они являются логически эквивалентными. По традиции в этих формулах используется постфиксная запись, при которой оператор находится справа от операндов. Постфиксная запись удобна тем, что при ней не нужны круглые скобки, а ее логика легко реализуется в компьютерных программах. Вероятно, разработчик растровых операций был поклонником Forth (расширяемый язык со стековой схемой вычислений без проверки типа), PostScript или инженерных калькуляторов HP, использующих постфиксную запись. В постфиксной записи растровых операций Р, S и D являются операндами, а а, о, п и х — соответственно операторами для операций AND, OR, NOT и XOR. Например, растровая операция 0хЕ2 записывается формулой DSPDxax. Преобразуя ее в инфиксную запись, мы получаем DX(S&(P4)))). В более наглядном виде эта формула выглядела бы так: (S&P) |(~S&D), или SPaSnDao. Почему же первой формуле отдается предпочтение перед второй? По двум причинам. На большинстве процессоров реализована инструкция XOR, не уступающая по скорости операции AND. В первой формуле используются три операции, а во второй — четыре. Для вычисления первой формулы нужен только один дополнительный
610 Глава 11. Нетривиальное использование растров регистр, а для второй — два регистра. Ниже приведена реализация этих формул на псевдокоде (для кадрового буфера с 32-разрядной кодировкой цвета): // Реализация 0хЕ2 DSPDxax по схеме DA(S&(PAD)) mov еах, Р // Р хог еах. D // PAD and еах. S // S&(PAD) хог еах. D // DA(S&(PAD)) mov D. еах // Записать результат // Реализация 0хЕ2 SPaSnDao по схеме (S&P)|(~S&D) mov еах. S // S and еах. Р // S&P mov ebx. S // S not ebx // ~S and ebx. D // -S&D or eax. ebx // (S&P) | (-S&D) mov D. еах // Записать результат Растровые операции в GDI обычно кодируются 32-разрядными двойными словами DWORD вместо простых байтов от 0 до 255. В старшем слове хранится один из 256 однобайтовых кодов растровых операций, о которых говорилось выше; младшее слово содержит кодировку формулы, определяющей растровую операцию. В исходной архитектуре для определения растровой операции используется только младшее слово; старшее слово содержит дополнительную информацию для аппаратных блиттеров. В старых реализациях растровые операции кодировались вручную на оптимизированном ассемблере, поэтому предпочтение отдавалось общим алгоритмам вместо 256 разных случаев для разных растровых операций и большой таблицы переходов. В современных реализациях используется автоматический генератор растровых операций, который в отличие от своих предшественников не жалуется на необходимость создания 256 разных функций. Механизм кодировки младшего слова тернарной растровой операции — настоящее произведение искусства, появившееся в те давние времена, когда программистам приходилось тщательно обдумывать каждую строку машинного кода и экономить каждый бит памяти. В формулах растровых операций используется 7 разных символов, причем длина формулы может достигать 12 символов. Простейшему механизму кодировки для этого понадобилось бы log2(712) или 33,7 бита информации, но разработчикам приходилось ограничиваться одним 16-разрядным словом. Формула растровой операции делится на две части: операторы и операнды (по аналогии со стеками операторов и операндов на некоторых стековых машинах). Операторы кодируются старшими 11 битами, а оставшиеся 5 бит остаются для операндов. Из 11 бит 10 используются для кодировки пяти логических операций, по два бита на операцию: 0 — NOT, 1 — XOR, 2 — OR, 3 — AND. Последний флаговый бит является признаком последней операции NOT. Закодировать строку операндов всего 5 битами очень нелегко, но умные программисты отыскали в строках операндов повторяющиеся цепочки. Всего было выделено 8 таких цепочек, для кодировки которых достаточно трех битов. Последние два бита определяют величину сдвига цепочек. Схема кодировки младшего слова тернарных операций GDI изображена на рис. 11.1.
Тернарные растровые операции 611 Оператор 0:NOT 1:XOR 2: OR 3:AND 0: SPDDDDDD 1:SPDSPDSP 2: SDPSDPSD 3: DDDDDDDD 4: DDDDDDDD r ^y "у Y y^ Op 5 Op 4 Op 3 Op 2 Op 1 ^ A. Л A Л 5: S+SP-DSS 6: S+SP-PDS 7: S+SD-PDS 15 14 13 12 11 10 9 8 7 6 5 4 3 Рис. 11.1. Структура младшего слова растровой операции Конечно, сказанное стоит пояснить на конкретном примере. Возьмем растровую операцию 0хЕ2; полный код растровой операции равен 0х00Е20746, поэтому младшее слово равно 0x0746. Таким образом, Ор5 = NOT, Op4 = NOT, ОрЗ = XOR, Ор2 = AND, Opl = XOR, дополнительная операция NOT не нужна, индекс цепочки равен 1, а смещение равно 2. Цепочка SPDSPDSP сдвигается на два символа и дает DSPDSPSP. У нас имеется пять операторов, но лишь три из них являются бинарными; два последних относятся к унарным. Следовательно, реально используются лишь четыре операнда. Цепочка усекается до DSPD; применение операторов, начиная с последнего символа, дает нам DSPDxaxnn, или после упрощения — DSPDxax; именно эта строка определяет растровую операцию в GDI. Знаки «+» и «-» в цепочках называются специальными операндами. Из 256 растровых операций 16 не могут быть выражены с использованием простого накопителя, в котором хранится только одна вычисляемая величина. Для этих 16 растровых операций промежуточный результат заносится в стек, а затем извлекается при необходимости. Специальные операнды всегда включаются в цепочку парами. В первый раз текущий результат заносится в стек, и загружается следующий операнд; во второй раз текущий результат объединяется с величиной, сохраненной в стеке, бинарной логической операцией. Во внутреннем представлении эти специальные операнды представляются одними и теми же битами (0x00), чтобы цепочка помещалась в 16-разрядном слове. На рис. 11.1 знак «-» соответствует занесению в стек, а знак «+» — извлечению из стека. Не забывайте о том, что цепочки читаются в обратном порядке. Конечно, эта славная архаичная структура экономит память и объем ассемблерного кода, но это достигается за счет быстродействия и наглядности. В новых реализациях GDI этот медленный механизм уже не используется, поэтому в общем случае младшее слово тернарной операции можно смело игнорировать. Однако трудно с уверенностью сказать, не захочет ли конкретный драйвер графического устройства проверить точное совпадение всех битов 32-разрядного кода
612 Глава 11. Нетривиальное использование растров растровой операции, поэтому для надежности рекомендуется проверять полный код растровой операции и использовать его. Тернарные растровые операции используют только 24 бита из 32-разрядного кода ROP. Старшие 8 бит кода обычно заполняются нулями. В Windows 98 и Windows 2000 появились два новых флага, управляющих копированием растров: CAPTUREBLT и N0MIRR0RBITMAP. Флаг CAPTUREBLT (0x40000000) используемся при работе с окнами, имеющими собственную поверхность вывода, которая может объединяться с содержимым других окон посредством альфа-наложения. При использовании флага CAPTUREBLT пикселы всех окон, расположенных поверх текущего окна, включаются в итоговое изображение. По умолчанию изображение состоит только из содержимого текущего окна. Флаг N0MIRR0RBITMAP (0x80000000) предотвращает зеркальное отражение растров по вертикали и горизонтали оси из-за разной направленности осей в исходном и приемном прямоугольниках. Диаграмма тернарных растровых операций Алгебраические формулы растровых операций точны и удобны, но визуальное представление поможет вам лучше разобраться в многочисленных разновидностях этих операций. Создав шаблоны для трех переменных, мы сможем сгенерировать итоговый растр по алгебраическим формулам и наглядно увидеть результаты применения всех операций. На рис. 11.2 приведена простая диаграмма тернарных растровых операций, полученная применением растровых операций к трем растрам, изображенным слева. ^_ йй 01 Q2 03 04 OS ОБ 07 08 09 ОА 0В ОС OD QE OF Pattern 20 ■ 000О0&0О0&0О&®ЯО Рис. 11.2. Диаграмма тернарных растровых операций
Тернарные растровые операции 613 Растр узора (8x8 пикселов) сгенерирован применением шаблона OxFO по направлениям X и Y. Источник сгенерирован по шаблону ОхСС, а приемник — по шаблону ОхАА. В данном примере белому цвету соответствует логическое значение 1, а черному — логический ноль. 256 маленьких растров представляют все возможные результаты растровых операций. Они сгенерированы созданием узорной кисти по узорному растру и последующим объединением источника с приемником при выбранной узорной кисти. Ниже приведен код, использованный при построении диаграммы растровых операций. Обратите внимание: растры 8x8 требуются для того, чтобы узорная кисть работала и в системах семейства Windows 95. Растр генерируется в совместимом контексте устройства, а затем масштабируется в экранном контексте устройства, поскольку узорные кисти не масштабируются. const WORD Bit_Pattern [] = { OxFO, OxFO, OxFO. OxFO. OxOF. OxOF. OxOF, OxOF OxCC. OxCC. 0x33. 0x33. const WORD Bit_Source [] OxCC. OxCC. 0x33. 0x33 const WORD Bit_Destination[] OxAA. 0x55. OxAA. 0x55 OxAA. 0x55. OxAA. 0x55. void Rop3Chart(HDC hDC) HBITMAP Pbmp = CreateBitmap(8, 8 HBITMAP Sbmp = CreateBitmap(8. 8. HBITMAP Dbmp - CreateBitmap(8. 8. HBITMAP Rbmp = CreateBitmap(8. 8. 1.1. Bit_Pattern); 1.1. Bit_Source); 1.1. Bit_Destination); 1. 1. NULL); HBRUSH Pat = CreatePatternBrush(Pbmp); HDC Sr - CreateCompatibleDC(hDC) HDC Dst = CreateCompatibleDC(hDC) HDC Rst = CreateCompatibleDC(hDC) // Узорная кисть // Совм. DC для источника // Совм. DC для приемника // Совм. DC для результата SelectObjectCSrc. Sbmp); SelectObject(Dst. Dbmp); SelectObjectCRst. Pbmp); StretchBlt(hDC. 20. 20. 80. 80. Rst. 0. 0. 8. 8. SRCCOPY) StretchBltChDC. 20. 220. 80. 80. Src. 0. 0. 8. 8. SRCCOPY) StretchBlt(hDC, 20. 420. 80. 80. Dst. 0. 0. 8. 8. SRCCOPY) SetBkMode(hDC. TRANSPARENT); TextOut(hDC. 20. 105. "Pattern". 7); TextOut(hDC. 20. 305. "Source". 6); TextOut(hDC. 20. 505. "Destination". 11); SelectObject(Rst. Rbmp); SelectObject(Rst. Pat); for (int i=0; i<16; i++) char mess[3]; wsprintf(mess. "ОЯХ", i); TextOut(hDC, 140 + i*38. 10. mess. 2);
614 Глава 11. Нетривиальное использование растров wsprintf(mess. "Щ". i); TextOut(hDC, 115. 30+1*38. mess. 2); } for (int rop=0; rop<256; rop++) { BitBltCRst. 0. 0. 8. 8. Dst. 0. 0. SRCCOPY); BitBltCRst. 0. 0. 8. 8. Src. 0. 0. GetRopCode(rop)); StretchBltChDC. 140 + (rop*16)*38. 30 + (rop/16)*38. 32. 32. Rst. 0. 0. 8. 8. SRCCOPY); } DeleteObject(Src); DeleteObject(Dst); DeleteObject(Rst); DeleteObject(Pat): DeleteObject(Pbmp); DeleteObject(Sbmp); DeleteObject(Dbmp): DeleteObject(Rbmp); } Часто используемые растровые операции Набор из 256 растровых операций выглядит впечатляюще, но на практике активно используется лишь десяток с небольшим операций. Разработчики Microsoft удосужились присвоить имена всего 15 из них. Если учесть, что в GDI существует 16 именованных бинарных растровых операций, 15 именованных тернарных операций явно недостаточно, поскольку каждой бинарной операции соответствует тернарная операция, результат которой не зависит от S. В табл. 1.1 перечислены 30 тернарных растровых операций, используемых в практическом программировании. По сравнению с бинарными операциями имена тернарных операций выглядят довольно странно. Все имена бинарных растровых операций начинаются с префикса R2_, поэтому напрашивается предположение, что имена тернарных операций должны начинаться с префикса R3_. Ничего подобного! В именах бинарных операций операция NOT обозначается «NOT», операция XOR обозначается «XOR», операция OR обозначается «MERGE», а операции AND соответствует обозначение «MASK». В именах тернарных операций «INVERT» иногда обозначает NOT, а иногда — XOR; операция OR обозначается «PAINT», a AND NOT обозначается «ERASE». При использовании незнакомых тернарных растровых операций необходимо действовать очень осторожно. Проверьте формулу и убедитесь, что она делает именно то, что вам нужно. Таблица 11.1. Тернарные растровые операции Зависимость Имя ROP3 Код ROP Формула Имя ROP2 Нет BLACKNESS 0x000042 0 R2JLACK WHITENESS 0xFF0062 1 R2 WHITE
Тернарные растровые операции 615 Зависимость Узор Источник Приемник Приемник и источник Узор и приемник Приемник и источник Узор, источник и приемник Имя ROP3 PATC0PY SRCC0PY N0TSRCC0PY MERGECOPY PATINVERT NOTSRCERASE SRCERASE SRCINVERT SRCAND MERGEPAINT SRCPAINT PATPAINT Код ROP 0xF00021 OxOFOOOl 0хСС0020 0x330008 0хАА0029 0x550009 ОхСОООСА 0xF0008A 0х0500А9 0х0А0329 0x5000325 0х5А0049 0x5F00E9 0хА000С9 0хА50065 0xAF0229 0xF50225 0xFA0089 ОхИООАб 0x440328 0x660046 0х8800С6 0хВВ0226 0хЕЕ0086 0xFB0A09 0хВ8074А 0хЕ20746 Формула Р -Р S -S D Ч) Р & S Р | S ~(Р | D) -P&D P&-D Р А D ~(P&D) Р & D ~ (Р А D) -Р | D Р | -D Р | D ~( S | D) S & ~D SAD S & D ~S | D S | D P | ~S | D PA(S&(PAD))) DA(S&(PAD))) Имя ROP2 R2_C0PYPEN R2_N0TC0PYPEN R2_N0P R2_N0TMERGEPEN R2_MASKN0TPEN R2_MASKPENN0T R2JC0RPEN R2_N0TMASKPEN R2_MASKPEN R2_N0TX0RPEN R2_MERGEN0TPEN R2_MERGEPENN0T R2_MERGEPEN Растровые операции в таблице упорядочены по степени зависимости от трех переменных Р, S и D. В этом отношении эта таблица отличается от большинства таблиц растровых операций, содержимое которых обычно упорядочивается по числовым значениям кодов. Понимание зависимостей поможет вам в программировании. Например, если растровая операция зависит от приемника (D), вряд ли ее стоит использовать при печати. Растровые операции предназначены для растровых устройств, у которых каждый пиксел адресуем, доступен для чтения и для записи. Некоторые графические устройства (например, принтеры PostScript) не являются полноценными растровыми устройствами; это означает, что они не
616 Глава 11. Нетривиальное использование растров поддерживают полного набора растровых операций (особенно тех, которые зависят от пикселов приемника). Если растровая операция не зависит от растра- источника, его не обязательно передавать при вызовах функций BitBlt, StretchBlt и StretchDIBits. В GDI предусмотрена специальная функция для выполнения растровых операций, не использующих источника: BOOL PatBltCHDC hDC. int nXLeft. int nYLeft. int nWidth, int nHeight. DWORD dwROP); Функция PatBlt комбинирует текущую кисть с прямоугольным регионом приемника. Обратите внимание на отсутствие параметров, определяющих растр-источник. Набор допустимых растровых операций не ограничивается именами ROP, выбранными Microsoft. Вы можете использовать любые растровые операции, в которых не задействован источник. Узнать, использует ли растровая операция ту или иную переменную, несложно. Для этого достаточно проверить, генерирует ли ROP одинаковые результаты при значениях этой переменной, равных 1 и 0. Проверочные функции для трех переменных приведены ниже. boo! inline RopNeedsNoDestination(int Rop) return ((Rop & OxM) » 1) == (Rop & 0x55); bool inline RopNeedsNoSource(int Rop) return ((Rop & OxCC) » 2) — (Rop & 0x33); bool inline RopNeedsNoPattern(int Rop) return ((Rop & OxFO) » 4) == (Rop & OxOF); BLACKNESS, WHITENESS Эти две растровые операции обычно используются для инициализации поверхностей и их возврата в исходное состояние. Операция BLACKNESS присваивает всем пикселам О, WHITENESS устанавливает все биты пикселов в 1. На устройстве с палитрой результат не всегда соответствует черному и белому цвету, но обычно первым цветом в палитре является черный, а последним — белый. Операции BLACKNESS и WHITENESS не зависят ни от одной из трех переменных. Как нетрудно догадаться, самая эффективная реализация этой операции организует заполнение памяти постоянными величинами. BLACKNESS реализуется вызовом memset(pBits, 0, nlmageSize), a WHITENESS — вызовом memset(pBits, OxFF, nlmageSize). При создании DDB или DIB-секции массив пикселов часто находится в неопределенном состоянии (исключение составляют инициализируемые DDB- растры и DIB-секции, отображаемые на память). Создаваемые поверхности рекомендуется инициализировать и переводить в определенное состояние. Черный и белый цвета отличаются от других тем, что они представляются как 0 и 1. В частности, для цвета С справедливы следующие утверждения: Black AND С = Black Black OR С = С
Тернарные растровые операции 617 Black X0R С - С White AND С = С White OR С = White White X0R С = NOT С При частичном заполнении растров черным или белым цветом с применением масок эти свойства играют важную роль при создании интересных эффектов. Только узор: PATCOPY, R3_NOTCOPYPEN Операция PATCOPY используется для заполнения прямоугольных областей текущей кистью (по аналогии с функцией FillRect). Противоположная операция (OxOFOOOl) не имеет официального названия, поэтому в табл. 11.1 имя для нее выбрано по аналогии с бинарной операцией R2_N0TC0PYPEN. Операция R3N0TC0PYPEN заполняет прямоугольную область цветом, противоположным цвету текущей кисти. Только источник: SRCCOPY, NOTSRCCOPY Мы неоднократно встречались с операцией SRCCOPY, которая просто копирует пикселы источника в приемник. Эта операция обычно требуется для отображения растра в исходном цвете. Операция NOTSRCCOPY копирует в приемник цвет, противоположный цвету источника. В режимах True Color и High Color эта операция может использоваться для создания негативов. В Windows NT 4.0 операция NOTSRCCOPY иногда реализуется неправильно (согласно документации (KB Q174534)). Ошибка должна быть исправлена в Service Pack 4. Только приемник: R3_NOP, DSTINVERT Операция DSTINVERT меняет цвет пиксела приемника на противоположный. Операция R3N0P заменяет цвет пиксела приемника тем же цветом — напрасная трата процессорного времени. Вероятно, у GDI хватает сообразительности, чтобы игнорировать операцию R3_N0P и обойтись без напрасного перемещения пикселов. Без приемника: MERGECOPY Существует десять тернарных растровых операций, зависящих от источника и узора и не зависящих от приемника. Лишь одной из этих операций было присвоено имя — MERGECOPY. Имя операции MERGECOPY (OxCOOOCA) выбрано неудачно. В бинарных растровых операциях строка «MERGE» обозначает логическую операцию OR, но здесь это правило почему-то не соблюдается. Операция MERGECOPY заменяет пиксел приемника результатом конъюнкции пиксела источника с пикселом узора. Если растр-источник является монохромным, MERGECOPY окрашивает белые пикселы в цвет кисти и оставляет черные пикселы без изменений. Если растр-источник не является монохромным, иногда бывает удобнее использовать в качестве маски кисть. Например, чтобы в цветном изображении отображался только красный канал, создайте однородную красную кисть (RGB(0xFF,0,0)) и воспользуйтесь растровой операцией MERGECOPY для вывода изображения. Красный канал копируется без изменений, а остальные две составляющие обнуляются. В результате создается изображение, состоящее из оттенков красного цвета.
618 Глава 11. Нетривиальное использование растров Приведенная ниже функция выводит один канал изображения, определяемый заданной маской. Например, если параметр mask равен RGB(0xFF,0,0), отображается только красный канал. void DisplayChannel(HDC hDC. int x, int y. int width, int height, HDC hDCSource. COLORREF mask) { HBRUSH hRed = CreateSolidBrush(mask); HBRUSH hOld = (HBRUSH) SelectObject(hDC. hRed); BitBlt(hDC, x, y, width, height. hDCSource. 0. 0. MERGECOPY); SelectObject(hDC. hOld); DeleteObject(hRed): } Разделение цветовых составляющих — стандартный прием, поддерживаемый во многих графических редакторах. Говорят, специалисты по компьютерной графике предпочитают просматривать изображение по каналам, устранять все дефекты и потом получать итоговое изображение, объединяя все каналы. Именно так создаются полноценные изображения в оттенках серого цвета. Если в приведенном выше примере приемная поверхность имеет 8-разрядный формат пикселов, а цветовая таблица настроена в соответствии с одноканальной цветовой шкалой, то позднее вам останется лишь изменить цветовую таблицу для получения изображения в оттенках серого. В листинге 11.1 приведена функция Channel Spl it, разделяющая произвольный цветной DIB-растр на три изображения в оттенках серого (по одному для каждого канала RGB). Листинг 11.1. Деление растра на каналы RGB // Создание изображения в оттенках серого (DIB-секции) // по одному каналу RGB в DIB HBITMAP ChannelSplit(const BITMAPINFO * pBMI. const void * pBits. COLORREF Mask. HDC hMemDC) { typedef struct { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColor[256]; } BMI8BPP: int width = pBMI->bmiHeader.biWidth; int height - pBMI->bmiHeader.biHeight; BMI8BPP bmi8bpp; memset(&bmi8bpp. 0, sizeof(bmi8bpp)): bmi8bpp.bmiHeader.biSize - sizeof(BITMAPINFOHEADER); bmi8bpp.bmiHeader.biWidth e width; bmi8bpp.bmiHeader.biHeight - height: bmi8bpp.bmiHeader.biPlanes - 1: bmi8bpp.bmiHeader.biBitCount - 8; bmi8bpp.bmiHeader.biCompression - BI_RGB; for (int i-0; i<256; 1++) // Цветовая таблица одного из каналов RGB {
Тернарные растровые операции 619 bmi 8bpp.bmi Col or[i].rgbRed ' - i & GetRValue(Mask); bmi8bpp.bmiColor[i].rgbGreen = i & GetGValue(Mask); bmi8bpp.bmiColor[i].rgbBlue - i & GetBValue(Mask); } HBITMAP hRslt - CreateDIBSectionCNULL. (BITMAPINFO *) & bmi8bpp. NULL, DIB_RGB_C0L0RS. NULL, NULL); if ( hRslt==NULL ) return NULL; SelectObjectChMemDC. hRslt); HBRUSH hBrush - CreateSolidBrush(Mask); // Однородный красный. // зеленый или синий цвет HGDIOBJ hOld - SelectObjectChMemDC. hBrush); StretchDIBits(hMemDC. 0. 0. width, height. 0. 0. width, height. pBits. pBMI. DIB_RGB_COLORS. MERGECOPY); for (i=0; i<256; i++) // Перейти к настоящей цветовой таблице // оттенков серого цвета { bmi 8bpp.bmi Col or[i].rgbRed - i; bmi8bpp.bmiColor[i].rgbGreen = i; bmi8bpp.bmiColor[i].rgbBlue = i: } SetDIBColorTableChMemDC. 0. 256. bmi8bpp.bmiColor); SelectObjectChMemDC. hOld); DeleteObject(hBrush); return hRslt; } Функция Channel Split создает DIB-секцию с 8-разрядной кодировкой пикселов и цветовой таблицей, содержащей оттенки цвета одного из каналов RGB. Например, если параметр mask равен RGB(255,0,0), цветовая таблица будет содержать элементы RGB(0,0,0), RGB(l.O.O) и т. д. до RGB(255,0,0). DIB-секция выбирается в совместимом контексте устройства вместе с однородной кистью, созданной на базе маски того же канала. Функция StretchDIBits использует растровую операцию MERGECOPY для выделения красного канала и сопоставляет каждому пикселу элемент цветовой таблицы DIB-секции. Если пиксел исходного DIB-раст- ра равен RGB(r,g,b), в результате операции MERGECOPY генерируется цвет RGB(г,0,0). В цветовой таблице DIB-секции ему соответствует индекс г, который и сохраняется в массиве пикселов DIB-секции. Наконец, происходит модификация цветовой таблицы, чтобы DIB-секция выводилась как изображение в оттенках серого цвета. Функция ChannelSplit работает с любыми DIB-растрами — с любой цветовой глубиной, с битовыми масками, сжатыми и несжатыми, почти не требуя дополнительного кодирования с вашей стороны. Впрочем, есть и недостатки — хотя в большинстве случаев сгенерированное изображение в оттенках серого выглядит
620 Глава 11. Нетривиальное использование растров вполне нормально, результат не идеален. Для получения более точного результата GDI пришлось бы просматривать всю цветовую таблицу в поисках идеального совпадения или оптимального приближения, а это относительно медленный процесс. Судя по результату (что, впрочем, проявляется лишь в изображениях с плавными переходами цветов), GDI использует механизм аппроксимации — вероятно, по соображениям быстродействия. Вместо того чтобы просматривать всю цветовую таблицу в поисках каждого пиксела, можно построить сетку RGB из N х N х N точек. При необходимости каждая точка сетки сопоставляется с элементом цветовой таблицы. Каждый пиксел RGB, сгенерированный в результате растровой операции, аппроксимируется ближайшей точкой сетки, индекс которой и считается цветовым индексом пиксела. Реализация полноценного алгоритма деления изображения на цветовые каналы требует операций непосредственно с массивом пикселов. Мы вернемся к этой теме позднее. Без источника: PATINVERT Из 10 тернарных растровых операций, зависящих от узора и приемника, но не от источника, имя присвоено только операции PATINVERT. Указанные растровые операции обладают теми же возможностями, что и бинарные растровые операции, выбираемые функцией SetR0P2. Операции ROP, в которых не задействован источник, могут использоваться с функциями PatBlt, что позволяет обойтись без более сложных вызовов. Одной из областей их применения является модификация текущего изображения в контексте, соответствующего физическому устройству или блоку памяти, связанному с DDB или DIB-секцией. Например, если у вас имеется черно-белое изображение, вы можете воспользоваться операцией R3_MASKPEN(0xA000C9), чтобы раскрасить его цветной кистью или разделить на каналы RGB. При использовании кисти с шахматным узором операция R3MASKPEN позволяет создать эффект частичного затенения, при котором половина пикселов сохраняет прежний цвет, а другая половина закрашивается черным цветом. Тернарные операции без узора Следующая группа растровых операций использует источник и приемник, но не использует узор. Из 10 возможных растровых операций этой категории имена присвоены 6. Вероятно, Microsoft считает эти операции более важными. Наличие имени у тернарной растровой операции обычно означает, что она требуется самой операционной системе. Операции SRCAND и SRC INVERT используются Windows при отображении значков и курсоров мыши. Ресурс значка/курсора обычно содержит группу изображений для разных вариантов размера и цветовой глубины. Каждый значок/курсор обычно состоит из двух растров: черно-белой маски и цветной маски. Черно-белый значок или курсор может состоять из одного растра двойной высоты; в этом случае выводимый растр описывается второй половиной растра. Маска выводится растровой операцией SRCAND и стирает ту область, в которой будет находиться цветной растр. После этого цветной растр выводится операцией SRC INVERT. Следующий фрагмент поможет вам лучше разобраться в применении растровых операций при выводе значка:
Тернарные растровые операции 621 HICON hlcon = (HICON) LoadImage(hMod. MAKEINTRESOURCE(resid). IMAGE_ICON, 48, 48. LR_DEFAULTCOLOR); if ( hlcon) { DrawIcon(hDC. x. y. hlcon); ICONINFO iconinfo; GetIconInfo(hIcon. & iconinfo); Destroylcon(hlcon); BITMAP bmp; GetObject(iconinfo.hbmMask, sizeof(bmp), & bmp); HGDIOBJ hOld = SelectObject(hMemDC. iconinfo.hbmMask); BitBltChDC, x+56. y. bmp.bmWidth, bmp.bmHeight. hMemDC.O.O.SRCCOPY); Sel ectObject(hMemDC. i coni nfо.hbmColor): BitBltChDC. x+112. y. bmp.bmWidth. bmp.bmHeight. hMemDC.O.O.SRCCOPY); SelectObject(hMemDC. iconinfo.hbmMask); BitBltChDC. x+168, y. bmp.bmWidth. bmp.bmHeight. hMemDC.0.0.SRCAND); SelectObject(hMemDC. i coni nfo.hbmColor); BitBltChDC. x+168, y. bmp.bmWidth. bmp.bmHeight. hMemDC.0.0.SRCINVERT); SelectObjectChMemDC. hOld); DeleteObject(i coni nfo.hbmMask); DeleteObject(i coni nfo.hbmColor); } Программа загружает значок размером 48 х 48 из модуля Loadlmage и выводит его стандартной функцией Win32 Drawl con; при этом значок масштабируется по своим стандартным размерам (обычно 32 х 32). Для удовлетворения нашего любопытства вызывается функция Getlconlnfo, возвращающая манипуляторы двух DDB-растров — маски и цветного растра. Затем эти два растра выводятся по отдельности, чтобы мы могли присмотреться к ним поближе. Далее вывод значка имитируется выводом обоих растров в одном и том же месте. При выводе маски используется растровая операция SRCAND, а при выводе цветного растра — операция SRCINVERT. На рис. 11.3 показано строение некоторых значков, используемых в графической среде Windows. Прежде чем продолжить обсуждение, давайте определимся с некоторыми терминами. При выводе значка, определенного в виде прямоугольной области, некоторые пикселы этой области должны изменяться, а некоторые должны оставаться прежними. Изменяемая область называется непрозрачной (opaque); все остальные пикселы образуют прозрачную область. Обычно непрозрачная область определяет форму значка — например, мусорной корзины или папки. Из рисунка видно, что в маске непрозрачная область обозначается черным цветом (0), а прозрачная — белым цветом (1). Таким образом, первый вызов BitBU, использующий растровую операцию SRCAND, закрашивает непрозрачную область черным цветом и оставляет прозрачную область без изменений. В цветном растре непрозрачная область заполнена цветными пикселами, а в прозрачной области находятся черные пикселы (0). Второй вызов BitBlt с использованием операции SRCINVERT выводит цветные пикселы без изменения прозрачной области.
622 Глава 11. Нетривиальное использование растров Рис. 11.3. Применение растровых операций при выводе значков Если бы при создании маски непрозрачная область описывалась белым цветом (1), а для прозрачной области был зарезервирован черный цвет (0), для очистки непрозрачной области вместо SRCAND следовало бы использовать растровую операцию 0x220326 (DSna). Обратите внимание: оператор NOT в DSna фактически инвертирует маску в процессе применения. Маска и цветной растр могут иметь разные непрозрачные области. Если непрозрачная область маски меньше непрозрачной области цветного растра, при использовании второй растровой операции некоторые пикселы непрозрачной области не закрашиваются черным цветом (1); операция SRCINVERT заменяет пиксел приемника значением D^S вместо S. С другой стороны, при совпадении непрозрачных областей того же результата можно добиться растровой операцией SRCPAINT (DSo). А что произойдет, если маска выглядит так, как показано на рис. 11.3, а прозрачные пикселы цветного растра окрашены в белый цвет (1) вместо черного (0)? В результате применения SRCAND и SRCINVERT прозрачные пикселы инвертируются вместо того, чтобы оставаться неизменными. Мы должны заменить первую растровую операцию, применяющую маску, на SRCERASE (SDna). Если в маске поменялись цвета, первая растровая операция должна быть заменена на N0TSRCERASE (SDon). Для вывода инвертированного источника можно воспользоваться растровой операцией MERGEPAINT (~S|D, DSno). Итак, мы нашли применения для всех шести тернарных растровых операций, не использующих узора, которым компания Microsoft присвоила имена. Если вы не полностью разобрались в том, как работают эти операции, в табл. 11.2 приведена краткая сводка их применения для вывода маски и цветного растра в разных условиях. В таблице для обозначения пикселов растра используется запись (X,Y), где X — цвет прозрачных пикселов, a Y — цвет непрозрачных пикселов. Таким образом, первая строка таблицы читается следующим образом: если в маске прозрачная область обозначена белыми пикселами, а непрозрачная область обозначена черными пикселами, после вывода маски операцией SRCAND прозрачная область остается без изменений, а непрозрачная область окрашивается в черный цвет. Затем цветной растр, в котором прозрачная область обозначена черным цветом,
Тернарные растровые операции 623 выводится растровой операцией SRC INVERT; в результате пикселы непрозрачной области заменяются пикселами цветного растра, а пикселы прозрачной области сохраняют прежнее состояние. Таблица 11.2. Прозрачное отображение растров с применением маски Маска (Белый, черный) (Белый, черный) (Белый, черный) (Черный, белый) (Белый, черный) (Черный, белый) ROP маски SRCAND SRCAND SRCAND R3_DSna SRCERASE NOTSRCERASE Результат применения маски (D, черный) (D, черный) (D, черный) (D, черный) (Dn, черный) (Dn, черный) Цветной растр (Черный, С) (Черный, С) (Белый, С) (Черный, С) (Белый, С) (Белый, С) ROP цветного растра SRCINVERT SRCPAINT MERGEPAINT SRCINVERT SRCINVERT SRCINVERT Итоговые результат <D,C) (D,C) (D,C) (D,C) (D,C) (D,C) Другие растровые операции Мы рассмотрели тернарные растровые операции, не зависящие от узора, источника или кисти, а также зависящие от одного или двух факторов. Общее количество растровых операций этих операций равно 2 + 2x3+ 10x3 = 38. Остальные 218 растровых операций зависят от всех трех переменных. Из этих 218 операций в GDI имя было присвоено только операции PATPAINT (P|~S|D). Операция PATPAINT объединяет пиксел кисти, инвертированный пиксел источника и пиксел приемника логическим оператором OR. He зная условий, которым должны подчиняться эти переменные, трудно понять, зачем нужна подобная растровая операция. Пока оставим PATPAINT в покое и займемся другими операциями, для которых можно найти практическое применение. В одном из способов рисования прозрачных растров используются два растра: маска и цветной растр-источник (см. выше пример с выводом значков). Недостаток подобного решения заключается в необходимости создания маски, точно совпадающей с пикселами цветного растра. А если определить маску в виде кисти вместо отдельного растра? Предположим, мы создали шахматный узор, в котором половина пикселов окрашена в черный цвет, а другая половина остается белой. Можно ли вывести на поверхности устройства лишь каждый второй пиксел цветного растра, не изменяя второй половины? Какую растровую операцию следует для этого применить? Нужная операция легко вычисляется при помощи булевой алгебры. Эффект, которого мы хотим добиться, описывается формулой (P&S)| (-P&D): если пиксел
624 Глава 11. Нетривиальное использование растров кисти равен 1 (белый), результат совпадает с пикселом растра-источника; в противном случае используется пиксел приемника. Заменяя Р, S и D кодами OxFO, ОхСС и ОхАА, мы получаем ОхСА. По таблице тернарных растровых операций мы находим полный код операции 0хСА07А9 и официальную формулу D^(P&(S4D)). Чтобы перейти к противоположной интерпретации Р, формула принимает вид (-P&S) | (P&D); ей соответствует код ОхАС. В результате мы получаем 0хАС0744 и официальную формулу S*(P&(S*D)). Функция Fadeln, приведенная в листинге 11.2, при помощи растровой операции 0хСА07А9 создает эффект «постепенного проявления». Исходное изображение выводится за четыре шага с применением разных узорных кистей. На первом шаге проявляется 1/64 пикселов исходного изображения, на втором — 4/64, на третьем — 16/64, на последнем — все пикселы. Листинг 11.2. Постепенное проявление растра с использованием растровых операций // Постепенное отображение DIB на приемной поверхности за 4 шага void FadeIn(HDC hDC. int x, int y, int w. int h. const BITMAPINFO * pBMI. const void * pBits) { const WORD Maskll[8] = { 0x80. 0x00. 0x00. 0x00. 0x00. 0x00. 0x00. 0x00 }; const WORD Mask22[8] = { 0x88. 0x00. 0x00. 0x00. 0x88. 0x00. 0x00. 0x00 }; const WORD Mask44[8] = { OxAA. 0x00. OxAA. 0x00. OxAA. 0x00. OxAA. 0x00 }; const WORD Mask88[8] - { OxFF. OxFF. OxFF. OxFF. OxFF. OxFF. OxFF. OxFF }; const WORD * Mask[4] - { Maskll. Mask22. Mask44. Mask88 }; for (int i=0; i<4; i++) { HBITMAP hMask - CreateBitmap(8. 8. 1. 1. Mask[i]); HBRUSH hBrush= CreatePatternBrush(hMask); DeleteObject(hMask); HGDIOBJ hOld = SelectObject(hDC. hBrush); // D"(P&(S"D)). if P then S else D StretchDIBits(hDC, x. y. w. h. 0. 0. w. h. pBits. pBMI. DIB_RGB_C0L0RS. 0xCA07A9); SelectObject(hDC. hOld): DeleteObject(hBrush); } } Выше рассматривался вывод значков с применением растровых операций SRCAND и SRCINVERT за два вызова функции BitBlt. В системе семейства Windows NT можно было бы создать узорную кисть на базе маски и объединить два вызова в один, используя при этом растровую операцию (D&PKS с кодом 0х6С01Е8. Эта идея реализована в следующем фрагменте.
Тернарные растровые операции 625 void MaskBitmapNTCHDC hDC, int x, int y, int width, int height. HBITMAP hMask. HDC hMemDC) { HBRUSH hBrush = CreatePatternBrush(hMask); HGDIOBJ hOld - SelectObject(hDC, hBrush); POINT org - { x, у }; LPtoDP(hDC, &org, 1); SetBrushOrgEx(hDC. org.x. org.y, NULL); BitBUChDC. x. y, width, height. hMemDC. 0. 0, 0x6C01E8); // S4P&D) SelectObject(hDC. hOld); DeleteObject(hBrush); } В системах семейства Windows 95 не поддерживаются узорные кисти больше 8 х 8 пикселов. Кроме того, из-за перемещения маски в узорную кисть эта функция будет нормально работать только в режиме отображения ММ_ТЕХТ, поскольку узорная кисть не масштабируется вместе с режимами отображения и мировыми преобразованиями. Для монохромного растра-источника можно воспользоваться цветной кистью, чтобы раскрасить растр и вывести его с использованием прозрачности. Если мы хотим, чтобы черные пикселы (0) источника выводились цветом кисти, а белые пикселы (1) оставались прозрачными, следует воспользоваться формулой (-S&P) | (S&D). ROP-код этой операции равен 0хВ8, или 0хВ8074А, а официальная формула имеет вид PA(S&(P4D)). При противоположной интерпретации растра-источника формула принимает вид (S&P) | (-S&D), и в итоге мы получаем код 0хЕ20746 с официальной формулой D^(S&(P^D)). Листинг 11.3 демонстрирует применение растровой операции 0хВ8074А для раскраски непрозрачных пикселов монохромного растра произвольной кистью. Функция ColorBitmap осуществляет вывод с ROP-кодом 0хВ8074А, а функция TestColoring иллюстрирует раскраску монохромного растра пятью разными кистями. Листинг 11.3. Раскраска монохромного растра с применением растровых операций void ColorBitmap(HDC hDC, int x, int y. int w, int h. HDC hMemDC, HBRUSH hBrush) { // PA(S&(PAD)). if (S) D else P HGDIOBJ hOldBrush - SelectObjectChDC, hBrush): BitBltChDC. x. y. w. h. hMemDC. 0. 0. 0xB8074A); SelectObject(hDC. hOldBrush); } void TestColoring(HDC hDC. HINSTANCE hlnstance) { HBITMAP hPttrn; HBITMAP hBitmap - LoadBitmap(hInstance. MAKEINTRESOURCE(IDB_CONFUSE)); BITMAP bmp; GetObject(hBitmap, sizeof(bmp). &bmp); Продолжение^
626 Глава 11. Нетривиальное использование растров } Листинг 11.3. Продолжение SetTextColor(hDC. RGB(0, 0. 0)); SetBkColorChDC. RGBCOxFF. OxFF. OxFF)); HDC hMemDC = CreateCompatibleDC(NULL); HGDIOBJ hOld = SelectObject(hMemDC. hBitmap); for (int i=0; i<5; i++) { HBRUSH hBrush; switch (i) { case 0: hBrush - CreateSolidBrush(RGB(OxFF, 0. 0)): break; case 1: hBrush - CreateSo1idBrush(RGB(0, OxFF, 0)); break; case 2; hPttrn = LoadBitmap(hInstance. MAKEINTRESOURCE(IDB_PATTERN0D); hBrush = CreatePatternBrush(hPttrn); DeleteObject(hPttrn); break; case 3: hBrush = CreateHatchBrush(HS_DIAGCROSS, RGB(0. 0. OxFF)); break; case 4: hPttrn = LoadBitmap(hInstance, MAKEINTRESOURCE(IDB_WOOD01)); hBrush = CreatePatternBrush(hPttrn); DeleteObject(hPttrn); } ColorBitmap(hDC. i*30+10-2. 1*5+10-2. bmp.bmWidth, bmp.bmHeight. hMemDC. (HBRUSH)GetStockObject(WHITE_BRUSH)); ColorBitmap(hDC. i*30+10+2. i*5+10+2. bmp.bmWidth. bmp.bmHeight. hMemDC. (HBRUSH)GetStockObject(DKGRAYJRUSH)); ColorBitmap(hDC. i*30+10. i*5+10, bmp.bmWidth. bmp.bmHeight. hMemDC. hBrush); DeleteObject(hBrush); BitBltChDC. 240. 25. bmp.bmWidth. bmp.bmHeight. hMemDC. 0. 0. SRCCOPY); SelectObject(hMemDC. hOld); DeleteObject(hBitmap); DeleteObject(hMemDC); Функция загружает монохромный растр с изображением одного недоумевающего человечка и рисует группу из пяти человечков. В данном примере использовались белая и черная кисти и небольшое смещение выводимых растров, создающее простейший объемный эффект. Поскольку растровая операция 0хВ8074 рисует растры в прозрачном режиме, выводятся только пикселы непрозрачной области. На рис. 11.4 изображено пять цветных растров вместе с исходным монохромным растром (справа).
Прозрачные растры 627 Рис. 11.4. Прозрачная раскраска монохромного растра Прозрачные растры Даже при таком количестве растровых операций в компании Microsoft полагают, что прозрачный вывод растров — очень сложная задача, поэтому для ее решения были созданы три специальные функции: BOOL PlgBlt(HDC hdcDest. CONST POINT * IpPoint. HDC hDCSrc. int nXSrc, int nYSrc. int nWidth. int nHeight. HBITMAP hbmMask. int xMask. int yMask); BOOL MaskBltCHDC hdcDest. int nXDest. int nYDest, int nWidth, int nHeight. HDC hDCSrc. int nXSrc. int nYSrc. HBITMAP hbmMask. int xMask. int yMask. DWORD dwRop); BOOL TransparentBltCHDC hdcDest. int nXOriginDest. int nYOriginDest. int nWidthDest. int nHeightDest, HDC hdcSrc, int nXOriginSrc. int nYOriginSrc. int nWidthSrc. int nHeightSrc. UINT crTransparent); Эти три функции поддерживаются не на всех платформах Win32. Функции PlgBlt и MaskBIt поддерживаются только в системах семейства Windows NT, а функция TransparentBIt — только в Windows 98, Windows 2000 и последующих системах. Ниже будет показано, как эти функции имитируются на базе других растровых функций и прямого доступа к массиву пикселов. Даже при беглом взгляде на прототипы функций нетрудно заметить сходство между ними. Все функции получают два манипулятора контекстов устройств — для источника и приемника. Следовательно, эти функции работают как с совместимыми контекстами устройств, в которых выбран DDB-растр или DIB-сек- ция, так и с физическими устройствами, поддерживающими растровые операции.
628 Глава И. Нетривиальное использование растров DIB напрямую не поддерживаются; чтобы использовать эти функции, вам придется преобразовать DIB в DDB или в DIB-секцию. Функция PlgBIt Функция PlgBIt решает две задачи: преобразование прямоугольного растра в параллелограмм и управление прозрачностью при помощи маски. Следовательно, результат вызова этой функции определяется тремя факторами: приемником, источником и узором. Исходный прямоугольник представляет собой подмножество точек поверхности-источника, определяемое параметрами nXSrc, nYSrc, nWidth и nHeight. Все параметры задаются в логической системе координат контекста-источника. Как и при использовании других, более простых функций блиттинга, в контексте- источнике не могут действовать преобразования поворота и сдвига, однако смещение, масштабирование и зеркальные отражения допускаются. Это ограничение гарантирует, что исходный прямоугольник в системе координат устройства контекста-источника всегда соответствует прямоугольнику, стороны которого параллельны обеим осям. Параллелограмм-приемник определяется манипулятором контекста устройства и массивом из трех точек. Выше уже говорилось о том, что аффинное преобразование однозначно определяется отображением трех точек одного пространства в три точки другого пространства. Три точки, на которые ссылается параметр lpPoint, однозначно определяют параллелограмм на приемной поверхности, четвертая вершина которого вычисляется по формуле D = В + С - А, где А = lpPoint[0], В = lpPoint[l] и С = lpPoint[2]. Левый верхний угол исходного прямоугольника отображается в А, правый верхний угол отображается в точку В, левый нижний угол отображается в С, а правый нижний — в D. Отображение исходного прямоугольника в приемный параллелограмм представляет собой общее аффинное преобразование, допускающее смещение, масштабирование, отражение, повороты и сдвиги. С геометрической точки зрения функция PlgBIt по сравнению с StretchBlt обеспечивает дополнительные повороты и сдвиги. Растр маски определяется манипулятором растра и двумя целыми числами. Растр должен быть монохромным, в противном случае вызов функции завершится неудачей. Два целых параметра xMask и yMask определяют местонахождение пиксела маски, соответствующего левому верхнему углу растра-источника. При выходе за границы маски она применяется повторно — так же, как узорная кисть используется при заливке замкнутых областей. Если маска не задана, в приемном параллелограмме отображается все содержимое источника. В противном случае пикселы маски со значением 1 (белый) соответствуют участкам, копируемым из источника в приемник, а пикселы маски со значением 0 (черный) оставляют пикселы приемника без изменений. Если преобразовать маску в узорную кисть, логика ее применения выражалась бы растровой операцией с кодом 0хСА07А9, то есть P&S|~P&D. В системах семейства Windows NT функция PlgBIt может быть заменена функцией StretchBlt с настройкой мирового преобразования, преобразованием маски в узорную кисть и использованием растровой операции 0хСА07А9. В других
Прозрачные растры 629 системах StretchBlt может заменить PlgBlt лишь при отсутствии поворотов и сдвигов и при условии, что размеры маски не превышают 8x8 пикселов. В листинге 11.4 приведен пример использования функции PlgBlt для вывода объемного куба. Функция DrawCube рисует три грани куба функцией PlgBlt с применением источника и маски. Функция MaskCube управляет созданием маски в форме прямоугольника с закругленными углами, размер которой совпадает с размерами растра-источника. Листинг 11.4. Рисование трехмерного куба с использованием функции PlgBlt void DrawCubeCHDC hDC, int x. int y, int dh. int dx, int dy. HDC hMemDC. int w. int h. HBITMAP hMask) { SetStretchBltMode(hDC. HALFTONE); // 6 // 0 4 // 1 111 5 // 3 POINT P[3] - { { x - dx, у - dy }. { x, у }. { x - dx, у - dy + dh } }; //012 POINT Q[3] - { { x. у }. { x + dx, у - dy }. { x, у + dh } }; //143 POINT R[3] = { { x - dx, у - dy }, { x, у - dy - dy }. { x, у } }; // 061 PlgBltChDC. P. hMemDC. 0. 0, w. h. hMask. 0, 0) PlgBlt(hDC. Q. hMemDC. 0. 0. w. h, hMask, 0. 0) PlgBltChDC. R. hMemDC, 0. 0. w. h. hMask. 0. 0) void MaskCube(HDC hDC. int size, int x. int y. int w. int h. HBITMAP hBmp. HDC hMemDC. bool mask, bool bSimulate) { HBITMAP hMask - NULL; if ( mask ) hMask = CreateBitmap(w, h, 1. 1. NULL); SelectObject(hMemDC, hMask); PatBltChMemDC. 0, 0, w, h. BLACKNESS); RoundRect(hMemDC. 0. 0, w. h. w/2. h/2); } int dx - size * 94 / 100; // cos(20) int dy - size * 34 / 100; // sin(20) SelectObjectChMemDC. hBmp); DrawCube(hDC. x+dx. y+size, size, dx, dy, hMemDC, w, h, hMask); if ( hMask ) DeleteObject(hMask);
630 Глава 11. Нетривиальное использование растров На рис. 11.5 изображен результат вывода двух кубов. Первый куб рисуется с деревянной текстурой без маски, в результате чего изображение получается однородным. На гранях второго куба выводится растр, сгенерированный программой для построения множества Мандельброта, с маской в виде прямоугольника с закругленными углами. Рис. 11.5. Трехмерный куб, созданный с помощью функции PlgBIt Эффектно, не правда ли? К сожалению, программа работает только в системах семейства NT (даже не в Windows 98!). Чтобы вы лучше усвоили описанные выше возможности GDI, было бы полезно создать реализацию PlgBIt, работающую на всех платформах Win32. Давайте попробуем это сделать. В листинге 11.5 приведена функция G_P1gB1t, имитирующая PlgBIt. Листинг 11.5. Реализация PlgBIt BOOL G_PlgBlt(HDC hdcDest. const POINT * pPoint. HDC hdcSrc. int nXSrc. int nYSrc, int nWidth. int nHeight. HBITMAP hbmMask, int xMask. int yMask) { KReverseAffine map(pPoint); if ( map.SimpleO ) // Отсутствие сдвига и поворота { int x - pPoint[0].x; int у = pPoint[0].y; int w - pPoint[l].x-pPoint[0].x; int h - pPoint[2].y-pPoint[0].y; if ( hbmMask ) // маска: if (M) the S else D, S A (~M & (SAD))
Прозрачные растры 631 { StretchBltChdcDest. х, у. w. h. hdcSrc. nXSrc. nYSrc. nWidth, nHeight, SRCINVERT); StretchTileChdcDest. x. y. w. h. hbmMask. xMask. yMask. nWidth. nHeight. 0x220326); return StretchBltChdcDest. x. y. w. h. hdcSrc. nXSrc. nYSrc. nWidth. nHeight. SRCINVERT); } else return StretchBltChdcDest. x. y. w. h. hdcSrc. nXSrc. nYSrc. nWidth. nHeight. SRCCOPY); } map.Setup(nXSrc. nYSrc. nWidth. nHeight): HDC hdcMask - NULL; int maskwidth - 0; int maskheight= 0; if ( hbmMask ) { BITMAP bmp; GetObjectChbmMask. sizeof(bmp). & bmp); maskwidth = bmp.bmWidth; maskheight = bmp.bmHeight; hdcMask « CreateCompatibleDC(NULL): SelectObjectChdcMask. hbmMask); } for Сint dy=map.miny; dy<=map.maxy; dy++) for (int dx^map.minx; dx<=map.maxx; dx++) { float sx. sy; map.MapCdx. dy. sx. sy); if ( Csx>=nXSrc) && (sx<=(nXSrc+nWidth)) ) if ( (sy>=nYSrc) && (sy<=(nYSrc+nHeight)) ) if ( hbmMask ) { if ( GetPixel(hdcMask. ((int)sx+xMask) % maskwidth. Uint)sy+yMask) % maskheight) ) SetPixeKhdcDest. dx. dy. GetPixeKhdcSrc. (int)sx. (int)sy)); } else SetPixeKhdcDest. dx. dy. GetPixeKhdcSrc. (int)sx, (int)sy)); } if ( hdcMask ) DeleteObject(hdcMask); return TRUE; }
632 Глава 11. Нетривиальное использование растров Сначала рассмотрим случай без поворотов и сдвигов. При отсутствии сдвигов и поворотов PlgBlt реализуется несколькими вызовами StretchBlt с простыми растровыми операциями. Функция G_PlgBlt создает в стеке экземпляр класса KReverseAffine, выполняющего преобразование. Если преобразования сдвига и поворота не выполняются, метод KReverseAffine: :Simple возвращает TRUE. Функция проверяет, был ли передан при вызове действительный манипулятор маски, и если не был — вызывает StretchBlt с операцией SRCC0PY. Если маска передается, мы должны имитировать ее несколькими вызовами функций. Вспомните, что говорилось выше: если пиксел маски окрашен в белый (1) цвет, пиксел приемника заменяется пикселом источника; в противном случае пиксел приемника остается без изменений. В булевой алгебре эта операция выражается формулой M&S|~S&D, или D^(M&(S^D)). При прямой реализации этой формулы нам потребуется промежуточный растр, поскольку D используется дважды. Но если перейти к формуле S^(~M&(S^D)), S будет использоваться дважды, a D — только один раз. Из формулы видно, как реализовать семантику применения маски. Сначала D преобразуется в S^D операцией SRC INVERT, затем новый приемник D объединяется с ~М операцией AND, после чего выполняется еще одна операция SRC INVERT с S. Для второй операции маска играет роль источника с учетом возможного мозаичного повторения. Для выполнения операции -S&D используется безымянная операция с ROP-кодом 0x220326. Обратите внимание: при выводе растра маски используется функция Stretch- Ti I e, обеспечивающая мозаичное повторение маски на приемной поверхности. Если используется преобразование сдвига или поворота, нам придется поработать по-настоящему. Программа вызывает метод KReverseAffine::Setup для настройки обратного аффинного преобразования из параллелограмма приемной поверхности в прямоугольник поверхности-источника. Этот способ очень часто применяется при обработке поворотов и сдвигов растров. При отображении пикселов источника на приемную поверхность при растяжении возникают пробелы, а сжатие приведет к дублированию вычислений. Следуя в обратном направлении, от приемника к источнику, мы гарантируем, что каждый пиксел приемника будет обработан, и притом ровно один раз; растяжение и сжатие при этом обрабатывается автоматически. Функция Setup также вычисляет ограничивающий прямоугольник для приемного параллелограмма. После подготовки совместимого контекста устройства для маски программа начинает в цикле перебирать все точки в ограничивающем прямоугольнике параллелограмма. При этом программа отображает каждую точку прямоугольника в координатное пространство растра-источника и проверяет ее принадлежность исходному прямоугольнику (поскольку ограничивающий прямоугольник больше параллелограмма). При хранении координат отображенной точки приемника и сравнениях с границами источника используются вещественные числа. Слишком ранний переход к целым числам приведет к ошибкам при выводе некоторых граничных пикселов вследствие погрешностей округления. Если точка принадлежит исходному прямоугольнику, значит, она входит и в параллелограмм, поэтому мы переходим к вычислению значения пиксела. При наличии маски программа читает соответствующий пиксел маски. Если этот пиксел окрашен в белый (1) цвет, пиксел источника выводится на приемной поверхности; в противном случае приемник не изменяется. Тем самым мы успеш-
Прозрачные растры 633 но реализовали семантику применения маски. Если маска не задана, пиксел источника просто копируется в приемник. Обратите внимание на прибавление xMask и yMask к координатам растра-источника — тем самым мы учитываем относительный сдвиг маски. Мозаичный эффект достигается вычислением остатка от деления координаты на соответствующий размер маски. Вспомогательный код и класс KReverseAffine приведены в листинге 11.6. Листинг 11.6. Вспомогательный код и класс для имитации PlgBIt // Параметры dx, dy, dw. dh определяют приемный прямоугольник // Параметры sw. sh определяют размеры прямоугольника-источника // Параметры sx. sy определяют начальную точку в растре-источнике, // который дублируется до размеров sw x sh BOOL StretchTile(HDC hDC. int dx. int dy. int dw. int dh. HBITMAP hSrc. int sx. int sy. int sw. int sh. DWORD гор) { BITMAP bmp; if ( ! GetObject(hSrc, sizeof(BITMAP). & bmp) ) return FALSE; HDC hMemDC = CreateCompatibleDC(NULL); HGDIOBJ hOld = SelectObject(hMemDC. hSrc); int syO = sy % bmp.bmHeight; // Смещение текущей плитки // по оси у for (int y=0; y<sh; у+=(bmp.bmHeight - syO)) { int height = min(bmp.bmHeight - syO. sh - y) int sxO = sx % bmp.bmWidth; for (int x=0; x<sw; x+=(bmp.bmWidth - sxO)) { int width = min(bmp.bmWidth - sxO. sw - x): // Текущая // ширина плитки StretchBlt(hDC. dx+x*dw/sw, dy+y*dh/sh, dw*width/sw. dh*height/sh. hMemDC, sxO. syO, width, height, гор); sxO = 0; // После первой плитки в ряду перейти к полной ширине } syO = 0; // После первого ряда перейти к полной высоте плиток } SelectObjectChMemDC. hOld); DeleteObject(hMemDC); Продолжение & // Текущая высота // плитки // Смещение текущей // плитки по оси х
634 Глава 11. Нетривиальное использование растров Листинг 11.6. Продолжение return TRUE; void minmaxdnt xO. int xl. int x2. int x3. int & minx, int & maxx) { if ( xO<xl ) { minx = xO; maxx = xl; } else { minx = xl; maxx = xO; } if ( x2<minx) minx = x2; else if ( x2>maxx) maxx = x2; if ( x3<minx) minx = x3; else if ( x3>maxx) maxx - x3; class KReverseAffine : public KAffine { int xO. yO. xl. yl. x2. y2; public: int minx, maxx, miny. maxy; KReverseAffine(const POINT * pPoint) { xO - pPoint[0].x: // Р0 PI yO - pPoint[0].y; // xl - pPoint[l].x; // yl - pPoint[l].y; // P2 P3 x2 - pPoint[2].x; y2 - pPoint[2].y; } bool Simple(void) const { return (yO--yl) && (x0==x2); } void Setup(int nXSrc, int nYSrc, int nWidth. int nHeight) { MapTri(xO. yO. xl. yl. x2, y2. nXSrc. nYSrc, nXSrc+nWidth, nYSrc. nXSrc. nYSrc+nHeight); minmax(xO. xl. x2, x2 + xl - xO. minx, maxx); minmax(yO, yl, y2. y2 + yl - yO. miny. maxy): Мы только что реализовали свой первый алгоритм с поддержкой поворотов и сдвигов, а также создали замену для мощной функции PlgBlt. Если выполнить эту программу, она построит почти такой же объемный куб, как на рис. 11.5. Правда, работает она примерно в 7 раз медленнее, поскольку в ней используются медленные функции GetPixel/SetPixel. Позднее в этой главе будут описаны приемы прямого доступа к пикселам, заметно повышающие быстродействие программы.
Прозрачные растры 635 Кватернарные растровые операции: MaskBIt Вероятно, какому-то высшему существу из Microsoft показалось, что тернарные растровые операции недостаточно сложны, поэтому к ним нужно добавить кватернарные растровые операции, зависящие от четырех переменных. Это выглядит довольно странно, учитывая, что в GDI имя было присвоено лишь одной тернарной растровой операции, зависящей от узора, источника и приемника — PATPAINT (P|~S|D). Мы даже не знаем, как использовать PATPAINT, хотя в предыдущем разделе было продемонстрировано применение некоторых тернарных операций, зависящих от всех трех переменных. Код тернарной растровой операции можно рассматривать как комбинацию кодов^ двух бинарных растровых операций. Старшая половина кода тернарной растровой операции используется в тех случаях, когда Р = 1; младшая половина используется, когда Р = 0. Для примера рассмотрим растровую операцию тождественной замены с кодом ОхАА; как старшая, так и младшая половина равна ОхА. Следовательно, результат растровой операции вообще не зависит от кисти. Код D равен ОхА, поэтому результат всегда представляет собой D, то есть исходное состояние приемника. Теперь рассмотрим операцию PATINVERT с кодом 0х5А. Старшая половина равна 0x5, а младшая — ОхА. Следовательно, когда Р - 1, используется растровая операция ~D, а когда Р = 0 — операция D, что дает PXD. Четвертым фактором в кватернарных растровых операциях является монохромный растр маски. Код кватернарной растровой операции состоит из двух кодов тернарных операций: основной и фоновой. Основная операция используется, если пиксел маски равен 1, а фоновая операция используется для пикселов маски, равных 0. В GDI определен макрос MAKER0P4, объединяющий два 24-разрядных тернарных кода в один 32-разрядный кватернарный код. #define MAKER0P4(fore, back) (DWORD) ((((back)«8) & OxFFOOOOOO) | (fore) Макрос берет 8-разрядный индекс растровой операции, сдвигает его 8 бит влево и объединяет с 24-разрядным кодом основной операции. В результате образуется 32-разрядный код кватернарной операции. Структура кватернарного ROP- кода изображена на рис. 11.6. Полный код основной операции 24 бита k-Кодировка формулы основной операции 16 бит-Н / ' '" ' ' >| Индекс : фоновой операции Индекс основной операции ' ^ Ор5 Ор4 ОрЗ Ор2 Ор1 Not ( N Parse String Offset k Полный код кватернарной операции 32 бита ► Рис. 11.6. Код кватернарной растровой операции Маска в кватернарных операциях не является равноправным фактором, поскольку она ограничивается всего двумя значениями: 1 (белый) и 0 (черный). Вы не сможете создать цветную маску, цветные пикселы которой будут объединяться с цветными пикселами кисти, источника и приемника. Кватернарные
636 Глава 11. Нетривиальное использование растров операции создавались прежде всего как простое и эффективное средство прозрачного вывода растров, a MaskBU — единственная функция GDI, получающая код кватернарной растровой операции. Понять, как работает функция MaskBU, не так уж сложно. Первые пять параметров определяют прямоугольник на приемной поверхности. Следующие три параметра определяют прямоугольник на поверхности источника, размеры которого совпадают с размерами приемной поверхности. Это те же восемь параметров, которые используются в BitBU. Впрочем, парной функции StretchMaskBlt (по аналогии с парой BitBU/StretchBU) не существует. Если приложение хочет выполнить масштабирование при вызове MaskBU, оно должно соответствующим образом настроить логические системы координат. Следующие три параметра, hbmMask, xMask и yMask, определяют растр маски. Маска дублируется по мозаичному принципу по аналогии с маской PlgBU. Последний параметр MaskBU определяет кватернарную растровую операцию. Если пиксел маски равен 1, новый пиксел приемной поверхности определяется пикселами кисти, источника и приемника, объединенными основной растровой операцией; в противном случае используется фоновая растровая операция. Найти хороший пример, демонстрирующий применение MaskBU, непросто, поэтому мы снова воспользуемся примером с выводом значков: void MaskBltDrawIcon(HDC hDC. int x, int y, HICON hlcon) { ICONINFO iconinfo; GetIconInfo(hIcon, & iconinfo); BITMAP bmp; GetObject(iconinfo.hbmMask. sizeof(bmp), & bmp); HDC hMemDC - CreateCompatibleDC(NULL); HGDIOBJ hOld = SelectObjectChMemDC. iconinfo.hbmColor); MaskBltChDC. x. y. bmp.bmWidth, bmp.bmHeight, hMemDC. 0,0. iconinfo.hbmMask. 0. 0. MAKER0P4(SRCINVERT. SRCCOPY)); SelectObject(hMemDC. hOld); DeleteObject(i coni nfо.hbmMask); DeleteObject(i coni nfо.hbmColor); DeleteObject(hMemDC); } В отличие от предыдущей программы мы обошлись всего одним вызовом функции. По сравнению с функцией MaskBitmapNT, использующей узорную кисть и экзотическую растровую операцию 0х6С01Е8 (SX(PXD)), мы передаем маску непосредственно при вызове MaskBU без применения узорной кисти. В этом примере используется кватернарная растровая операция MAKER0P4 (SRC IN VERT, SRCCOPY), которая выполняет логическую операцию XOR для пикселов маски, равных 1, и простое копирование для нулевых пикселов маски. Эти операции соответствуют правилам вывода значков. На практике единичным основным пикселам соответствуют нулевые пикселы цветного растра, поэтому операция XOR не изменяет пиксела приемника.
Прозрачные растры 637 Имитация MaskBlt Функция MaskBlt обеспечивает простую концептуальную модель выполнения различных растровых операций с основными и фоновыми пикселами. К сожалению, приложения, использующие MaskBlt, работают только в операционных системах семейства Windows NT. В этом разделе мы разработаем модель функции MaskBlt для других систем. Моделирование MaskBlt — очень хорошее упражнение, позволяющее лучше разобраться в применении растровых операций. Сразу договоримся, что мы не будем реализовывать растровые операции на уровне пикселов, поскольку для этого нам потребуется запрограммировать все 256 тернарных растровых операций, от которых зависит функция MaskBlt. Вместо этого мы попробуем имитировать MaskBlt при помощи нескольких тернарных операций. Конечно, это приведет к некоторому снижению быстродействия, но потери оправдываются познавательной ценностью такого упражнения. Функция GMaskBlt (листинг 11.7) полностью реализует все возможности BitBlt. Задача разбивается на несколько подзадач — от простых, использующих не более трех растровых операций, до более общих, требующих временного растра и шести растровых операций. Функция сначала извлекает из кода кватернарной ROP индексы основной и фоновой операции. Как было сказано выше, функция MaskBlt должна использовать основную растровую операцию для единичных пикселов маски (белых) и фоновую операцию для нулевых пикселов (черных). Следовательно, наша реализация должна быть ориентирована на выполнение двух ROP: основной и фоновой. Листинг 11.7. Имитация MaskBlt BOOL TriBitBlt(HDC hdcDest. int nXDest. int nYDest, int nWidth, int nHeight, HDC hdcSrc. int nXSrc, int nYSrc, HBITMAP hbmMask. int xMask, int yMask, DWORD ropl. DWORD rop2. DWORD rop3) { HDC hMemDC = CreateCompatibleDC(hdcDest); SelectObject(hMemDC, hbmMask); if ( (ropl»16)!=0xAA ) // not D BitBlt(hdcDest, nXDest. nYDest. nWidth. nHeight, hdcSrc, nXSrc. nYSrc, ropl); BitBlt(hdcDest. nXDest, nYDest. nWidth, nHeight. hMemDC. xMask, yMask. rop2); Del eteObjectC hMemDC); if ( (rop3»16)!=0xAA ) // not D return BitBltChdcDest. nXDest. nYDest. nWidth. nHeight. hdcSrc, nXSrc, nYSrc. rop3); else return TRUE; } inline bool D_independent(DWORD гор)
638 Глава 11. Нетривиальное использование растров return ((OxAA & гор)»1)== (0x55 & гор); } inline boo! S_independent(DWORD гор) { return ((OxCC & rop)»2)== (0x33 & гор): BOOL G_MaskBlt(HDC hdcDest, int nXDest, int nYDest. int nWidth. int nHeight. HDC hdcSrc. int nXSrc. int nYSrc. HBITMAP hbmMask. int xMask. int yMask, DWORD dwRop ) DWORD back = (dwRop » 24) & OxFF; DWORD fore = (dwRop » 16) & OxFF; if ( back==fore ) // основная операция совпадает с фоновой. // маска hbmMask не нужна return BitBlt(hdcDest. nXDest, nYDest. nWidth. nHeight. hdcSrc. nXSrc. nYSrc, dwRop & OxFFFFFF); // if (M) D=fore(P.S.D) else D=back(P.S.D) if ( D_independent(back) ) // Фоновая операция не зависит от D return TriBitBlt(hdcDest. nXDest. nYDest. nWidth, nHeight, hdcSrc. nXSrc. nYSrc. hbmMask. xMask, yMask. fore^back « 16. // ( fore^back. fore^back ) SRCAND, // ( fore'back, 0 ) (back'OxAA) « 16); // { fore, back } if ( D_independent(fore) ) // Основная операция не зависит от D return TriBitBlt(hdcDest. nXDest. nYDest. nWidth. nHeight. hdcSrc. nXSrc. nYSrc, hbmMask. xMask. yMask. (fore^back) « 16, // ( fore^back, fore^back ) 0x22 «16. // ( 0. fore'back ) (fore'OxAA) « 16); // { fore, back } // И основная, и фоновая операция зависят от D if ( S_independent(back) && S_independent(fore) ) return TriBitBlt(hdcDest. nXDest. nYDest. nWidth. nHeight. NULL, 0, 0, hbmMask, xMask, yMask, OxAA « 16. // ( D. D ) ( (fore & OxCC) || (back & 0x33) ) « 16. OxAA « 16); // И основная, и фоновая операция зависят от D // Либо основная, либо фоновая операция зависит от S HBITMAP hTemp = CreateCompatibleBitmap(hdcDest, nWidth, nHeight); HDC hMemDC = CreateCompatibleDC(hdcDest): SelectObject(hMemDC, hTemp); BitBltChMemDC. 0. 0. nWidth. nHeight. hdcDest. nXDest. nYDest, SRCCOPY); SelectObject(hMemDC. GetCurrentObject(hdcDest. 0BJ_BRUSH)); Продолжение^
Прозрачные растры 639 Листинг 11.7. Продолжение BitBlt(hMemDC. О, 0. nWidth. nHeight. hdcSrc, nXSrc, nYSrc. back « 16); // hMemDC содержит итоговое // фоновое изображение BitBltChdcDest. 0, 0. nWidth. nHeight. hdcSrc. nXSrc. nYSrc. fore « 16); // Основное изображение TriBitBlt(hdcDest. nXDest. nYDest. nWidth. nHeight. hMemDC. 0. 0. hbmMask. xMask. yMask. SRCINVERT. // ( fore'back. fore'back ) SRCAND. // ( fore'back, 0 ) SRCINVERT); // { fore, back } DeleteObject(hMemDC); DeleteObject(hTemp); return TRUE; } Если основная растровая операция совпадает с фоновой, растр маски не используется, а задача решается одним вызовом BitBlt. Если фоновая растровая операция не зависит от растра приемника, MaskBlt реализуется не более чем тремя вызовами BitBlt. При первом вызове используется растр-источник и растровая операция «основа XOR фон». При втором вызове используется маска и растровая операция SRCAND, в результате чего приемник переходит в состояние «if (M) (основаАфон) else 0». При третьем вызове используется растр-источник и операция «фон XOR D». В итоге приемная поверхность переходит в состояние «if (M) основа else фон» — именно этого мы и добивались. Рассмотрим пример. Допустим, основная растровая операция имеет формулу DXS, а фоновая — Р. Следовательно, результат, которого мы хотим добиться, — «if (M) DXS else P». Согласно приведенному выше алгоритму, первая растровая операция описывается формулой DXSXP. После применения маски с операцией SRCAND мы переходим к формуле «if (M) DXSXP else 0». Наконец, растр-источник используется повторно с операцией DXP, и результат равен «if (M) DXS else P». Почему мы требуем, чтобы фоновая растровая операция не зависела от приемника? Потому, что первая растровая операция изменяет состояние приемника. Для всех последующих растровых операций, использующих исходное состояние приемника, необходимо создать его копию. Аналогичным образом, если основная растровая операция не зависит от приемника, достаточно использовать в качестве второй операции операцию NOTSRCAND (0x22), а в качестве третьей — (основав). Еще один простой случай — когда и основная, и фоновая операции не зависят от источника. В этом случае мы можем интерпретировать маску как источник, сконструировать новую растровую операцию и выполнить BitBlt при выборе маски в совместимом контексте устройства. Индекс ROP вычисляется по формуле (основа&0хСС)|(фон&0хЗЗ), где ОхСС обозначает источник, а ОхЗЗ — «NOT источник». Как видите, мы можем разбирать ROP на составляющие и собирать их заново. Предположим, основной операцией является ROP PATCOPY (OxFO), а фоновой — PATINVERT (0х5А); обе операции не зависят от S. Новая рас-
640 Глава 11. Нетривиальное использование растров тровая операция вычисляется по формуле (0xF0&0xCC)|(0x5A&0x33), то есть 0хС0|0х12 = 0xD2, или PX(D&~S). Обратите внимание: в роли S в данном случае выступает растр маски. Данная формула означает, что для S = 1 должна использоваться операция Р, а для S = 0 — P^D. Если ни основная, ни фоновая операция не относятся к этим простым случаям, возникают проблемы. Мы знаем, что обе растровые операции (основная и фоновая) зависят от приемника, но не можем добиться нужного эффекта одним вызовом BitBlt. После первого вызова BitBlt приемник изменяется, поэтому все последующие ссылки на него будут относиться к измененному, а не к исходному состоянию приемника. Существует единственный выход — создать временный растр, скопировать в него приемную поверхность, а затем построить на ней фоновое изображение. После этого на главной приемной поверхности строится основное изображение, которое объединяется с фоновым изображением с использованием маски. Таким образом, функция MaskBlt моделируется несколькими вызовами BitBlt — от одного до шести. Цветовые ключи: TransparentBIt Обе рассмотренные функции, PlgBlt и MaskBlt, используют монохромный растр- маску для управления выводом растра-источника. Главный недостаток решений, основанных на применении масок, заключается в том, что мы должны создать два идеально совпадающих растра: источник и маску. Маска должна точно соответствовать источнику как по размерам, так и по мельчайшим деталям изображения. Построение этих растров требует большого количества монотонной работы. Решение этой проблемы стоит поискать в Голливуде, у специалистов по визуальным эффектам. В кино уже давно используется методика комбинированных съемок с применением так называемого «синего экрана». Сначала съемка производится на фоне равномерно освещенного экрана синего цвета. В процессе монтажа синий фон заменяется другим изображением-«подложкой». Например, актера можно снять в студии подвешенным на нескольких незаметных шнурах, а потом наложить полученное изображение на изображение неба; получится, что человек парит в воздухе. Кстати, синий цвет — не единственный из возможных, хотя при съемках он используется чаще всего. Существует и другой прием, работающий по тому же принципу, — все участки изображения, яркость которых превышает заданный порог (или наоборот, оказывается ниже его), заменяются участками другого изображения. Эти две методики основаны на применении так называемых цветовых ключей. В GDI на платформах Windows 98 и Windows 2000 поддержка цветовых ключей была представлена новой функцией TransparentBIt. При вызове функция TransparentBIt получает 11 параметров. Первые пять параметров определяют прямоугольник приемной поверхности устройства, следующая пятерка — прямоугольник на поверхности источника, а последний параметр — цветовой ключ, заданный в виде RGB-значения. Функция TransparentBIt копирует на приемную поверхность пикселы источника, не совпадающие с цветовым ключом; при необходимости изображение увеличивается или уменьшается. Обратите внима-
Прозрачные растры 641 ние: функция TransparentBlt, в отличие от StretchBIt, не поддерживает зеркального отражения. Если растр был предварительно обработан для применения цветового ключа, использовать функцию TransparentBlt очень легко. Давайте вернемся к выводу значков, но на этот раз — с помощью функции TransparentBlt. Вспомните: значок состоит из монохромной маски и цветного растра. Прозрачные участки цветного растра обычно окрашиваются в черный цвет. Если черный цвет не встречается в изображении, он обычно выбирается для обозначения прозрачности. Согласно общепринятому правилу, цветовой ключ, как правило, определяется цветом первого пиксела растра. Следующая функция определяет цвет первого пиксела растра значка и передает его в качестве цветового ключа при вызове Transpa- rentBI t. Таким образом, значок выводится всего одной функцией блиттинга. void TransparentBltDrawIconCHDC hDC. int x. int y, HICON hlcon) { ICONINFO iconinfo; GetlconlnfoChlcon, & iconinfo); BITMAP bmp; GetObject(iconinfo.hbmMask, sizeof(bmp). & bmp); HDC hMemDC = CreateCompatibleDC(NULL); HGDIOBJ hOld = SelectObject(hMemDC. iconinfo.hbmColor); COLORREF crTrans = GetPixel(hMemDC. 0. 0); TransparentBlt(hDC. x. y. bmp.bmWidth, bmp.bmHeight. hMemDC. 0. 0. bmp.bmWidth. bmp.bmHeight. crTrans); SelectObject(hMemDC. hOld): DeleteObj ect(i coni nfо.hbmMask): DeleteObject(i coni nfо.hbmCol or); DeleteObject(hMemDC); } Функция TransparentBlt весьма эффектна, особенно учитывая ее «голливудское» происхождение. К сожалению, она поддерживается только в Windows 98, Windows 2000 и последующих системах. В Windows NT 4.0 поддержка TransparentBlt отсутствует. Эта функция экспортируется не из GDI32.DLL, а из MSIM32.DLL, поэтому к вашей программе должна быть подключена дополнительная библиотека MSIMG32.DLL По имеющейся информации, реализация TransparentBlt в Windows 98 приводит к утечке ресурсов, поэтому широко использовать эту функцию не рекомендуется. Короче, у нас достаточно причин для создания собственной реализации, не зависящей от платформы. Имитация TransparentBlt Одним из важнейших этапов в реализации TransparenBIt является построение растра маски по цветовому ключу. Монохромные DDB-растры обладают очень удобными средствами для создания масок. Вспомните: когда цветное изображение преобразуется в монохромный растр, все пикселы, цвет которых совпадает с фоновым цветом приемного контекста, преобразуются в 1 (белый), а остальным
642 Глава 11. Нетривиальное использование растров пикселам присваивается 0 (черный). Также следует учитывать, что белый цвет фона назначается контексту устройства по умолчанию. Для растра маски также необходим совместимый контекст устройства. В некоторых приложениях расходы по созданию маски и совместимого контекста устройства для каждого вывода растра могут оказаться неприемлемыми. В листинге 11.8 приведена реализация TransparentBU, использующая вспомогательный класс KDDBMask для управления растром маски и совместимым контекстом устройства. Функция GTransparentBU создает экземпляр класса KDDBMask в стеке, создает маску, а затем использует ее для прозрачного вывода исходного растра. В своих приложениях вы можете вынести экземпляр маски на более высокий уровень, чтобы избежать его многократного создания. Листинг 11.8. Имитация TransparentBIt BOOL G_TransparentBlt(HDC hdcDest. int nDxo. int nDyO, int nDw. int nDh, HDC hdcSrc. int nSxO. int nSyO. int nSw, int nSh. UINT crTransparent) { KDDBMask mask; mask.Create(hdcSrc, nSxO, nSyO. nSw, nSh, crTransparent); return mask.TransBlt(hdcDest, nDxO, nDyO. nDw, nDh. hdcSrc. nSxO, nSyO, nSw, nSh); } class KDDBMask { HDC mJiMemDC; HBITMAP mJiMask; HBITMAP mJiOld; int mjiMaskWidth; int mjiMaskHeight; void Release(void) { if ( mJiMemDC ) { SelectObject(m_hMemDC. mJiOld); DeleteObject(m_hMemDC); mJiMemDC = NULL; m_h01d = NULL; } if ( mJiMask ) { DeleteObject(m_hMask); mJiMask = NULL; } } public: KDDBMask() {
Прозрачные растры 643 mJiMemDC - NULL; mJiMask - NULL; m_h01d = NULL: } -KDDBMask0 { Releasee); } BOOL Create(HDC hDC. int nX. int nY. int nWidth. int nHeight. UINT crTransparent); BOOL ApplyMask(HDC HDC. int nX. int nY. int nWidth. int nHeight. DWORD Rop); BOOL TransBlt(HDC hdcDest. int nDxO. int nDyO. int nDw. int nDh. HDC hdcSrc. int nSxO. int nSyO. int nSw. int nSh); // Создать монохромный растр маски по исходному DC BOOL KDDBMask::Create(HDC hDC. int nX. int nY. int nWidth. int nHeight. UINT crTransparent) { Releasee); RECT rect - { nX. nY. nX + nWidth. nY + nHeight }; LPtoDPChDC. (POINT *) & rect. 2); mjiMaskWidth = abs(rect.right - rect.left); mjiMaskHeight = absCrect.bottom - rect.top); // Получить настоящие // размеры // Создать совместимый контекст и монохромную маску mJiMemDC - CreateCompatibleDC(hDC); mJiMask = CreateBitmap(m_nMaskWidth. mjiMaskHeight. 1. 1, NULL); m_h01d - (HBITMAP) SelectObject(m_hMemDC. mJiMask); COLORREF oldBk = SetBkColor(hDC. crTransparent); // Ассоциировать // crTransparent с 1 // (белый цвет) BOOL rslt - StretchBlt(m_hMemDC. 0. 0. m_nMaskWidth. mjiMaskHeight. hDC. nX. nY. nWidth. nHeight. SRCCOPY); SetBkColor(hDC. oldBk); return rslt; BOOL KDDBMask::ApplyMask(HDC hDC. int nX. int nY. int nWidth. int nHeight. DWORD rop) { COLORREF oldFore = SetTextColor(hDC. RGB(0. 0. 0)); // Черный COLORREF oldBack - SetBkColor(hDC. RGB(255. 255. 255)); // Белый BOOL rslt = StretchBlt(hDC. nX. nY. nWidth. nHeight. mJiMemDC. 0. 0. mjiMaskWidth, mjiMaskHeight. rop); Продолжение &
644 Глава 11. Нетривиальное использование растров Листинг 11.8. Продолжение SetTextColor(hDC, oldFore); SetBkColor(hDC, oldBack); return rslt; } // D=D"S. D=D & Mask. D=D"S --> if (Mask==l) D else S BOOL KDDBMask::TransBlt(HDC hdcDest. int nDxO. int nDyO. int nDw, int nDh. HDC hdcSrc. int nSxO. int nSyO. int nSw, int nSh) { StretchBlt(hdcDest, nDxO, nDyO. nDw, nDh, hdcSrc, nSxO. nSyO. nSw. nSh, SRCINVERT); // D"S ApplyMask(hdcDest. nDxO. nDyO. nDw, nDh, SRCAND); // if trans D"S else 0 return StretchBlt(hdcDest, nDxO. nDyO. nDw. nDh, hdcSrc, nSxO. nSyO. nSw. nSh. SRCINVERT); // if trans D else S } Метод KDDBMask:: Create создает монохромный растр по заданному контексту устройства на основании цветового ключа. Метод вычисляет размеры растра, отображая исходный прямоугольник на систему координат устройства (на случай, если в исходном контексте устройства не используется принятый по умолчанию режим отображения ММТЕХТ). Фактическое преобразование цветного растра в монохромный зависит от фонового цвета исходного контекста устройства. Метод KDDB: :TransB1t использует знакомую последовательность растровых операций SRCINVERT/SRCAND/SRCINVERT для достижения эффекта прозрачности. Прозрачность без маски Практически во всех методиках прозрачного вывода растров используется монохромный растр, играющий роль маски. В ресурсах курсоров и значков имеется встроенная маска; функциям PlgBIt и MaskBlt растр маски передается в числе параметров. Единственным исключением является функция TransparentBlt, работающая с цветовыми ключами. Впрочем, в нашей имитации мы вернулись к работе с масками. Возникает вопрос: как реализовать прозрачность без применения масок? Например, если маску по каким-либо причинам трудно создать или она поглощает много ресурсов? Существуют ли альтернативные решения? В этом разделе рассматриваются некоторые из этих альтернатив. Прозрачный вывод с использованием геометрических фигур Базовый алгоритм прозрачного вывода под управлением маски состоит из трех шагов.
Прозрачность без маски 645 1. Наложение рисунка на приемную поверхность операцией XOR. 2. Объединение маски с приемной поверхностью операцией AND. 3. Повторное наложение рисунка на приемную поверхность операцией XOR. После этих трех этапов область, соответствующая черному (0) цвету маски, заменяется изображением-источником, а остальные пикселы остаются без изменений. Обратите внимание на то, что маска используется всего один раз для закраски непрозрачных областей черным (0) цветом. Если область, образованную черными пикселами маски, можно описать в виде совокупности геометрических фигур, то вместо создания отдельного растра маску можно нарисовать командами заливки областей GDI. Ниже приведен пример рисования эллиптического DIB-растра без растра маски. BOOL OvalStretchDIBits(HDC hDC. int XDest. int YDest. int nDestWidth. int nDestHeight. int XSrc, int YSrc. int nSrcWidth. int nSrcHeight. const void *pBits, const BITMAPINFO *pBMI. UINT iUsage) { StretchDIBits(hDC. XDest. YDest. nDestWidth. nDestHeight. XSrc. YSrc. nSrcWidth. nSrcHeight. pBits. pBMI. iUsage. SRCINVERT); SaveDC(hDC); SelectObject(hDC. GetStockObject(BLACKJRUSH)): SelectObject(hDC. GetStockObject(BLACK_PEN)); Ellipse(hDC. XDest. YDest. XDest + nDestWidth. YDest + nDestHeight); RestoreDCChDC. -1); return StretchDIBits(hDC. XDest. YDest. nDestWidth. nDestHeight. XSrc. YSrc. nSrcWidth. nSrcHeight. pBits. pBMI. iUsage. SRCINVERT); } Эта функция сначала выводит источник при помощи функции StretchDIBits с растровой операцией SRCINVERT, а затем рисует эллипс функцией Ellipse. Повторный вызов StretchDIBits с операцией SRCINVERT гарантирует, что изображение будет выводиться только в областях, находящихся внутри эллипса. При использовании функции Oval StretchDIBits для рисования нетривиального растра возникает неприятное мерцание, поскольку инвертирование пикселов — относительно медленный процесс. Чтобы уменьшить мерцание, можно позаимствовать структуру цветного растра у значков, где прозрачные пикселы обычно окрашиваются в черный цвет. В этом случае, после окраски непрозрачной области приемника в черный цвет, можно воспользоваться раЬтровой операцией SRCPAINT для объединения источника с приемником. Предполагается, что мы можем модифицировать растр-источник таким образом, чтобы его прозрачные пикселы были окрашены в черный цвет; это можно сделать при помощи DDB или DIB-секции, выбранной в совместимом контексте устройства. Функция OvalStretchBlt иллюстрирует эту идею. BOOL OvalStretchBlt(HDC hDC. int XDest. int YDest. int nDestWidth. int nDestHeight. HDC hDCSrc. int XSrc. int YSrc. int nSrcWidth. int nSrcHeight) { // Окрасить источник за пределами эллипса в ЧЕРНЫЙ цвет
646 Глава 11. Нетривиальное использование растров SaveDC(hDCSrc); BeginPath(hDCSrc); RectangleChDCSrc. XSrc, YSrc. XSrc + nSrcWidth+1. YSrc + nSrcHeight+1); EllipseChDCSrc. XSrc. YSrc, XSrc + nSrcWidth. YSrc + nSrcHeight); EndPath(hDCSrc); Sel ectObject (hDCSrc. GetStockObject (BLACKJRUSH)) ; SelectObj ect(hDCSrc. GetStockObj ect(BLACK_PEN)); FillPath(hDCSrc); RestoreDCChDCSrc, -1): // Нарисовать ЧЕРНЫЙ эллипс на приемной поверхности SaveDC(hDC); SelectObject(hDC. GetStockObj ect(BLACK_BRUSH)): SelectObject(hDC. GetStockObject(BLACK_PEN)); Ellipse(hDC. XDest. YDest. XDest + nDestWidth. YDest + nDestHeight): RestoreDCChDC. -1); // Объединить источник с приемником return StretchBltChDC. XDest. YDest. nDestWidth, nDestHeight. hDCSrc. XSrc. YSrc. nSrcWidth. nSrcHeight. SRCPAINT); } Сначала мы при помощи функций для работы с траекториями закрашиваем область за пределами эллипса черной кистью. Пикселы внутри эллипса остаются без изменений. Кстати говоря, если растр выводится несколько раз, эту операцию можно выполнить всего один раз и многократно использовать ее результат. Второй шаг — стирание приемной поверхности — остается прежним. На третьем шаге вместо SRC INVERT используется растровая операция SRCPAINT. В результате мерцание должно уменьшиться, поскольку только пикселы эллипса переходят от исходного цвета к черному, а затем заменяются пикселами источника. Чтобы полностью устранить мерцание, следует выполнить весь вывод на внеэкранном растре, а затем скопировать его на экран. Прозрачный вывод с использованием отсечения Если маска имеет простую геометрическую форму, прозрачный вывод без мерцания легко реализуется с использованием региона отсечения. К сожалению, программисты часто недооценивают возможности регионов отсечения — в основном из-за того, что в 16-разрядном интерфейсе GDI поддержка регионов оставляла желать лучшего. Следующая функция выводит овальный растр, для чего используется одна простая операция блиттинга с применением региона отсечения. BOOL ClipOvalStretchDIBits(HDC hDC. int XDest. 1nt YDest. int nDestWidth. int nDestHeight. int XSrc. int YSrc. int nSrcWidth, int nSrcHeight. const void *pBits, const BITMAPINFO *pBMI, UINT iUsage) { RECT rect = { XDest. YDest. XDest + nDestWidth. YDest + nDestHeight }; LPtoDPChDC. (POINT *) & rect. 2); HRGN hRgn = CreateEl1ipticRgnlndirect(& rect);
Прозрачность без маски 647 SaveDC(hDC); SelectClipRgn(hDC. hRgn); DeleteObject(hRgn): BOOL rslt - StretchDIBits(hDC. XDest, YDest, nDestWidth, nDestHeight. XSrc, YSrc, nSrcWidth, nSrcHeight. pBits, pBMI. iUsage. SRCCOPY); RestoreDCChDC. -1); return rslt; } Вероятно, стоит напомнить некоторые факты, относящиеся к работе с регионами и отсечением. Регионы отсечения определяются в координатах устройства, а не в логических координатах. Следовательно, перед тем как создавать регион вызовом CreateElipticRgnlndirect, функция ClipOvalStretchDIBits сначала должна отображать приемный прямоугольник в систему координат устройства приемного контекста. Предварительная подготовка изображений В некоторых ситуациях прозрачный растр должен отображаться только на поверхностях с однородным цветом фона. Например, растровые метки команд меню обычно отображаются на фоне меню, однородный цвет которого определяется текущей конфигурацией системы. Чтобы обеспечить максимальную эффективность при работе с такими изображениями, следует перед выводом подготовить изображение в итоговом виде. В Win32 API появилась чрезвычайно удобная функция, которая помогает в решении этой задачи: HANDLE LoadlmageCHINSTANCE hinst, LPCTSTR IpszName. UINT uType. int cxDesired. int cyDesired, UINT fuLoad); Функция Load I mage загружает курсоры мыши, значки и растры из ресурсов или внешних файлов. Для курсоров и значков можно задать желательный размер. Для растров Loadlmage может создать DDB или DIB-секцию. Отображая некоторые цвета изображения на другие цвета, можно подготовить изображение к выводу. При загрузке из ресурса первый параметр задает манипулятор экземпляра модуля; в этом случае параметр IpszName определяет имя ресурса. Он также может определять имя внешнего файла, если в параметре fuLoad передается флаг LR_LOADFROMFILE. Параметр uType может быть равен IMAGEJITMAP, IMAGE_CURSOR или IMAGE_ICON (для растров, курсоров мыши и значков соответственно). Пара cxDesired/cyDesired используется только для определения желательных размеров курсора или значка. Последний параметр, fuLoad, управляет процессом преобразования. Например, флаг LR_CREATEDIBSECTION указывает на то, что вместо DDB следует создать DIB-секцию. Флаг LR_L0ADMAP3DC0L0RS отображает пикселы с RGB(128.128.128) на COLORJDSHADOWS, RGBC192,192,192) - на COLORJDFACE, a RGBC233, 233,233) — на C0L0RJ3DLIGHT. При указании флага LR_M0N0CHR0ME растр загружается в черно-белом формате. Флаг LR_TRANSPARENT отображает пикселы, цвет которых совпадает с цветом первого пиксела изображения, на системный цвет фона окна
648 Глава 11. Нетривиальное использование растров C0L0RWIND0W. Флаг LRVGACOLOR требует, чтобы в растрах использовались цвета VGA. За подробностями обращайтесь к документации MSDN. Среди этих флагов наибольший интерес вызывает LRJTRANSPARENT. При установке этого флага Loadlmage заменяет пикселы, цвет которых совпадает с цветом первого пиксела изображения, цветом C0L0RWIND0W. Следовательно, если растр выводится на фоне C0L0RWIND0W, вывод всего растра простейшей операцией SRCCOPY приведет к тому же эффекту, что и вывод растра с применением маски. Однако эта возможность может использоваться только в том случае, если растр задействует палитру и отображается на фоне системного цвета C0L0R_WIND0W. Почему в GDI нет функции, которая позволяла бы назначить произвольный цвет в качестве прозрачного? В следующем фрагменте показано, как при помощи функции Loadlmage загрузить серию изображений и создать простейшую анимацию. Мы используем изображение комара из DirectX SDK. Анимационная последовательность состоит из трех изображений с разными положениями ног и крыльев. При последовательном выводе растров с небольшим смещением возникает иллюзия движения. void TestLoadlmageCHDC hDC, HINSTANCE hlnstance) { HBITMAP hBitmap[3]; const nID [] » { IDB_M0SQUIT1. IDB_M0SQUIT2. IDB_M0SQUIT3 }; for (int i=0; i<3; i++) hBitmap[i] = (HBITMAP) LoadImage(hInstance. MAKEINTRESOURCE(nID[i]). IMAGEJITMAP. 0. 0. LR_LOADTRANSPARENT | LR_CREATEDIBSECTION ); BITMAP bmp; GetObject(hBitmap[0]. sizeof(bmp), & bmp); HDC hMemDC - CreateCompatibleDC(hDC); SelectObjectChDC. GetSysColorBrush(COLOR_WINDOW)); int lastx = -1; int lasty = -1; HRGN hRgn = CreateRectRgn(0. 0. 0. 0); for (i-0: i<600; i++) { SelectObject(hMemDC. hBitmap[iX3]); int newx = i; int newy - abs(200-iX400): if ( lastx! —1 ) { SetRectRgn(hRgn, newx. newy. newx+bmp.bmWidth, newy + bmp.bmHeight); ExtSelectClipRgn(hDC. hRgn. RGNJDIFF); PatBlt(hDC. lastx. lasty. bmp.bmWidth. bmp.bmHeight. PATCOPY); SelectClipRgn(hDC. NULL); }
Альфа-наложение 649 BitBUChDC, newx, newy, bmp.bmWidth, bmp.bmHeight, hMemDC, 0. 0, SRCCOPY); lastx = newx; lasty = newy; } DeleteObject(hRgn); DeleteObject(hMemDC); DeleteObject(hBitmap[0]); DeleteObject(hBitmap[l]); DeleteObject(hBitmap[2]); } При создании окна цвет фона по умолчанию равен C0L0R_WIND0W; функция Loadlmage заменяет им черный цвет (прозрачный). Следовательно, для вывода изображения достаточно одного вызова BitBlt с растровой операцией SRCCOPY. Для создания анимации мы должны вывести одно изображение, стереть его, перейти в новую позицию и повторить вывод и стирание. В нашем примере предыдущее изображение стирается функцией PatBU. Обратите внимание: поскольку мы работаем с чистым фоном окна, стирание сводится к простой закраске цветом C0L0RWIND0W. При более сложном фоне нам пришлось бы сохранить участок фона и восстановить его. Чтобы уменьшить мерцание, мы при помощи региона отсечения исключаем участок, на котором выводится новое изображение, из обновляемой области, что позволяет избавиться от повторного изменения пикселов экрана и обеспечивает плавность анимации. Альфа-наложение В нескольких последних разделах мы подробно рассматривали растровые операции. Если хорошенько подумать, во многих ситуациях поразрядные растровые операции не имеют особого смысла. Собственно, что вы получите при объединении двух пикселов поразрядными операциями AND, OR или XOR? Конечно, мы нашли практическое применение для некоторых простых растровых операций при выводе растров, раскраске монохромных изображений, фильтрации RGB- каналов и постепенного проявления растров. Операция AND обычно используется в сочетании с монохромной маской для удаления ненужных пикселов, а операция XOR — для избирательного объединения растров источника и приемника. Мы никогда не объединяем изображения слепо, не зная точно, что при этом произойдет. К сожалению, поразрядные растровые операции с цветными изображениями не всегда имеют осмысленную интерпретацию в реальном мире (не считая нескольких случаев, описанных выше). Базовая формула прозрачного вывода растров, (M&D) | (-M&S), читается следующим образом: «Если пиксел маски равен 1 (белый цвет), результатом операции является пиксел приемника; в противном случае использовать пиксел источника». В формуле используется семантика поразрядных операций булевой алгебры. При переходе к арифметическим операциям формула принимает вид M*D+(1-M)*S. Именно в ней и заключается вся сущность альфа-наложения. Альфа-наложением (alpha blending) называется методика графического вывода, в которой итоговый пиксел вычисляется в виде взвешенной суммы двух
650 Глава 11. Нетривиальное использование растров пикселов (источника и приемника). Весовой коэффициент источника обычно называется альфа-коэффициентом (а). Весовой коэффициент приемника равен 1 - а, где за единицу принимается максимальное цветовое значение. Альфа-наложение выполняется не поразрядно, а для- каждого цветового канала по отдельности. Нулевой альфа-коэффициент соответствует абсолютно прозрачным пикселам источника, а единичный — полностью непрозрачным пикселам. Для графических поверхностей с 24- или 32-разрядной кодировкой цвета концептуальные формулы альфа-наложения выглядят так: Dst.red = Src.red * alpha + (1-alpha) * Dst.red : Dst.green - Src.green * alpha + (1-alpha) * Dst.green ; Dst.blue = Src.blue * alpha + (1-alpha) * Dst.blue ; Dst.alpha = Src.alpha * alpha + (1-alpha) * Dst.alpha ; Альфа-наложение относится к числу новых возможностей, появившихся в Windows 98 и Windows 2000. Вся поддержка альфа-наложения состоит из одной структуры данных и одной функции. typedef struct _BLENDFUNCTION { BYTE BlendOp; BYTE BlendFlags; BYTE SouyrceConstantAlpha; BYTE AlphaFormat; } BLENDFUNCTION; BOOL AlphaBlend(HDC hdcDest. int nXOriginDest. int nYOriginDest, int nWidthDest. int nHeightDest, HDC hdcSrc. int nXOriginSrc. int nYOriginSrc. int nWidthSrc. int nHeightSrc. BLENDFUNCTION blendFunction); По своему прототипу функция AlphaBlend напоминает StretchBU. Первые пять параметров определяют приемный контекст устройства и прямоугольник приемной поверхности в логических координатах. Следующие пять параметров определяют контекст устройства источника и прямоугольник на поверхности источника в логических координатах. При этом действуют стандартные ограничения для контекста источника, то есть исходный прямоугольник должен находиться в контексте источника, а в последнем не могут действовать преобразования сдвига и поворота, приводящие GDI в замешательство. Обратите внимание: ограничения на контекст источника не позволяют напрямую использовать DIB с функцией AlphaBlend. Последний параметр, blendFunction, содержит структуру BLENDFUNCTION, передаваемую по значению. Эта структура заменяет код растровой операции, используемый при вызове StretchBU. Структура BLENDFUNCTION управляет процессом объединения двух растров, источника и приемника. Поле BlendOp определяет операцию наложения источника, однако единственным допустимым значением этого поля является AC_SRC_0VER, при котором растр-источник накладывается на приемник на основании альфа-коэффициентов источника. Поддержка альфа-наложения в OpenGL предусматривает и другие варианты (например, источник с постоянным цветом). Следующее поле, BlendFlags, должно быть равно нулю; его использование зарезервировано на будущее. Последнее поле, AlphaFormat, прини-
Альфа-наложение 651 мает два значения: 0 означает постоянный альфа-коэффициент, a ACSRCjALPHA — использование альфа-коэффициентов отдельных пикселов. Если поле AlphaFormat равно 0, для всех пикселов растра-источника используется одинаковый альфа-коэффициент, заданный в поле SourceAlphaConstant. Допустимые значения лежат в интервале 0-255, а не 0-1, как можно было бы предположить. В данном случае 0 означает полную прозрачность, а 255 — полную непрозрачность. Альфа-коэффициенты пикселов приемника равны 255- SourceConstantAlpha. В этом случае альфа-наложение выполняется по следующим формулам: Dst.red = Round((Src.red * SourceConstantAlpha + (255-SourceConstantAlpha) * Dst.red ))/255); Dst.green = Round((Src.green * SourceConstantAlpha + (255-SourceConstantAlpha) * Dst.green))/255); Dst.blue - Round((Src.blue * SourceConstantAlpha + (255-SourceConstantAlpha) * Dst.blue ))/255); Dst.alpha - Round((Src.alpha * SourceConstantAlpha + (255-SourceConstantAlpha) * Dst.alpha))/255): Если поле А1 phaFormat равно AC_SRC_ALPHA, данные альфа-канала должны входить в пикселы поверхности источника. Другими словами, это должен быть физический контекст устройства в 32-разрядном режиме или совместимый контекст устройства, в котором выбран 32-разрядный DDB-растр или DIB-секция. В любом случае каждый пиксел источника состоит из четырех 8-разрядных каналов: красного, зеленого, синего и альфа-канала. Альфа-канал каждого пиксела используется в сочетании с полем SourceConstantAlpha для объединения источника с приемником. Вычисления производятся по следующим формулам: Tmp.red - Round((Src.red * SourceConstantAlpha)/255); Tmp.green « Round((Src.green * SourceConstantAlpha)/255); Tmp.blue - Round((Src.blue * SourceConstantAlpha)/255); Tmp.alpha - Round((Src.alpha * SourceConstantAlpha)/255); beta - 255 - Tmp.alpha; Dst.red - Tmp.red + Round((beta * Dst.red )/255): Dst.green = Tmp.green + Round((beta * Dst.green )/255); Dst.blue - Tmp.blue + Round((beta * Dst.blue )/255): Dst.alpha = Tmp.alpha + Round((beta * Dst.alpha )/255): Внимательно рассматривая эти формулы, можно заметить, что альфа-коэффициент уровня пикселов Src.alpha применяется только к пикселам приемника, но не к пикселам источника. GDI предполагает, что альфа-коэффициент уже был внесен в данные растра-источника предварительным умножением. Вероятно, это сделано для удобства игрового программирования, где сцены могут быть предварительно сгенерированы во внешней программе или создаваться в результате работы других компонентов. Изображение с предварительным внесением альфа-данных напоминает прозрачный растр с черным фоном (цветной растр в значке); оно тоже ускоряет вывод за счет гибкости. Если значение SourceCons- tantAl pha равно 255, временную переменную Tmp вычислять не нужно. Если быстродействие критично для вашей программы, возможно, вас обеспокоит необходимость деления на 255 для каждого пиксела. На процессорах Intel полноценное деление занимает десятки тактов и потому считается исключи-
652 Глава 11. Нетривиальное использование растров тельно медленной операцией. Доверьтесь реализации GDI и современным компиляторам — они достаточно умны, чтобы заменить деление на константу умножением со сдвигом. Пример альфа-наложения с постоянным коэффициентом Проще всего реализовать альфа-наложение с постоянным коэффициентом. Для него даже не требуется, чтобы поверхность источника была 32-разрядной, поскольку альфа-коэффициент передается в структуре BLENDFUNCTION. Рассмотрим простой пример с наложением нескольких прямоугольников со сплошной заливкой: void SimpleConstantAlphaBlending(HDC hDC) { const int size = 100; for (int i=0; i<3; i++) { RECT rect = { i*(size+10) + 20. 20+size/3. i*(size+10) + 20 + size. 20+size/3 + size }; const COLORREF Color[] = { RGB(0xFF. 0. 0). RGB(0. OxFF. 0). RGB(0. 0. OxFF) }; HBRUSH hBrush = CreateSolidBrush(Color[i]); FillRect(hDC. & rect. hBrush); // Три исходных прямоугольника DeleteObject(hBrush); BLENDFUNCTION blend = { AC_SRC_OVER. 0. 255/2. 0 }; // альфа=0.5 AlphaBlend(hDC. 360+((3-i)*3)*size/3. 20+i*size/3. size. size. hDC. i*(size+10)+20, 20+size/3. size. size, blend); } } В этом примере источником и приемником является один и тот же контекст устройства. Сначала мы рисуем три однородных прямоугольника красного, зеленого и синего цвета, а затем накладываем их на фон окна функцией Alpha- Blend. При каждом вызове используется постоянный альфа-коэффициент 0,5. Сначала фон окна окрашен в сплошной белый цвет RGB(0xFF,0xFF,0xFF). После наложения красного прямоугольника пикселы окрашиваются в цвет RGB(0xFF,0x80,0x80). После наложения второго, зеленого прямоугольника пикселы на пересечении красного и зеленого прямоугольников принимают цвет RGB(0x80,0xBF,0x40). После наложения третьего, синего прямоугольника пересечение всех трех прямоугольников содержит пикселы с цветом RGB(0x40,0x60, 0x90). Возможно, это не совсем то, чего вы ожидали, но эта величина рассчитывается по формулам альфа-наложения: RGBC0x40.0x60.0x90) - RGB(OxFF.OxFF.OxFF) * 0.125 + RGB(OxFF.O.O) * 0.125 + RGB(O.OxFF.O) * 0.25 + RGB(0.0,0xFF) * 0.5
Альфа-наложение 653 Результат наложения показан на рис. 11.7. Рис. 11.7. Альфа-наложение цветных прямоугольников с постоянным коэффициентом Постепенное проявление и исчезновение растров В разделе «Прозрачные растры» была приведена функция последовательной «проявки» растра с применением растровых операций. Альфа-наложение предоставляет новые средства для постепенного вывода растров. Ниже приведен новый вариант функции, использующий альфа-наложение с постоянным коэффициентом. BOOL AlphaFade(HDC hDCDst. int XDst, int YDst. int nDstW. int nDstH, HDC hDCSrc. int XSrc. int YSrc. int nSrcW. int nSrcH) { for (int i=5; i>=l; i--) { // Альфа 1/5. 1/4, 1/3. 1/2. 1/1 BLENDFUNCTION blend - { AC_SRC_OVER. 0. 255 / i . 0 }; if ( ! AlphaBlendChDCDst. XDst. YDst. nDstW. nDstH. hDCSrc. YSrc. YSrc, nSrcW. nSrcH, blend): } return TRUE; } Функция AlphaFade накладывает растр-источник на приемную поверхность в пять этапов, с альфа-коэффициентами 1/5, 1/4, 1/3, 1/2 и 1/1. После первого вывода часть изображения уже присутствует в приемнике, поэтому накапливаемые коэффициенты будут равны 1/5, 2/5, 3/5, 4/5 и, наконец, 5/5. Прозрачные окна В Windows 98/2000 появился новый расширенный стиль окна. При установке флага WSEXLAYERED весь вывод в окно вместо непосредственного вывода на экран кэшируется в растре, размеры которого совпадают с размерами экрана. Далее содержимое этого растра может быть выведено на экран посредством альфа- наложения. Приложение даже может задать цветовой ключ для такого окна. Все пикселы, цвет которых совпадает с цветом ключа, будут прозрачными, то есть
654 Глава 11. Нетривиальное использование растров среди них будут видны пикселы, находящиеся под окном. Когда на экране появляются ранее закрытые части окна со стилем WS_EX_LAYERED, перерисовка со стороны приложения не нужна — GDI просто заново выводит на экран кэшированное содержимое растра. При правильной реализации этот стиль позволяет создавать новые визуальные эффекты и повышает быстродействие за счет затрат на хранение кэшированных растров. Стиль WS_EX_LAYERED указывается либо при вызове CreateWindowEx, либо позднее при помощи SetWIndowLong. После создания окна можно задать постоянный альфа-коэффициент для окна и необязательный цветовой ключ при помощи функции SetLayeredWi ndowAttri butes: BOOL SetLayeredWindowAttributes(HWND hWnd. COLORREF crKey, BYTE bAlpha. DWORD dwFlags): Параметр hWnd содержит манипулятор окна с флагом стиля WS_EX_LAYERED. Параметр dwFlags содержит один или оба флага LWA_C0L0RKEY и LWA_ALPHA. При использовании флага LWA_C0L0RKEY параметр сгКеу определяет цветовой ключ прозрачности. Для флага LWA_ALPHA параметр ЬАТ pha определяет постоянный альфа- коэффициент источника. Стиль WS_EX_LAYERED может использоваться только для окон верхнего уровня. Следующий фрагмент показывает, как создать окно со стилем WS_EX_LAYERED в функции окна: switch ( uMsg) { case WM_CREATE: mJiWnd = hWnd; SetWi ndowLong(m_hWnd. GWLJXSTYLE. GetWindowLong(m_hWnd. GWLJXSTYLE) | WS_EX_LAYERED); SetLayeredWindowAttributes(mJiWnd. RGB(0. 0, 1). OxCO, LWA_ALPHA | LWA_C0L0RKEY ); return 0; } В этом фрагменте функция GetWindowLong возвращает текущие флаги расширенных стилей, которые после объединения с WSEXLAYERED записываются на прежнее место. При вызове SetLayeredWi ndowAttri butes устанавливается альфа-коэффициент 0,75 (ОхСО/255) и цветовой ключ RGB (0,0,1) — несколько необычный цвет, очень близкий к черному. Выполнение этого фрагмента заметно влияет на внешний вид окна. Практически весь вывод в окне, включая дочерние окна, становится полупрозрачным, хотя и выполняется гораздо медленнее. Впрочем, меню или диалоговые окна остаются непрозрачными. На рис. 11.8 показан пример — окно с DIB-растром, наложенное на исходный текст программы в MSVC IDE. Обратите внимание: рисунок был получен сохранением всего экрана. Если сохранить только содержимое окна, вместо экранного изображения вы получите только содержимое кэшированного растра. Как обычно бывает с новыми технологиями, прозрачные окна выводятся значительно медленнее обычных. Также огорчает и то, что меню выводятся непрозрачными, а окна не перерисовываются так, как положено.
Альфа-наложение 655 у - pBMI->bmiHeader.biHeight - 1 - у; BYTE * D - pBitsDst + GetOffsetCpBMIDst, dx. j + dy); BYTE * S « pBitsSrc + GetOffsetCpBMISrc. sx, j + sy); Рис. 11.8. Прозрачное окно Альфа-канал: класс AirBrush Во всех примерах, приведенных выше, используется постоянный альфа-коэффициент, применяемый к каждому пикселу растра-источника. Возможности постоянных альфа-коэффициентов ограничены. Например, они даже не справляются с задачей прозрачного вывода растров, которая легко решается при помощи маски. Растр маски можно рассматривать как альфа-канал с кодировкой 1 бит/пиксел, отделенный от основного растра. Давайте рассмотрим некоторые типичные применения альфа-каналов. Хотя функция AlphaBlend позволяет выбирать в совместимом контексте устройства как DDB, так и DIB-секции, при использовании альфа-каналов можно работать только с DIB-секциями. Дело в том, что в экранных режимах, не использующих 32-разрядную кодировку цвета, 32-разрядный DDB-растр не будет совместим с экранным контекстом устройства. В 32-разрядной DIB-секции каждый пиксел хранится в 4 байтах. Первый три байта обычно содержат данные синего, зеленого и красного каналов, а в последнем байте хранится альфа-канал. AlphaBlend — единственная функция, которая читает и записывает данные альфа-канала, поэтому GDI не оказывает особой помощи в подготовке этих данных. При использовании альфа-наложения на уровне отдельных пикселов AlphaBlend предполагает, что пикселы источника были предварительно умножены на альфа-коэффициент. Следовательно, чтобы воспользоваться альфа-каналом, мы должны обладать прямым доступам к пикселам DIB-секции.
656 Глава 11. Нетривиальное использование растров В современных графических редакторах обычно поддерживаются разные типы кистей (не путать с кистями GDI!), предназначенных для рисования больших точек и толстых линий. Кисть в графическом редакторе определяется своей формой, цветом, ориентацией, жесткостью и другими хитроумными атрибутами. Например, довольно часто встречается круглая цветная кисть с жесткостью в 50 % — характеристикой, определяющей скорость изменения пикселов кисти от однородного цвета в центре до абсолютно прозрачного цвета на периметре. При рисовании точек или линий такой кистью их границы плавно сливаются с фоном без образования четких контуров, как при стандартном рисовании линий средствами GDI. Описанный эффект можно легко воспроизвести при помощи альфа-канала. В листинге 11.9 приведен класс KAirBrush, реализованный на базе DIB-секции. Листинг 11.9. Класс KAirBrush class KAirBrush { HBITMAP mJiBrush; HDC mJiMemDC: HBITMAP m_h01d; int mjiWidth; int mjiHeight; void Release(void) { SelectObject(m_hMemDC. m_h01d); DeleteObject(m_hMemDC); DeleteObject(mJiBrush); NULL; m hBrush - NULL; m hOld = } public: KAirBrushO { mJiBrush m hMemDC m hOld } -KAirBrushO { Releasee) NULL; m_ - NULL; = NULL; = NULL; JiMemDC void Create(int width, int height. C0L0RREF color); void Apply(HDC hDC. int x, int y); void KAirBrush::Apply(HDC hDC, int x, int y) { BLENDFUNCTION blend = { AC SRC OVER, 0. 255. AC SRC ALPHA
Альфа-наложение 657 AlphaBlend(hDC. x-m_nWidth/2. y-m_nHeight/2, mjiWidth. m_nHeight. mJiMemDC, 0. 0. mjiWidth. mjiHeight, blend); } void KAirBrush::Create(int width, int height. COLORREF color) { ReleaseO; BYTE * pBits; BITMAPINFO Bmi - { { sizeof(BITMAPINFOHEADER). width, height. 1. 32. BI_RGB } }; m_hBrush - CreateDIBSection(NULL. & Bmi. DIB_RGB_COLORS. (void **) & pBits. NULL. NULL); mJiMemDC = CreateCompatibleDC(NULL); m_h01d - (HBITMAP) SelectObject(m_hMemDC. m_hBrush); m_nWidth = width; mjiHeight = height; // Однородный цветной круг на белом фоне { PatBlt(m_hMemDC. 0. 0. width, height. WHITENESS); HBRUSH hBrush = CreateSolidBrush(color); SelectObject(m_hMemDC. hBrush); SelectObject(m_hMemDC. GetStockObject(NULL_PEN)); Ellipse(m_hMemDC. 0. 0. width, height); SelectObject(m_hMemDC. GetStockObject(WHITE_BRUSH)); DeleteObject(hBrush); } BYTE * pPixel = pBits; // Вычислить альфа-канал и умножить значения пикселов for (int y=0; y<height; у++) for (int x=0: x<width; x++. pPixel+=4) { // Расстояние от центра, нормализованное в интервале [0..255] int dis - (int) ( sqrt( (x-width/2) * (x-width/2) + (y-height/2) * (y-height/2) ) * 255 / (max(width. height)/2) ); BYTE alpha = (BYTE) max(min(255-dis. 255). 0); pPixel[0] - pPixel[0] * alpha / 255; pPixeUl] = pPixel[l] * alpha / 255; pPixel[2] - pPixel[2] * alpha / 255; pPixel[3] = alpha; } }
658 Глава 11. Нетривиальное использование растров Класс KAirBrush хранит кисть в DIB-секции, поэтому в переменных класса хранятся манипулятор растра и совместимого контекста, манипулятор исходного растра и размеры кисти. Метод KAirBrush::Create строит DIB-секцию кисти по размерам и заданному цвету. Он создает DIB-секцию с 32-разрядным цветом и совместимый контекст устройства, в котором выбирается DIB-секция, после чего выводится белый фон и однородный цветной круг. В результате мы получаем круглую кисть с жесткостью в 100 % на белом фоне. Следующий фрагмент вычисляет альфа-канал и вносит его в данные RGB посредством предварительного умножения. Для этого программа последовательно перебирает все пикселы DIB-секции, вычисляет их расстояние от центра круга, определяет альфа-коэффициент, умножает на него составляющие RGB и сохраняет коэффициент в альфа-канале. В результате мы получаем 32-разрядную DIB-секцию, в которую были заранее внесены данные альфа-канала. Метод KAirBrush::Apply просто выводит кисть, располагая ее центр в заданной точке (х,у). При этом постоянный альфа-коэффициент устанавливается равным 255, поскольку нас интересуют только данные альфа-канала. Если приложение поддерживает работу с графическим планшетом, фиксирующим силу нажима, постоянный альфа-коэффициент может использоваться для постепенного изменения кисти вдоль линии. Использовать класс KAirBrush несложно. Сначала создайте экземпляр KAirBrush — например, во время инициализации представления (view). На панели инструментов можно создать кнопки для изменения цвета или формы кисти. При обработке некоторых сообщений мыши кисть выводится в текущей позиции курсора. Ниже приведен типичный фрагмент программы. Примерный результат показан на рис. 11.9. switch ( uMsg) { case WM_CREATE: mJ>rush.Create(32. 32, RGB(0, OxFF. 0)); return 0; case WMJ.BUTT0ND0WN: wParam - MKJ.BUTT0N; // Перейти к следующей секции case case WM_M0USEM0VE: if (wParam & MK_LBUTT0N ) { m_brush.Apply(m_hDCBitmap. LOWORD(lParam), HIWORD(lParam)); Refresh(LOWORDdParamO). HIWORD(lParam)); } return 0; } Аналогичная методика применяется при выводе геометрических фигур с размытыми краями или при наложении изображений. Чтобы размыть границы выводимых линий, многоугольников, кругов и т. д., присвойте внутренним пикселам альфа-коэффициент 255, а внешним пикселам — альфа-коэффициент 0. Альфа-коэффициенты пограничных пикселов должны отражать степень их размытия. Например, при рисовании линии под углом 45° некоторые пограничные
Альфа-наложение 659 пикселы будут принадлежать линии лишь наполовину, поэтому их альфа-коэффициенты должны быть равны 127. Иногда размытие требует проведения довольно сложных вычислений. Один простой, хотя и недешевый способ заключается в создании монохромного растра, увеличенного в п раз по сравнению с оригиналом. В этом растре рисуется увеличенный вариант геометрической фигуры. Полученное изображение делится на блоки размером п х и, и сумма пикселов каждого блока преобразуется в данные альфа-канала. • * » ш * W Рис. 11.9. Точки и линии, нарисованные при помощи класса KAirBrush Имитация альфа-наложения Альфа-наложение, как и другие приятные возможности GDI, поддерживается не на всех платформах Win32, что ограничивает возможности его применения. Если вы хотите использовать альфа-наложение в реальной программе, вам придется имитировать его своими силами. Один из вариантов реализации рассматривается ниже. На этот раз мы не пытаемся имитировать функцию AlphaBlend, а ограничимся реализацией альфа-наложения между двумя 32-разрядными DIB-растрами. Функция AlphaBlend3232 приведена в листинге 11.10. Листинг 11.10. Альфа-наложение между двумя 32-разрядными DIB-растрами // Вычисление смещения пиксела DIB inline int GetOffset(BITMAPINFO * pBMI. int x. int y) { if ( pBMI->bmiHeader.biHeight > 0 ) // Для перевернутого растра у = pBMI->bmiHeader.biHeight - 1 - у; return ( pBMI->bmiHeader.biWidth * pBMI-> bmiHeader.biBitCount + 31 ) / 32 * 4 * у + ( pBMI->bmiHeader.biBitCount / 8 ) * x; } // Альфа-наложение между двумя 32-разрядными DIB-растрами BOOL AlphaBlend3232(BITMAPINF0 * pBMIDst, BYTE * pBitsDst, int dx, int dy, int w, int h. Продолжение^
660 Глава 11. Нетривиальное использование растров Листинг 11.10. Продолжение BITMAPINFO * pBMISrc, BYTE * pBitsSrc, int sx, int sy. BLENDFUNCTION blend) { int alpha = blend.SourceConstantAlpha; // Постоянный альфа-коэффициент int beta = 255 - alpha; int format; if ( blend.AlphaFormat==0 ) format = 0; else if ( alpha==255 ) format = 1; else format = 2; for (int j=0; j<h; j++) { BYTE * D = pBitsDst + GetOffset(pBMIDst. dx. j + dy); BYTE * S = pBitsSrc + GetOffset(pBMISrc. sx, j + sy); int i: switch ( format ) { case 0: // Только постоянный альфа-коэффициент for (i=0: i<w; i++) { D[0] = ( S[0] * alpha + beta * D[0] + 127 D[l] - ( S[l] * alpha + beta * D[l] + 127 D[2] • ( S[2] * alpha + beta * D[2] + 127 D[3] - ( S[3] * alpha + beta * D[3] + 127 D +- 4; S += 4; } break; case 1: // Только альфа-канал for (i=0; i<w; i++) { beta - 255 - S[3]; D[0] - S[0] + ( beta * D[0] + 127 ) / 255 D[l] - S[l] + ( beta * D[l] + 127 ) / 255 D[2] - S[2] + ( beta * D[2] + 127 ) / 255 D[3] - S[3] + ( beta * D[3] + 127 ) / 255 D +- 4; S +- 4; } break; case 2: // Постоянный коэффициент вместе с альфа-каналом for (i=0; i<w: i++) { beta - 255 - ( S[3] * alpha + 127 ) / 255; D[0] - ( S[0] * alpha + beta * D[0] + 127 ) / 255; D[l] = ( S[l] * alpha + beta * D[l] + 127 ) / 255: ) / 255 ) / 255 ) / 255 ) / 255
Итоги 661 D[2] = ( S[2] * alpha + beta * D[2] + 127 ) / 255; D[3] - ( S[3] * alpha + beta * D[3J + 127 ) / 255; D +« 4; S += 4; } } } return TRUE; } Приемником и источником для функции Al phaBl end3232 являются одинаковые по размеру прямоугольники в 32-разрядном DIB-растре. Таким образом, мы можем вычислить адрес пиксела как в источнике, так и в приемнике, а масштабирование этой функцией не поддерживается. Альфа-наложение делится на три случая: только постоянный альфа-коэффициент, только альфа-канал и одновременное применение постоянного альфа-коэффициента с альфа-каналом. Функция перебирает все пикселы приемного прямоугольника и объединяет их с пикселами источника. Для таких важных операций, как альфа-наложение, следует создать несколько вариантов функции для разных комбинаций поверхности источника и приемника. Например, функция Al phaBl endl632 будет работать с 16-разрядным приемником и 32-разрядным источником, а функция Al phaBl end824 будет получать 8-разрядный приемник, использующий палитру и требующий поиска в цветовой таблице, и 24-разрядный источник, не поддерживающий альфа-канала. Итоги Основной темой этой главы является формирование новых пикселов по нескольким операндам. Мы подробно рассмотрели тернарные и кватернарные растровые операции, различные варианты прозрачного вывода растров, альфа-наложение и отображение растров на параллелограмм. Также были описаны общие принципы работы растровых операций, процесс разбиения и анализа ROP-кодов и построение растровых операций для решения конкретных практических задач. В этой главе нашлось место и для новых экзотических средств GDI для вывода растров (MaskBlt, PlgBlt, TransparentBlt и Al phaBl end) с примерами использования и имитации этих функций на тех платформах Win32, где они не поддерживаются. После описания растровых форматов GDI (глава 10) и функций GDI (глава 11) нам остается лишь узнать, как организовать прямой доступ к массиву пикселов, как реализовать возможности, предоставляемые GDI, а в некоторых случаях — и улучшить реализацию. В следующей главе рассматривается прямой доступ к пикселам и его применение при обработке графических изображений. Примеры программ К главе 11 прилагается всего одна программа AdvBitmap, демонстрирующая весь изложенный материал (табл. 11.3).
662 Глава 11. Нетривиальное использование растров Таблица 11.3. Программа главы 11 Каталог проекта Samples\Chapt_l l\Adv__Bitmap Описание Вывод диаграммы тернарных растровых операций, демонстрация вывода значков с применением растровых операций, спрайтовая анимация, альфа-наложение, применение масок при выводе растров, постепенное проявление растров, PlgBlt и т. д. Соответствующие команды находятся в меню Test, а также появляются после открытия ВМР-файлов
Глава 12 Графические алгоритмы и растры Windows Аппаратно-независимые растры и DIB-секции удобны тем, что приложение может напрямую работать с их массивами пикселов и цветовыми таблицами. Довольно часто этот прямой доступ оказывается абсолютно необходимым для реализации возможностей, не поддерживаемых GDI, или достижения повышенного быстродействия по сравнению с функциями GDI. Кстати, то и другое уже встречалось нам ранее. Имитируя функцию PlgBlt, мы воспользовались функциями GDI GetPixel и SetPixel. Как выяснилось, эти функции заметно уступают по быстродействию реализации PlgBlt в Windows 2000 GDI. С каждым вызовом GetPixel /SetPixel связаны затраты на проверку параметров, переключение из пользовательского режима в режим ядра, преобразование цветов и т. д. Функция GetPixel для выполнения своей задачи даже создает временный растр и вызывает внутреннюю реализацию BitBlt. He удивительно, что она так медленно работает. При использовании альфа-наложения GDI не обеспечивает нормальной поддержки для настройки альфа-канала в 32-разрядном растре или предварительного умножения каналов RGB на альфа-коэффициент. Следовательно, для решения этих задач вам придется напрямую работать с массивом пикселов. В этой главе вы научитесь напрямую работать с данными DIB и DIB-секций для реализации различных графических алгоритмов. Среди рассматриваемых тем — прямой доступ к массивам пикселов, аффинные преобразования растров на базе прямого доступа, преобразование цветов и пикселов растра, а также обработка изображений с применением пространственных фильтров.
664 Глава 12. Графические алгоритмы и растры Windows Прямой доступ к пикселам Прежде всего нам понадобится несколько общих функций для работы с отдельными пикселами DIB или DIB-секции. При наличии полной структуры BITMAPINFO и указателя на массив пикселов работа с DIB-секцией почти не отличается от работы с DIB. В сущности, нам нужны аналоги функций GetPixel и SetPixel GDI. При работе с аппаратно-независимым растром обращение к отдельным пикселам сжатого изображения — задача не из простых. Если прочитать пиксел из растра, сжатого по алгоритму RLE, еще реально (хотя и очень долго), то записать что-либо в сжатый растр практически невозможно. Ведь если новый пиксел отличается от соседних, возможно, вам придется расширять строку развертки. Поэтому мы предполагаем, что все сжатые растры (как растры со сжатием RLE, так и сжатые изображения в формате JPEG или PNG) заранее распакованы. Также следует учитывать, что GetPixel и SetPixel работают с данными C0L0RREF, которые могут представлять значения RGB или индексы палитры. Наша базовая функция должна использовать тот же формат пикселов, что и растр, с которым она работает (то есть цветовые индексы для растров с палитрой или 16-, 24- или 32-разрядные значения RGB для растров, не использующих палитру). Поверх этих базовых функций строятся функции, работающие с C0L0RREF. В листинге 12.1 приведены два новых метода класса KDIB: GetPixel Index и Set- Pixel Index. Функция GetPixellndex возвращает цветовые данные для пиксела в заданной позиции. Для растра с кодировкой 1 бит/пиксел эти данные будут состоять из 1 бита, для растра с кодировкой 2 бита/пиксел — из 2 битов и т. д. Функция SetPixel Index решает противоположную задачу — она заменяет пиксел в заданной позиции новыми цветовыми данными. Листинг 12.1. Общие функции для обращения к пикселам DIB const BYTE Shiftlbpp[] ={ 7. 6, 5. 4. 3, 2. 1. О }; const BYTE Masklbpp [] - { 0x7F, OxBF, OxDF. OxEF, 0xF7, OxFB, OxFD. OxFE }; const BYTE Shift2bpp[] = { 6. 4, 2. 0 }; const BYTE Mask2bpp [] = { ~0xC0. -0x30. -OxOC. -0x03 }; const BYTE Shift4bpp[] = { 4. 0 }; const BYTE Mask4bpp [] - { ~0xF0. ~0x0F }; DWORD KDIB::GetPixelIndex(int x. int y) const { if ( (x<0) || (x>=m_nWidth) ) return -1; if ( (y<0) || (y>=m_nHeight) ) return -1; BYTE * pPixel e mjDOrigin + у * mjiDeUa; switch ( m nlmageFormat ) { case DIB_1BPP: return ( pPixe1[x/8] » Sh1ftlbpp[x*8] ) & 0x01;
Прямой доступ к пикселам 665 case DIB_2BPP: return ( pPixel[x/4] » Shift2bpp№4] ) & 0x03; case DIB_4BPP: return ( pPixel[x/2] » Shift4bpp[xH] ) & OxOF; case DIB_8BPP: return pPixel[x]; case DIBJL6RGB555: case DIB_16RGB565: return ((WORD *)pPixel)[x]: case DIB_24RGB888: pPixel += x * 3; return (pPixelCO]) | (pPixel[1] « 8) | (pPixel[2] « 16): case DIB_32RGB888: case DIB_32RGBA8888: return ((DWORD *)pPixel)[x]; return -1; BOOL KDIB::SetPixelIndex(int x, int y. DWORD index) { if ( (x<0) || (x>=m_nWidth) ) return FALSE; if ( (y<0) || (y>=m_nHeight) ) return FALSE; BYTE * pPixel = m_pOrigin + у * m_nDelta; switch ( mjiImageFormat ) { case DIB_1BPP: pPixel[x/8] - (BYTE) ( ( pPixel[x/8] & Masklbpp[xS8] ) | ( (index & 1) « Shiftlbpp[x*8] ) ); break; case DIB_2BPP: pPixel[x/4] = (BYTE) ( ( pPixel[x/4] & Mask2bpp[x*4] ) | ( (index & 3) « Shift2bpp[xS4] ) ); break; case DIB_4BPP: pPixel[x/2] = (BYTE) ( ( pPixel[x/2] & Mask4bpp№] ) | ( (index & 15) « Shift4bpp№] ) ); break; case DIB_8BPP; pPixel[x] = (BYTE) index; break; Продолжение^
666 Глава 12. Графические алгоритмы и растры Windows Листинг 12.1. Продолжение case DIB_16RGB555: case DIB_16RGB565: ((WORD *)pPixel)[x] = (WORD) index; break: case DIB_24RGB888: ((RGBTRIPLE *)pPixel)[x] - * ((RGBTRIPLE *) & index); break; case DIB_32RGB888: case DIB_32RGBA8888: ((DWORD *)pPixel)[x] - index; break; default: return FALSE; } return TRUE; } В функциях предусмотрена проверка границ. Поскольку выход за пределы растра обычно приводят к возникновению GPF (общих ошибок защиты), мы сразу пресекаем подобные попытки. Если координаты находятся за границами растра, функция возвращает код ошибки. После проверки параметров функция вычисляет адрес первого пиксела строки развертки по координате г/, используя адрес логического начала растра и разность между начальными адресами двух соседних строк развертки. Эти две характеристики вычисляются заранее с учетом порядка (прямого или обратно) следования строк развертки в DIB. Например, для стандартных DIB-растров с обратным порядком строк развертки логическое начало DIB находится в конце данных растра, а смещение строк развертки является отрицательной величиной. При непосредственном обращении к пикселам учитывается их формат. Для растров с кодировкой 1, 2 и 4 бит/пиксел каждый пиксел занимает лишь часть байта, поэтому программа должна вычислить величину сдвига в битах и построить маску. Проще всего работать с 8-разрядными растрами, в которых каждый пиксел занимает ровно один байт. В 16-разрядном растре пиксел занимает два байта. Вызывающая сторона должна самостоятельно преобразовать 16-разрядные данные пиксела в формат RGB. С 24-разрядными растрами дело обстоит сложнее, поскольку мы не можем обратиться к 24-разрядному пикселу как к DWORD и замаскировать старшие 8 бит. Хотя в литературе по программированию иногда встречаются программы, где реализован такой подход, на самом деле это недопустимо. Например, при создании 24-разрядной DIB-секции 64 х 64 размер массива пикселов составит 64 х 64 х 3 = 12 Кбайт. На процессорах Intel будет выделено ровно три страницы памяти. Смещение последнего пиксела в растре равно 0x2FFD. При попытке прочитать его как DWORD процессор выйдет на 1 байт за пределы 12-килобайтно- го блока, что, скорее всего, приведет к возникновению GPF. Метод SetPixel Index имеет практически такую же структуру, как GetPixel Index. Для 1-, 2- и 4-разрядных растров присваивание пикселу сводится к удалению
Аффинные преобразования растров 667 его первоначальных битов при помощи маски и внесению новых данных. Для 24-разрядных растров указатель преобразуется к типу указателя на RGBTRIPLE и данные копируются как структура RGBTRIPLE, чтобы компилятор сгенерировал код для копирования ровно трех байтов. Выполняете ли вы операции с растрами, требующие произвольного доступа к пикселам, — например, повороты, зеркальные отражения или копирование данных между растрами одинакового формата? Функции GetPixel Index и Set- Pixel Index — это именно то, что вам нужно. Эти функции также очень хорошо работают для растров, не использующих палитры. Если вы захотите окрасить в красный цвет пиксел растра с кодировкой 8 бит/пиксел, вам придется предварительно свериться с цветовой таблицей. Мы вернемся к этой теме позднее. Аффинные преобразования растров Применение методов GetPixel Index и SetPixel Index, созданных в предыдущем разделе, лучше продемонстрировать на конкретном примере. Давайте попробуем реализовать общий алгоритм аффинных преобразований растров. Вообще говоря, основные принципы решения этой задачи уже встречались нам ранее при имитации функции PlgBlt. В листинге 12.2 приведены две функции: KDIB: :PlgBlt и KDIB: :TransformBitmp. Функция KDIB::PlgBlt преобразует прямоугольник, находящийся внутри DIB- растра, в параллелограмм, находящийся внутри другого DIB-растра. Параллелограмм определяется тремя точками приемной поверхности. Функция KDIB::PlgBlt, как и одноименная функция GDI, поддерживает любые двумерные аффинные преобразования, включая смещение, зеркальное отражение, повороты и сдвиги. По своей структуре KDIB::PlgBlt напоминает нашу имитацию функции GDI PlgBlt, но для работы с пикселами вместо медленных функций GDI GetPixel и SetPixel в ней используются функции GetPixel Index и SetPixel Index. Листинг 12.2. Общие аффинные преобразования растров BOOL KDIB::PlgBlt(const POINT * pPoint. KDIB * pSrc. int nXSrc. int nYSrc, int nWidth, int nHeight) { KReverseAffine map(pPoint); map.Setup(nXSrc, nYSrc. nWidth. nHeight): for (int dy-map.miny; dy<«map.maxy; dy++) for (int dx»map.minx; dx<«map.maxx; dx++) { float sx, sy: map.Map(dx. dy, sx, sy); if ( (sx>=nXSrc) && (sx<(nXSrc+nWidth)) ) if ( (sy>»nYSrc) && (sy<(nYSrc+nHeight)) ) SetPixelIndex(dx, dy. pSrc->GetPixelIndex( (int)sx. (int)sy)); } Продолжение &
668 Глава 12. Графические алгоритмы и растры Windows Листинг 12.2. Продолжение return TRUE; } HBITMAP KDIB::TransformBitmap(XFORM * xm. COLORREF crBack) { int xO. yO, xl. yl. x2. y2. x3. y3; Map(xm, 0. 0. xO. yO); // 0 1 Map(xm. m_nWidth. 0. xl. yl); // Map(xm, 0. m_nHeight. x2. y2); 111 3 Map(xm, mjiWidth. mjiHeight. x3, y3); int xmin, xmax; int ymin, ymax; minmax(x0. xl. x2. x3. xmin. xmax); minmax(y0, yl, у2. уЗ. ymin. ymax); int destwidth = xmax - xmin; int destheight = ymax - ymin; KBitmapInfo dest; dest.SetFormat(destwidth. destheight. m_pBMI->bmi Header.bi Bi tCount. m_pBMI->bmi Header.bi Compressi on); BYTE * pBits; HBITMAP hBitmap » CreateDIBSection(NULL. dest.GetBMK). DIB_RGB_COLORS. (void **) & pBits. NULL. NULL); if ( hBitmap==NULL ) return NULL; { HDC hMemDC - CreateCompatibleDC(NULL); HGDIOBJ hOld - SelectObject(hSMemDC. hBitmap): HBRUSH hBrush - CreateSolidBrush(crBack); RECT rect - { 0. 0. destwidth. destheight }: FillRect(hMemDC. & rect. hBrush); DeleteObject(hBrush); SelectObject(hMemDC. hOld): DeleteObjectChMemDC); } KDIB destDIB; destDIB.AttachDIB(dest.GetBMK). pBits, 0); POINT P[3] = { { x0-xmin. yO-ymin }. { xl-xmin. yl-ymin }. { x2-xmin. y2-ymin } }: destDIB.PlgBlt(P. this. 0. 0. mjiWidth. mjiHeight); return hBitmap;
Аффинные преобразования растров 669 Функция KDIB: rPlgBlt предполагает, что заранее был создан приемный растр нужного размера, формат пикселов которого соответствует формату растра-источника. В результате преобразования растра обычно генерируется новый растр другого размера. При поворотах и сдвигах возникают уголки, которые должны заполняться цветом фона, поскольку они лежат за пределами преобразованного изображения. Функция KDIB: :TransformBitmap отвечает за «подготовку сцены» — к вызову KDIB: :PlgBlt. При вызове ей передается матрица преобразования и цвет фона. На основании текущего формата DIB-растра функция вычисляет точный размер преобразованного растра и создает DIB-секцию соответствующего размера с текущим форматом растра. Перед передачей функции KDIB: :PlgBlt для непосредственного преобразования созданная DIB-секция закрашивается цветом фона. На рис. 12.1 изображена картинка с цветами, повернутая на 15° функцией KDIB::PlgBlt. Рис. 12.1. Поворот растров функцией KDIB::PlgBLt На компьютере с относительно слабым процессором Pentium 200 МГц функция KDIB::PlgBlt рассчитывает поворот изображения 1024 х 768, 24 бит/пиксел за 1,062 секунды; получается 0,7 мегапиксела в секунду. Если для сравнения заменить вызовы GetPixellndex/SetPixel Index вызовами функций GDI GetPixel/ SetPixel, время обработки увеличивается до 16,9 секунды (0,044 мегапиксела в секунду). Эксперимент наглядно доказывает, что прямой доступ к пикселам работает гораздо быстрее функций GDI GetPixel/SetPixel. Учитывая, что функция KDIB::PlgBlt использует вещественные вычисления, выигрыш по быстродействию оказывается даже больше, чем в 17 раз.
670 Глава 12. Графические алгоритмы и растры Windows Быстрые специализированные преобразования растров Когда быстродействие выходит на первый план, общие алгоритмы приходится оптимизировать для конкретных ситуаций. Специализация особенно важна для графических алгоритмов — таких, как преобразование изображений. Если вам захочется писать специализированные функции для разных форматов DIB, можно закодировать операции с пикселами конкретного формата DIB «на месте»; это позволит избавиться от издержек на вызов функции во внутреннем цикле, проверку формата растра, двойное вычисление адресов пикселов и т. д. Оптимизация также возможна в области применения вещественных вычислений для отображения координат приемного растра в координаты источника. Стандартная реализация преобразований вещественных чисел в целые работает очень медленно, поскольку в ней задействован вызов функции. В листинге 12.3 приведена функция преобразования растров, не использующая вещественных вычислений и работающая только с 24-разрядными растрами. Функция P1gB1t24 начинается так же, как и PlgBlt — с подготовки обратного преобразования из приемника в источник. Функция KReverseAffine: .-Setup возвращает ограничивающий прямоугольник приемной поверхности, который затем сравнивается с размерами приемного растра, чтобы убедиться в правильности полученных значений. Параметры ограничивающего прямоугольника источника преобразуются к формату с фиксированной точкой умножением на константу FACTOR, равную 65 536. Затем аналогичные операции выполняются с матрицей преобразования. В данном случае используется формат с фиксированной точкой, состоящий из 16-разрядной целой и 16-разрядной дробной частей. Такое представление позволяет работать с большими растрами и обеспечивает достаточную точность. Листинг 12.3. Оптимизированная функция преобразования 24-разрядных DIB-растров BOOL KDIB::PlgBlt24(const POINT * pPoint. KDIB * pSrc. int nXSrc, int nYSrc, int nWidth, int nHeight) { // Множитель для перехода от FLOAT к формату с фиксированной точкой const int FACTOR = 65536; // Сгенерировать обратное преобразование от приемника к источнику KReverseAffine map(pPoint); map.Setup(nXSrc, nYSrc, nWidth. nHeight); // Обеспечить принадлежность границам растра приемника if ( map.minx < 0 ) map.minx = 0; if ( map.maxx > mjiWidth ) map.maxx = mjiWidth; if ( map.miny < 0 ) map.miny = 0; if ( map.maxy > mjiHeight ) map.maxy - mjiHeight; // Прямоугольник источника в формате с фиксированной точкой
Быстрые специализированные преобразования растров 671 int sminx - nXSrc * FACTOR; int sminy = nYSrc * FACTOR; int smaxx = ( nXSrc + nWidth ) * FACTOR; int smaxy - ( nYSrc + nHeight ) * FACTOR; // Матрица преобразования в формате с фиксированной точкой int mil = (int) (map.m_xm.eMll * FACTOR); int ml2 - (int) (map.m_xm.eM12 * FACTOR); int m21 = (int) (map.m_xm.eM21 * FACTOR); int m22 = (int) (map.m_xm.eM22 * FACTOR); int mdx = (int) (map.m_xm.eDx * FACTOR); int mdy = (int) (map.m_xm.eDy * FACTOR); BYTE * SOrigin = pSrc->m_pOrigin; int SDelta = pSrc->m_nDelta; // Перебрать строки развертки приемного растра for (int dy=map.miny; dy<map.maxy; dy++) { // Вычислить адрес первого пиксела в приемнике BYTE * pDPixel = m_pOrigin + dy * mjiDelta + map.minx * 3; // Адрес первого пиксела в источнике int sx = mil * map.minx + m21 * dy + mdx; int sy = ml2 * map.minx + m22 * dy + mdy; // Перебрать все пикселы в строке развертки for (int dx=map.minx; dx<map.maxx; dx++, pDPixel+=3, sx+=mll, sy+=ml2) if ( (sx>=sminx) && (sx<smaxx) ) if ( (sy>=sminy) && (sy<smaxy) ) { // Адрес пиксела источника BYTE * pSPixel - SOrigin + (sy/FACTOR) * SDelta; // Скопировать три байта * ((RGBTRIPLE *)pDPixel) - ((RGBTRIPLE *)pSPixel)[sx/FACTOR]: return TRUE; } Для каждой строки развертки адрес пиксела вычисляется всего один раз и сохраняется в переменной pDPixel, которая позднее увеличивается на три байта для каждого пиксела (для 24-разрядного растра). Таким образом, издержки на каждый пиксел приемника сокращаются до простого сложения. Вычисление пиксела источника, соответствующего текущему пикселу приемника, производится «на месте»; значение преобразуется в формат с фиксированной точкой. Исходные значения sx, sy, вычисленные за пределами внутреннего цикла, увеличиваются на элементы матрицы преобразования еМН и еМ12. Полученные значения сравниваются с границами ограничивающего прямоугольника в формате с фиксированной точкой, чтобы в выборке участвовали только пикселы растра-
672 Глава 12. Графические алгоритмы и растры Windows источника. Для получения адреса пиксела источника числа с фиксированной точкой sx и sy необходимо преобразовать в целые числа; задача решается простым делением на константу FACTOR. Компилятор достаточно умен, чтобы заменить деление операцией сдвига. При копировании данных пиксела снова используется структура RGBTRIPLE. 24-разрядный растр 1024 х 768, использованный при тестировании класса KDIB::PlgBlt, наша улучшенная функция PlgBU24 поворачивает за 172 миллисекунды. Таким образом, в секунду обрабатывается 4,36 мегапиксела — по сравнению с общим решением на базе GetPixellndex/SetPixelIndex достигается выигрыш в 520%. А если сравнить с решением на базе GetPixel/SetPixel, функция PlgBlt24 работает в 100 раз быстрее! Более того, возможности оптимизации PlgBlt24 еще не исчерпаны. Например, вместо проверки каждого пиксела на принадлежность ограничивающему прямоугольнику растра-источника можно заранее вычислять точки пересечения приемной строки развертки с ограничивающим прямоугольником источника, что позволит обойтись без проверок на уровне отдельных пикселов. Преобразования цветов Существует множество графических алгоритмов, в которых каждый пиксел растра должен изменяться по тем или иным правилам. Цветовые преобразования применяются к каждому пикселу независимо от остальных, вне какого-либо глобального контекста. Примерами таких алгоритмов является преобразование цветных растров в оттенки серого, гамма-коррекция, преобразования цветовых пространств, регулировка оттенка, яркости или насыщенности и т. д. Все алгоритмы преобразования цветов построены по одному шаблону. Если у растра имеется цветовая таблица, преобразованиям подвергаются все элементы цветовой таблицы. В противном случае мы перебираем все пикселы растра и применяем преобразование к каждому пикселу в отдельности. В простейшем варианте используется общий алгоритм, которому среди параметров передается указатель на функцию. Каждое преобразование цвета описывается простой статической функций. Чтобы обработать растр с применением заданного преобразования, достаточно вызвать общий алгоритм и передать специализированную функцию преобразования цвета в качестве параметра. В другом, похожем решении определяется абстрактный класс цветового преобразования с виртуальной функций, выполняющей непосредственную работу. В растровых алгоритмах быстродействие обычно является критическим фактором, поэтому вызов простой или виртуальной функции для каждого пиксела большого растра оказывается неприемлемым. Конечно, мы не хотим заново повторять один и тот же код или строить программу на базе макросов. Остается единственная альтернатива — функции-шаблоны. Конечно, лучше было бы определить эти алгоритмы для класса KDIB, однако в ситуации, когда компилятор C++ не позволяет определять функции-шаблоны в качестве членов класса, придется создать статическую функцию-шаблон, которой передается указатель на экземпляр KDIB. Ситуация усложняется тем, что
Преобразования цветов 673 компилятор C++ не поддерживает и дружественных функций-шаблонов, поэтому некоторые закрытые члены класса придется преобразовать в открытые. В листинге 12.4 приведен алгоритм преобразования цветов DIB-растра, построенный на базе шаблона. Листинг 12.4. Шаблон для преобразования цветов растра template <class Dummy> bool ColorTransform(KDIB * dib. Dummy map) { // Цветовая таблица OS/2 DIB: 1-. 4-, 8 бит/пиксел, сжатие RLE if ( dib->m_pRGBTRIPLE ) { for (int i=0; i<dib->m_nClrUsed; i++) map(dib->m_pRGBTRIPLE[i].rgbtRed. di b->m_pRGBTRIPLE[i].rgbtGreen. dib->m_pRGBTRIPLE[i].rgbtBlue); return true; } // Цветовая таблица Windows DIB: 1-. 4-, 8 бит/пиксел, сжатие RLE if ( dib->m_pRGBQUAD ) { for (int i=0; i<dib->m_nClrUsed; i++) map(dib->m_pRGBQUAD[i].rgbRed, di b->m_pRGBQUAD[i].rgbGreen. dib->m_pRGBQUAD[i].rgbBlue); return true: } for (int y=0: y<dib->m_nHeight; y++) { int width = dib->m_nWidth: unsigned char * pBuffer = (unsigned char *) dib->m_pBits + dib->m_nBPS * y; switch ( dib->m_nImageFormat ) { case DIB_16RGB555: // 15-разрядный формат RGB. 5-5-5 for (; width>0: width--) { BYTE red = ( (* (WORD *) pBuffer) & 0x7C00 ) » 7; BYTE green = ( (* (WORD *) pBuffer) & ОхОЗЕО ) » 2; BYTE blue = ( (* (WORD *) pBuffer) & OxOOlF ) « 3: map( red. green, blue ); * ( WORD *) pBuffer - ( ( red » 3 ) « 10 ) | ( ( green » 3 ) « 5 ) | ( blue » 3 ): pBuffer += 2; Продолжение^
674 Глава 12. Графические алгоритмы и растры Windows Листинг 12.4. Продолжение } break; case DIB_16RGB565: // 16-разрядный формат RGB, 5-6-5 for (; width>0; width--) { BYTE red - ( (* (WORD *) pBuffer) & 0xF800 ) » 8 BYTE green « ( (* (WORD *) pBuffer) & 0x07E0 ) » 3 BYTE blue = ( (* (WORD *) pBuffer) & OxOOlF ) « 3 map( red. green, blue ); * ( WORD *) pBuffer - ( ( red » 3 ) « 11 ) | ( ( green » 2 ) « 5 ) | ( blue » 3 ); pBuffer += 2; } break; case DIB_24RGB888: // 24-разрядный формат RGB for (; width>0; width--) { map( pBuffer[2]. pBuffer[l], pBuffer[0] ); pBuffer +- 3; } break; case DIB_32RGBA8888: // 32-разрядный формат RGBA case DIB_32RGB888: // 32-разрядный формат RGB for (; width>0; width--) { map( pBuffer[2]. pBuffer[l], pBuffer[0] ); pBuffer += 4; } break; default: return false; } } return true; } Функция ColorTransform получает два параметра: указатель на экземпляр KDIB и указатель на функцию. Конечно, передача указателя на функцию без предварительного задания прототипа выглядит несколько странно, но очевидно, этот способ поддерживается и используется в STL. Первая часть функции обрабатывает цветовые таблицы ВМР-файлов в формате OS/2: каждая структура RGBTRIPLE обрабатывается вызовом функции преобразования цветов (параметр, тар). Функция преобразования цветов получает по ссылке три параметра (красный, зеленый и синий канал) и возвращает преобразованный цвет в этих же переменных. Фрагмент для работы с цветовой таблицей растров Windows выглядит аналогично, если не считать того, что на этот раз используется структура RGBQUAD.
Преобразования цветов 675 В этой части кода обрабатываются все форматы DIB с палитрой, включая сжатые и несжатые форматы RLE. Оставшийся код обрабатывает 16-разрядные растры High Color, 24- и 32-разрядные растры True Color с альфа-каналами. Логический порядок массива пикселов в данном случае роли не играет, поскольку программа просто перебирает пикселы в порядке их расположения в памяти. Для 16-разрядных растров поддерживаются два распространенных формата. Программа должна извлечь каналы RGB, преобразовать каждый из них в 8-разрядную величину, вызвать функцию преобразования цвета, а затем снова упаковать полученный результат в 16- разрядное слово. С 24-разрядными растрами все совсем просто. В 32-разрядном растре альфа-канал остается без изменений. Все остальные экзотические форматы DIB (например, внедренные изображения JPEG или PNG, а также растры с нестандартными битовыми полями) не поддерживаются текущей реализацией функции ColorTransform. Преобразование растров в оттенки серого Преобразование цветов из пространства RGB в оттенки серого обычно осуществляется по формуле: Серый = 0,299 х Красный + 0,587 х Зеленый + 0,114 х Синий В компьютерной реализации нам хотелось бы обойтись без вычислений с плавающей точкой. Ниже приведен метод (построенный на базе шаблона Col or- Transform) преобразования растра RGB в оттенки серого цвета. // 0.299 * красный + 0.587 * зеленый + 0.114 * синий inline void MaptoGray(BYTE & red, BYTE & green, BYTE & blue) red - ( red * 77 + green * 150 + blue * 29 + 128 ) / 256; green = red; blue - red; class KImage : public KDIB public; bool ToGreyScale(void); bool KImage:;ToGreyScale(void) return ColorTransform(this, MaptoGray); Для инкапсуляции алгоритмов обработки растров, разработанных в этой главе, мы создаем класс KImage, производный от KDIB. Класс KImage не содержит дополнительных переменных. Из всех методов этого класса выше приведен только метод ToGreyScale. Позднее мы добавим в этот класс другие методы. Метод KImage::ToGreyScale преобразует текущий цветной DIB-растр в оттенки серого. Для этого он просто вызывает функцию-шаблон ColorTransform и передает ей функцию преобразования цвета MaptoGray. Функция MaptoGray, исполь-
676 Глава 12. Графические алгоритмы и растры Windows зуя целочисленные операции, вычисляет яркость серого цвета и присваивает ее всем трем каналам RGB. В отладочной версии MaptoGray компилируется как отдельная функция, указатель на которую передается ColorTransform. В окончательной версии для достижения оптимального быстродействия все вызовы MaptoGray заменены подставляемым кодом. Гамма-коррекция Выводимые изображения подвержены фотометрическим искажениям, обусловленным нелинейной реакцией экрана монитора на интенсивность сигнала. Фотометрическая реакция устройства вывода называется гамма-реакцией (gamma response). В разных операционных системах для экрана монитора используются разные гамма-характеристики. Например, изображение, подготовленное на компьютере Macintosh, на PC выглядит слишком темным. С другой стороны, изображение, переданное с сервера PC на Macintosh, может показаться излишне светлым. Для компенсации этих различий приходится корректировать гамма-характеристики устройства. Гамма-коррекция обычно выполняется независимо по всем трем каналам RGB. Три массива по 256 байт вычисляются заранее и передаются программному гамма-преобразователю или видеоадаптеру. Каждый массив относится к одному из каналов. Гамма-коррекция легко реализуется с помощью функции-шаблона Color- Transform. BYTE redGammaRamp[256]: BYTE greenGammaRamp[256]; BYTE blueGammaRamp[256]; inline void MapGamma(BYTE & red. BYTE & green. BYTE & blue) { red = redGammaRamp[red]: green = greenGammaRamp[green]; blue = blueGammaRamp[blue]; } BYTE gamma(double g. int index) { return min(255. (int) ( (255.0 * pow(index/255.0. 1.0/g)) + 0.5 ) ); } bool KImage::GammaCorrect(double redgamma, double greengamma. double bluegamma) { for (int i=0; i<256; i++) { redGammaRamp[i] = gamma( redgamma. i); greenGammaRamp[i] - gamma(greengamma. i); blueGammaRamp[i] = gamma( bluegamma. i); } return ColorTransform(this. MapGamma); }
Преобразования цветов 677 В приведенном фрагменте реализуется функция пользовательского уровня KDIB: :GammaCorrect. Эта функция получает три независимых гамма-коэффициента, значения которых обычно лежат в интервале от 0,2 до 5,0. Функция вычисляет три гамма-таблицы (по одной для каждого RGB-канала) по определению гамма-коррекции, после чего вызывает функцию ColorTransform и передает ей в качестве преобразователя функцию MapGamma. Функция MapGamma просто берет из таблицы элемент с заданным индексом. Гамма-коррекция с коэффициентом, равным 1, представляет собой тождественное преобразование цвета. При гамма-коэффициенте меньше 1 изображение «темнеет», а если коэффициент превышает 1 — «светлеет». Если применить гамма-коррекцию 2,2 к изображению, подготовленному на Macintosh, оно будет выглядеть точно так же, как во время создания. На рис. 12.2 показано изображение тигра до и после гамма-коррекции. Рис. 12.2. Гамма-коррекция Механизм поиска в таблице, реализованный функцией MapGamma, может использоваться и для регулировки цвета по другим критериям. На самом деле имеется лишь одно обязательное условие — каналы RGB должны быть независимыми друг от друга. Например, redGammaRamp можно определить таким образом, чтобы интенсивность красного канала уменьшалась на 10 %, а остальные каналы оставались без изменений. Win32 GDI поддерживает настройку характеристик гамма-коррекции графического устройства, если оборудование и драйвер устройства поддерживают загрузку гамма-таблиц. Задача решается функцией SetDeviceGammaRamp, входящей
678 Глава 12. Графические алгоритмы и растры Windows в ICM 2.0. В DirectDraw операции с гамма-таблицами также выполняются через интерфейс IDirectDrawGammaControl. Все современное оборудование PC должно поддерживать загрузку гамма-таблиц. Преобразование пикселов в растрах Шаблонный алгоритм преобразования цветов, представленный в предыдущем разделе, в действительности перебирает цвета, а не пикселы растра. Впрочем, различия касаются в основном растров с палитрой, для которых алгоритм преобразования цветов просто перебирает все элементы цветовой таблицы (вместо всех пикселов растра). Существует большой класс алгоритмов обработки графических изображений, требующих обслуживания каждого пиксела. Допустим, при построении гистограммы вам придется перебрать все пикселы, чтобы вычислить истинное распределение цветов в растре. При делении цветного растра на несколько каналов желательно, чтобы результаты представляли собой отдельные изображения в оттенках серого, с которыми можно выполнять дальнейшие операции. В алгоритмах цветоделения, используемых графическими редакторами, также организуется перебор всех пикселов. В этом разделе мы построим общий алгоритм преобразования пикселов растра и продемонстрируем его на нескольких практических примерах. На этот раз вместо шаблона будут использоваться указатель на функцию, виртуальная функция и абстрактный базовый класс. Вариант с применением шаблонов из предыдущего раздела обеспечивал очень хорошее быстродействие, за которое приходилось расплачиваться созданием нескольких копий двоичного кода для каждого экземпляра шаблона. Другое ограничение, обусловленное спецификой компилятора, заключается в том, что шаблон воплощается в виде простой функции. Мы не можем использовать класс C++, инкапсулирующий данные вместе с кодом. В частности, для реализации гамма-коррекции требовались глобальные переменные. Родовой класс преобразований пикселов В листинге 12.5 приведен класс KPixel Mapper — абстрактный базовый класс, на основе которого создаются различные алгоритмы преобразования пикселов. Этот класс предназначен для обработки отдельных пикселов или одиночных строк развертки. Листинг 12.5. Абстрактный базовый класс KPixelMapper // Абстрактный класс для преобразования // отдельных пикселов и строк развертки class KPixelMapper { BYTE * m_pColor; // Указатель на цветовую таблицу BGR... int mjiSize; // Размер элемента таблицы (3 или 4) int m nCount; // Количество элементов в таблице
Преобразование пикселов в растрах 679 // Вернуть true, если данные изменились virtual bool MapRGBCBYTE & red. BYTE & green. BYTE & blue) - 0; // Вернуть true, если данные изменились virtual bool MapIndexCBYTE & index) { MapRGB(m_pColor[index*m_nSize+2], m_pColor[index*m_nSize+l]. m_pColor[index*m_nSi ze]); return false; public: KPixelMapper(void) { m_pColor = NULL; mjiSize - 0; m_nCount = 0; } virtual ~ KPixelMapperO void SetColorTableCBYTE * pColor. int nEntrySize. int nCount) { m_pColor - pColor; mjiSize = nEntrySize; mjiCount = nCount; } virtual bool StartLine(int line) { return true; virtual void Maplbpp(BYTE * pBuffer. int width) virtual void Map2bpp(BYTE * pBuffer. int width) virtual void Map4bpp(BYTE * pBuffer. int width) virtual void Map8bpp(BYTE * pBuffer. int width) virtual void Map555(BYTE * pBuffer. int width) virtual void Map565(BYTE * pBuffer. int width) virtual void Map24bpp(BYTE * pBuffer. int width); virtual void Map32bpp(BYTE * pBuffer. int width); void KPixelMapper::Maplbpp(BYTE * pBuffer. int width) { BYTE mask = 0x80; int shift = 7; for (; width>0; width--) Продолжение &
680 Глава 12. Графические алгоритмы и растры Windows Листинг 12.5. Продолжение { BYTE index = ( ( pBuffer[0] & mask ) » shift ) & Oxl; if ( Maplndex(index) ) pBuffer[0] = ( pBuffer[0] & ~ mask ) || (( index & OxOF) « shift); mask »= 1; shift -= 1; if ( mask==0 ) { pBuffer ++; mask = 0x80; shift = 7; } } } void KPixelMapper;:Map24bpp(BYTE * pBuffer. int width) { for (; width>0; width--) { MapRGB( pBuffer[2]. pBuffer[l]. pBuffer[0] ); pBuffer += 3; } } Все основные методы класса являются виртуальными. Метод MapRGB представляет собой чисто виртуальную функцию, обрабатывающую один пиксел в формате RGB. Классом KPi xel Mapper он не реализуется, поскольку предполагается, что производный класс предоставит собственную реализацию для выбранного алгоритма. Метод Maplndex работает с пикселами в формате индекса цветовой таблицы. Наша стандартная реализация преобразует цветовой индекс в значение RGB и вызывает MapRGB. Методы Maplbpp, Map2bpp, ..., Map32bpp обеспечивают обработку строк развертки для всех распространенных форматов DIB-растров. Их стандартная реализация перебирает все пикселы в строке развертки и вызывает для каждого пиксела MapRGB или Maplndex. Все эти методы оформлены в виде виртуальных функций, поэтому производный класс может реализовать их по-своему. Например, производный класс может решить, что 24-разрядные изображения для него особенно важны, переопределить Мар24Ьрр и заменить вызовы MapRGB подставляемым кодом для получения максимального быстродействия. Учтите, что для быстродействия критическую роль играет внутренний цикл. В листинге приведены два обработчика строк развертки для 1-й 24-разрядного формата. Обе функции, MapRGB и Maplndex, возвращают логический признак изменения параметров, переданных по ссылке. На основании полученного значения вызывающая сторона может решить, следует ли изменять исходный пиксел. Чтобы воспользоваться классом KPi xel Mapper для преобразования DIB-растра, мы создаем новую функцию KImage:: Pi xel Transform, которая должна создать экземпляр класса KPixelMapper и передать ему строки развертки. Функция Pixel- Transform приведена ниже — как видите, она устроена очень просто.
Преобразование пикселов в растрах 681 bool KImage::PixelTransform(KPixelMapper & map) { if ( m_pRGBTRIPLE ) map.SetColorTable((BYTE *) m_pRGBTRIPLE, sizeof(RGBTRIPLE), mjiClrUsed); else if ( m_pRGBQUAD ) map.SetColorTable((BYTE *) m_pRGBQUAD, sizeof(RGBQUAD). mjiClrUsed); for (int y=0; y<mjiHeight; y++) { unsigned char * pBuffer = (unsigned char *) m_pBits + m_nBPS * y; if ( ! map.StartLine(y) ) break; switch ( mjiImageFormat ) { case DIB_1BPP: map.Maplbpp(pBuffer. mjiWidth); break; case DIB_2BPP: map.Map2bpp(pBuffer. mjiWidth); break; case DIB_4BPP: map.Map4bpp(pBuffer, mjiWidth); break; case DIB_8BPP: map.Map8bpp(pBuffer. mjiWidth); break; case DIB_16RGB555: // 15-разрядный формат RGB, 5-5-5 map.Map555(pBuffer, mjiWidth); break; case DIB_16RGB565: // 16-разрядный формат RGB, 5-6-5 map.Map565(pBuffer, mjiWidth); break; case DIB_24RGB888: // 24-разрядный формат RGB map.Map24bpp(pBuffer, mjiWidth); break; case DIB_32RGBA8888: // 32-разрядный формат RGBA case DIB_32RGB888: // 32-разрядный формат RGB map.Map32bpp(pBuffer, mjiWidth); break; default: return false; return true;
682 Глава 12. Графические алгоритмы и растры Windows Мы наблюдаем четкое разделение обязанностей: класс, производный от KPixel - Mapper, занимается преобразованием отдельных пикселов, сам класс KPixel Mapper преобразует строки развертки, а метод KImage: :PixelTransform преобразует весь DIB-растр. Если вы захотите поддерживать растровый формат, отличный от формата BMP, вам придется лишь написать свою собственную функцию Pixel- Transform. Если потребуется реализовать новый графический алгоритм из области преобразования пикселов, достаточно написать класс, производный от KPixelMapper. Родовой класс цветоделения Давайте займемся вполне практической задачей — реализацией алгоритма цветоделения, то есть построения в каждом из каналов изображений в оттенках серого по цветному изображению. Общая идея заключается в отображении RGB- пиксела на байт, сохраняемый в 8-разрядном растре с цветовой таблицей в оттенках серого. Как обычно, наша реализация должна быть как можно более универсальной, чтобы она могла поддерживать разные типы цветоделения. На этот раз нам понадобится простая функция, управляющая классом цветоделения (Operator в листинге 12.6). Листинг 12.6. Цветоделение с применением класса KPixelMapper typedef BYTE (* Operator)(BYTE red. BYTE green, BYTE blue); // Родовой класс цветоделения, производный от KPixelMapper // Управляется функций Operator, передаваемой «Channel::Split class KChannel : public KPixelMapper { Operator mJDperator; int mjiBPS: BYTE * m_pBits; BYTE * nrpPixeT: // Вернуть true, если данные изменились virtual bool MapRGB(BYTE & red. BYTE & green. BYTE & blue) { * mjDPixel ++ - m_Operator(red. green, blue): return false; } virtual bool StartLine(int line) { mjpPixel - m_pBits + line * mjiBPS; // Первый пиксел // строки развертки return true; } public: BITMAPINFO * SplitCKImage & dib. Operator oper); }:
Преобразование пикселов в растрах 683 BITMAPINFO * KChannel::SplitCKImage & dib. Operator oper) { mJDperator = oper; mjiBPS = (dib.GetWidthO +3) /4*4; // Размер строки развертки // для 8-разрядного DIB int headsize - sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD); BITMAPINFO * pNewDIB - (BITMAPINFO *) new BYTE [headsize + mjiBPS * abs(dib.GetHeightO)]; memset(pNewDIB. 0. headsize); pNewDIB->bmiHeader.biSize - sizeof(BITMAPINFOHEADER); pNewDIB->bmiHeader.biWidth - dib.GetWidthO; pNewDIB->bmiHeader.biHeight - dib.GetHeightO; pNewDIB->bmiHeader.biPlanes - 1; pNewDIB->bmiHeader.biBitCount = 8; pNewDIB->bmiHeader.biCompression = BI_RGB; for (int c=0; c<256; C++) { pNewDIB->bmiColors[c].rgbRed = c; pNewDIB->bmiColors[c].rgbGreen = c; pNewDIB->bmiColors[c].rgbBlue = c; m_pBits - (BYTE*) & pNewDIB->bmiColors[256]; if ( pNewDIB==NULL ) return NULL; dib.Pixe"ITransform(* this); return pNewDIB; } BITMAPINFO * KImage::SplitChannel(Operator oper) { KChannel channel; return channel.Split(* this, oper); } Класс KChannel является производным от KPixel Mapper. Центральное место в нем занимает метод Split, получающий ссылку на DIB и Operator. Метод Split создает 256-цветный DIB-растр, размеры которого совпадают с размерами исходного растра, заполняет палитру оттенков серого и сохраняет адрес массива пикселов в переменной mjpBits, которая будет использоваться при переборе пикселов. Затем вызывается метод KDIB:: Pixel Transform, перебирающий все пикселы растра, что в конечном счете приводит к вызову KChannel:: MapRGB. Наша реализация MapRGB вызывает Operator для отображения RGB-пиксела в байт и сохраняет полученное значение в качестве значения пиксела создаваемого 256-цветного растра. Метод StartLine вызывается в начале каждой строки развертки, что
684 Глава 12. Графические алгоритмы и растры Windows позволяет программе правильно устанавливать начальную позицию приемной строки. Класс работает с одним каналом. Для обработки нескольких каналов следует либо организовать последовательную обработку, либо воспользоваться новой реализацией, которая создает несколько 8-разрядных DIB-растров и получает новый тип Operator, возвращающий сразу несколько результатов. Пример выделения каналов Работать с классом KChannel несложно; все, что от вас потребуется, — предоставить нужную функцию. Ниже приведено несколько примеров функций для выполнения распространенных операций в моделях RGB, CMYK и HLS. // Выделение красного канала в RGB inline BYTE TakeRedCBYTE red. BYTE green, BYTE blue) { return red; } // Выделение черной составляющей в KCMY inline BYTE TakeK(BYTE red, BYTE green, BYTE blue) { // min ( 255-red, 255-green. 255-blue) if ( red < green ) if ( green < blue ) return 255 - blue: else return 255 - green; else return 255 - red; } // Выделение оттенка в HLS inline BYTE TakeH(BYTE red. BYTE green. BYTE blue) { KColor color(red. green, blue): color.ToHLSO; return (BYTE) (color.hue * 255 / 360); } Ниже показано, как эти функции используются в KDIBView — классе дочерних окон MDI, отображающих содержимое DIB. LRESULT KDIBView::OnCommand(int nld) { switch( nld ) { case IDM_COLOR_SPLITRGB: CreateNewView(m_DIB.SplitChannel(TakeRed), "Red Channel"); CreateNewView(m_DIB.SplitChannel(TakeGreen), "Green Channel"): CreateNewView(m_DIB.SplitChannel(TakeBlue), "Blue Channel"); return 0; case IDM_COLOR_SPLITHLS: CreateNewView(m_DIB.SplitChannel(TakeH), "Hue Channel");
Преобразование пикселов в растрах 685 CreateNewView(m_DIB.SplitChannel(TakeL), CreateNewView(m_DIB.SplitChannel(TakeS). return 0; case IDM_COLOR_SPLITKCMY: CreateNewView(m_DIB.SplitChannel(TakeK), CreateNewView(m_DIB.SplitChannel(TakeC). CreateNewView(m_DIB.SplitChannel(TakeM), CreateNewView(m_DIB.SplitChannel(TakeY), return 0; "Lightness Channel"); "Saturation Channel"); "Black Channel"); "Cyan Channel"); "Magenta Channel"); "Yellow Channel"); Функция SplitChannel возвращает указатель на упакованный DIB-растр. Функция KDIBView: :CreateNew использует его для создания нового дочернего окна MDI, в котором этот DIB-растр выводится. При выборе одной из команд выделения каналов в главном окне MDI создается несколько новых окон; в каждом окне выводится новый DIB-растр в оттенках серого. На рис. 12.3 показан результат деления куба RGB на каналы RGB. Обратите внимание: в оттенках серого светлые цвета обладают более высокой интенсивностью, темные цвета — более низкой интенсивностью. Это объясняет и то, почему один из трех ромбов на каждом изображении окрашен в чистый белый цвет — потому что на этой грани исходного цветного куба соответствующий канал обладал максимальной интенсивностью (255). Ряс. 12.3. Выделение цветовых каналов RGB
686 Глава 12. Графические алгоритмы и растры Windows Гистограмма Чтобы наглядно показать, что класс KPixel Mapper является родовым классом для преобразования пикселов, мы построим совершенно другой производный класс — генератор гистограмм. // Класс для построения гистограмм RGB, производный от KPixelMapper class KHistogram : public KPixelMapper { int m_FreqRed[256]; int m_FreqGreen[256]; int m_FreqBlue[256]; int m_FreqGray[256]; // Вернуть true, если данные изменились virtual bool MapRGB(BYTE & red. BYTE & green, BYTE & blue) { m_FreqRed[red] ++; m_FreqGreen[green] ■ m_FreqBlue[blue] m_FreqGray[(red * 77 + green * 150 + blue * 29 + 128 ) / 256] ++; return false; } public: void SampleCKImage & dib); void KHistogram::SampleCKImage & dib) { memset(m_FreqRed. 0, sizeof(m_FreqRed)): memset(m_FreqGreen. 0, sizeof(m_FreqGreen)); memset(m_FreqB1ue, 0. sizeof(m__FreqBlue)); memset(m_FreqGray, 0, sizeof(m_FreqGray)); dib. Pixel Transforms this): } Класс KHistogram подсчитывает относительные частоты составляющих RGB и уровня серого в четырех целочисленных массивах. Реализация MapRGB просто увеличивает соответствующие счетчики. После вызова KHistogram::Sample накопленные данные гистограмм можно вывести в графическом виде — это поможет пользователю понять, какие изменения следует внести в изображение. Пространственные фильтры В приведенном выше алгоритме значение выходного пиксела определяется одним входным пикселом. Существует другой класс графических алгоритмов, в которых выходной пиксел вычисляется по смежным пикселам. Такие алгоритмы обычно называются пространственными фильтрами (spatial filters). Например,
Пространственные фильтры 687 фильтр сглаживания может генерировать выходной пиксел, вычисляя среднее значение для блока размерами 3x3 пиксела, что позволяет отфильтровать случайный шум. Пространственный фильтр получает исходный растр и строит по нему растр- приемник. Обычно пространственный фильтр обрабатывает блоки, состоящие из N х N пикселов, где N — нечетное числов. В большинстве распространенных пространственных фильтров N = 3. Центр блока соответствует текущему обрабатываемому пикселу, а остальные пикселы — его соседям. При нечетном N блок симметричен относительно центрального пиксела. При обработке всего изображения пространственный фильтр не может применяться к пикселам, расположенным на расстоянии менее (N - 1)/2 пикселов от края, поскольку в этом случае некоторые пикселы блока N х N выходят за пределы изображения. Проблема решается либо прямым копированием пиксела источника в приемник, либо заполнением пикселов, расположенных близко от края, однородным цветом. Классы и функции, приведенные выше, не подходят для работы с пространственными фильтрами. Необходимо новое решение, которое позволяло бы использовать в качестве входных данных для каждого пиксела блок из N х N пикселов. В листинге 12.7 приведен абстрактный класс KFilter. Листинг 12.7. Класс KFilter для работы с пространственными фильтрами // Абстрактный класс для применения пространственных фильтров // на уровне отдельных пикселов и строк развертки class KFilter { int mjiHalf; virtual BYTE Kernel(BYTE * pPixel. int dx, int dy) - 0; public: int GetHalf(void) const { return mjnHalf; } KFilter(void) { mjnHalf - 1; } virtual ~ KFilterO { } virtual void Filter8bpp(BYTE * pDst, BYTE * pSrc. int nWidth, int dy); virtual void Filter24bpp(BYTE * pDst. BYTE * pSrc, int nWidth. int dy); virtual void Filter32bpp(BYTE * pDst, BYTE * pSrc, int nWidth, int dy); virtual void DescribeFilter(HDC hDC. int x. int y) }: void KFilter::Filter8bpp(BYTE * pDst. BYTE * pSrc. int nWidth, int dy) { memcpy(pDst, pSrc, mjnHalf); pDst +« mjnHalf; pSrc += mjnHalf; for (int i=nWidth - 2 * mjiHalf; i>0: i--) * pDst ++ - Kernel(pSrc++, 1. dy); Продолжение^
688 Глава 12. Графические алгоритмы и растры Windows Листинг 12.7. Продолжение memcpy(pDst, pSrc, mjiHalf); } void KFilter::Filter24bpp(BYTE * pDst, BYTE * pSrc, int nWidth, int dy) { memcpy(pDst. pSrc. mjiHalf * 3); pDst += m_nHalf * 3; pSrc += mjiHalf * 3; for (int i=nWidth - 2 * mjiHalf; i>0; i--) { * pDst ++ = Kernel(pSrc++, 3, dy); * pDst ++ = Kernel(pSrc++, 3. dy); * pDst ++ = Kernel(pSrc++. 3, dy); } memcpy(pDst, pSrc. mjiHalf * 3); } void KFilter::Filter32bpp(BYTE * pDst, BYTE * pSrc, int nWidth. int dy) { memcpy(pDst, pSrc, mjiHalf * 4); pDst +- mjiHalf * 4; pSrc += mjiHalf * 4; for (int i=nWidth - 2 * mjiHalf: i>0; i--) { * pDst ++ = Kernel(pSrc++. 4, dy); * pDst ++ - Kernel(pSrc++, 4, dy); * pDst ++ = Kernel(pSrc++, 4. dy); * pDst ++ - * pSrc++: // Копировать альфа-канал } memcpytpDst, pSrc, mjiHalf * 4); } Класс KFilter выглядит значительно проще класса KPixelTransform — в основном из-за того, что он работает только с 8-, 24- и 32-разрядными растрами в оттенках серого. Пространственные фильтры выполняют с пикселами математические операции, не имеющие нормальной интерпретации для изображений с палитрой. В принципе можно было организовать поддержку для 15- и 16-разрядных изображений, однако это существенно увеличит объем работы. Класс KFilter работает с одноканальным изображением в оттенках серого. 24- и 32-разрядные изображения рассматриваются как совокупность нескольких каналов, обрабатываемых независимо друг от друга. Чисто виртуальная функция KFilter::Kernel определяет принцип работы пространственного фильтра. Для Kfilter:: Kernel пиксел представляет собой один байт в интервале 0...255, определяющий интенсивность данного канала. Функция получает указатель на текущий пиксел и смещения следующих пикселов в той же строке и том же столбце. Зная эти три величины, реализация Kernel может обратиться к любому из соседних пикселов при помощи несложных операций сложения и вычитания. Функция возвращает байт, который записывается в выходное изображение вызывающей стороной. Как видите, 15- и 16-разрядные строки развертки плохо вписываются
Пространственные фильтры 689 в эту модель. Переменная mjiHalf содержит значение (N-l)/2, поэтому для фильтров 3x3 она обычно равна 1. Методы Fi1ter8bpp, Fi1ter24bpp и Fi1ter32bpp обрабатывают три типа строк развертки, которые мы поддерживаем: 8-, 24-и 32-разрядные. Они получают указатель на строки развертки приемника и источника, ширина строки развертки в пикселах и смещение следующей строки. В каждой строке развертки первые и последние mjiHalf пикселов просто копируются. Остальные пикселы передаются методу Kernel поканально, а полученные результаты записываются в приемную строку развертки. Функции объявлены виртуальными, чтобы их можно было переопределить в производных классах. В класс KImage добавлен новый метод KImage:: Spatial Filter, предназначенный для передачи DIB классу KFilter. Метод создает новый приемный массив пикселов, копирует в него несколько первых и последних необрабатываемых строк развертки и вызывает один из методов фильтрации KFilter для обработки остальных строк. В завершение старый массив пикселов заменяется новым. Приведенную реализацию можно изменить таким образом, чтобы она генерировала новый растр или сохраняла результат в обработанном массиве пикселов источника, чтобы дополнительные затраты памяти не превышали размера нескольких строк развертки. boo! KImage::SpatialFi 1 ter(KFi 1 ter & pFilter) { BYTE * pDestBits = new BYTE[m_nImageSize]; if ( pDestBits«NULL ) return false; for (int y=0; y<m_nHeight; y++) { BYTE * pBuffer = (BYTE *) m_pBits + m_nBPS * y; BYTE * pDest = (BYTE *) pDestBits + m_nBPS * y; if ( (y>=filter.GetHalf()) && (y<(m_nHeight- filter.GetHalfO)) ) switch ( m_nImageFormat ) { case DIBJBPP: filter.Filter8bpp(pDest, pBuffer, mjiWidth. mjiBPS): break; case DIB_24RGB888: // 24-разрядный RGB pFilter->Filter24bpp(pDest. pBuffer. mjiWidth, mjiBPS); break; case DIB_32RGBA8888: // 32-разрядный формат RGBA case DIB_32RGB888: // 32-разрядный формат RGB pFilter->Filter32bpp(pDest, pBuffer. mjiWidth. mjiBPS): break; default: delete [] pDestBits; return false;
690 Глава 12. Графические алгоритмы и растры Windows } else memcpy(pDest, pBuffer, mjiBPS); } memcpy(m_pBits. pDestBits. m_nImageSize); array delete [] pDestBits; return true; } Пространственный фильтр 3x3 обычно описывается матрицей 3 х 3 и весом. Числа матрицы 3x3 умножаются на цветовые значения соответствующих пикселов блока, а сумма делится на общий вес. Результат может выйти за границы интервала 0...255, используемого для хранения интенсивности цветового канала, поэтому результат иногда приходится усекать. Некоторые фильтры перед усечением прибавляют к результату константу. Ниже приведет шаблон для класса, поддерживающего пространственные фильтры 3 х 3 с дополнительным весом и прибавлением константы: template <int kOO. int kOl. int k02, int klO, int kll. int kl2. int k20, int k21, int k22, int weight, int add, bool checkbound, TCHAR * name> class K33Filter : public KFilter { virtual BYTE Kernel(BYTE * P. int dx, int dy) { int r - ( P[-dy-dx] * kOO + P[-dy] * kOl + P[-dy+dx] * k02 + PC -dx] * klO + P[0] * kll + P[ +dx] * kl2 + P[ dy-dx] * k20 + P[dy] * k21 + P[ dy+dx] * k22 ) / weight + add; if ( checkbound ) if ( г < 0 ) return 0; else if ( r > 255 ) return 255; return r; } }: Класс K33Fi1ter имеет 12 параметров. Первые девять параметров определяют матрицу коэффициентов 3 х 3, за которой следует вес, прибавляемая константа и логическое значение, управляющее проверкой границ. Вообще говоря, реализацию можно было построить на вещественных вычислениях — код остается прежним, изменится только тип данных. Однако в этом случае нам придется выполнять девять умножений с плавающей точкой и преобразовывать вещественное число в целое. Шаблон K33Filter существенно улучшает быстродействие пространственного фильтра за счет применения только целых параметров. Мы работаем лишь с целыми числами, и каждый фильтр получает собственный набор параметров шаблона. С точки зрения компилятора девять умножений и одно деление представляют собой легко оптимизируемые операции с константами.
Пространственные фильтры 691 Если флаг проверки границ не установлен, компилятору не нужно генерировать соответствующий код. При этом увеличение объема кода оказывается минимальным, поскольку для каждого фильтра переопределяется всего одна функция. Давайте воспользуемся нашими классами для определения нескольких пространственных фильтров и посмотрим, на какие чудеса они способны. Фильтры сглаживания и резкости На рис. 12.4 после первого исходного рисунка показаны результаты применения трех пространственных фильтров: сглаживания, сглаживания по Гауссу и резкости (для наглядности масштаб равен 3:1). Гауссово сглаживание | 0 1 0| * I 1 k 1| | 0 1 0| / 8 Сглаживание | 1 1 1| * | 1 1 1| | 1 1 1| / 9 Заострение | 0 -1 8| |-1 9 -1| | 0 -1 0| / 5 Рис. 12.4. Фильтры сглаживания и резкости Три показанных на рисунке фильтра определяются следующим образом: TCHAR szSmooth[] = _T("Smooth"); TCHAR szGuasianSmooth[] - _T("Guasian Smooth"); TCHAR szSharpening[] = _T("Sharpening"); K33Filter< 1, 1. 1, 1, 1, 1. 1. 1. 1. 9, 0. false. szSmooth > filter33_smooth; K33Filter< 0, 1, 0, 1. 4. 1. 0. 1. 0, 8. 0, false, szGuasianSmooth > filter33_guasiansmooth; K33Filter< 0. -1. 0, -1. 9. -1. 0. -1. 0, 5. 0, true, szSharpening > filter33_sharpening: Вверху слева изображена исходная картинка. Справа от нее показан результат применения сглаживающего фильтра. Его матрица 3x3 состоит из одних единиц, а вес равен 9. Следовательно, этот фильтр присваивает пикселу среднее
692 Глава 12. Графические алгоритмы и растры Windows значение пикселов в блоке 3x3. Сглаживающий фильтр называется низкочастотным фильтром, поскольку он сохраняет низкочастотные участки и отфильтровывает высокочастотные искажения. В частности, он может использоваться для сглаживания линий, фигур и растров, выводимых средствами GDI. На рисунке видно, как сглаживающий фильтр маскирует зазубренные края исходной картинки. После применения сглаживающего фильтра на границах глифа появляются серые пикселы. Фильтр сглаживания по Гауссу также относится к категории низкочастотных фильтров. Вместо равномерного распределения этот фильтр назначает больший весовой коэффициент центральному пикселу. Фильтры этого типа могут определяться и для большего радиуса; на рисунке показан фильтр 3x3. Фильтр резкости вычитает соседние пикселы из текущего, чтобы подчеркнуть переходы в изображении. Он относится к категории высокочастотных фильтров, которые выделяют высокочастотные компоненты изображения и оставляют низкочастотные участки без изменений. Регулируя весовой коэффициент центрального пиксела, можно менять степень резкости. Для монохромного изображения, показанного на рисунке, результат применения фильтра резкости практически незаметен. Выделение границ и рельеф На рис. 12.5 показаны результаты применения фильтра Лапласа и двух рельефных фильтров. Эти фильтры определяются следующим образом: TCHAR szLaplasian[] = JTCLaplasian"); TCHAR szEmbossl35[] - _T("Emboss 135°"); TCHAR szEmboss90[] - _T("Emboss 90° 50Г); K33F1lter<-l, -1. -1. -1. 8, -1. -1. -1. -1. 1, 128, true, szLaplasian > filter33_lap1asian; K33Filter< 1, 0, 0, 0. 0. 0, 0. 0. -1. 1, 128. true, szEmbossl35 > filter33_embossl35; K33Filter< 0. 1. 0, 0. 0. 0, 0. -1. 0. 2, 128, true, szEmboss90 > filter33_emboss90; По виду матрицы фильтр Лапласа похож на высокочастотный фильтр, но он генерирует абсолютно другое изображение. Фильтр Лапласа относится к категории фильтров выделения границ с нулевой суммой коэффициентов матрицы. Фильтр выделения границ заменяет равномерно окрашенные области черным цветом, а области с изменениями — цветом, отличным от черного. В приведенном примере фильтр прибавляет к каждому каналу 128, чтобы отрицательный результат не заменялся нулем. В результате прибавления 128 равномерно окрашенные области становятся серыми. Следующие два фильтра, называемые рельефными фильтрами, преобразуют цветное изображение в оттенки серого со своеобразными объемными эффектами. В одном углу матрицы рельефного фильтра стоит 1, а элемент в противоположном углу равен -1. Применение рельефного фильтра можно рассматривать как вычитание изображения, смещенного на определенную величину от оригинала. Результат увеличивается на 128, чтобы нулевая точка переместилась в середину шкалы оттенков серого. Относительные позиции пикселов со значения-
Пространственные фильтры 693 ми 1 и -1 определяют направление рельефного выделения. В нашем примере продемонстрированы два направления. Во втором примере результат умножения делится на 2, поэтому степень рельефности изображения уменьшается. Рис. 12.5. Выделение границ и рельефные фильтры Морфологические фильтры На рис. 12.6 показаны три новых пространственных фильтра: фильтры сжатия и расширения, а также контурный фильтр. Чтобы результат был более наглядным, изображения выводятся в масштабе 2:1. Это так называемые морфологические фильтры. Они отличаются от предыдущих фильтров, основанных на линейной комбинации пикселов. Морфологический фильтр использует матрицу N х N для проверки соседних пикселов. Результат проверки определяет цвет пиксела, находящегося в центре. Фильтр сжатия генерирует черный цвет лишь в том случае, если все пикселы блока окрашены в черный цвет. В противном случае генерируется белый цвет. Таким образом, в результате применения фильтра сжатия белые участки изображения расширяются. Фильтр расширения генерирует белый цвет лишь в том случае, если все пикселы блока окрашены в белый цвет. В противном случае генерируется черный цвет. Таким образом, в результате применения фильтра расширения белые участки изображения сужаются.
694 Глава 12. Графические алгоритмы и растры Windows m m ^н^ннщ^нщщ^нщ Контур m ■ Рис. 12.6. Морфологические фильтры Контурный фильтр сначала выполняет сжатие, а затем вычитает из полученного изображения оригинал. Для равномерно окрашенных областей контурный фильтр генерирует черный цвет (0), поскольку сжатое изображение совпадает с оригиналом. Новые белые пикселы, возникшие в результате сжатия, остаются белыми. В результате возникает белый контур исходного изображения на черном фоне. На рисунке показано, к каким результатам приводит применение всех трех фильтров к монохромному изображению текстового символа. Черный цвет является основным, а белый — фоновым, поэтому при сжатии белые фоновые участки увеличиваются, а основные черные — уменьшаются. Расширение приводит к обратным последствиям. В нашем примере линии буквы «ш» при сжатии становятся тоньше, а при расширении — толще. Контурный фильтр оставляет белый контур буквы. Эти морфологические фильтры создавались для работы с монохромными изображениями. При работе с цветными изображениями, разделенными на несколько каналов в оттенках серого, расширение имитируется через вычисление минимума, а сжатие — через вычисление максимума. Ниже приведена наша реализация фильтра расширения. Функция KErosion:: Kernel находит минимальное значение по девяти пикселам блока 3 х 3 и возвращает его вызывающей стороне. Для цветных изображений также можно было вычислить минимальное значение по восьми пикселам, окружающим центральный пиксел, и вернуть в качестве результата среднее арифметическое центрального пиксела и минимума. В этом варианте эффект расширения несколько снижается. Чтобы создать фильтр сжатия, достаточно вместо минимума вычислить максимум. // Минимум - расширение темных областей class KErosion : public KFilter
Итоги 695 { inline void smaller(BYTE &x. BYTE y) { if ( у < x ) x = у; } BYTE Kernel(BYTE * pPixel. int dx, int dy) { BYTE m = pPixel[-dy-dx]; smaller(m, pPixel[-dy]); smaller(m, pPixel[-dy+dx]); smaller(m, pPixel[ -dx]); smaller(m, pPixel[ +dx]); smaller(m, pPixel[ dy-dx]); smaller(m, pPixel[dy]); smaller(m. pPixel[ dy+dx]); return min(pPixel[0], m): // /2; } }: Обработка изображений — весьма интересная тема. Впрочем, книга все же посвящена графическому программированию, поэтому нас в первую очередь интересует прямой доступ к массивам пикселов DIB и DIB-секций и то, как с его помощью реализовать все эти замечательные эффекты. Для этого мы создали несколько родовых классов и шаблонов, к которым приложения могут добавить собственные компоненты для решения специализированных задач. Итоги Эта глава была посвящена прямому доступу к массивам пикселов DIB и DIB- секций. На основе прямого доступа к пикселам растра строится множество интересных алгоритмов и эффектов. В этой главе было показано, как при помощи прямого доступа к пикселам реализовать общий алгоритм аффинного преобразования растров без использования средств поворота растров, доступных только в системах семейства NT. Как вы убедились, специализированный, высоко оптимизированный алгоритм аффинного преобразования, работающий только с целыми числами, способен обрабатывать миллионы пикселов в секунду. На базе прямого доступа к пикселам реализуются эффектные графические алгоритмы, не поддерживаемые напрямую средствами GDI. В этой главе был построен набор родовых классов и шаблонов для реализации алгоритмов преобразования цветов и пикселов, а также пространственных фильтров. Используя абстрактные классы и шаблоны, разработанные в этой главе, можно создать множество других графических алгоритмов. Приемы, рассмотренные в этой главе, могут использоваться для создания эффекта сглаживания или имитации рельефа. Кроме того, их можно использо-
696 Глава 12. Графические алгоритмы и растры Windows вать на поверхностях DirectDraw, которые фактически представляют собой DIB- секции с аппаратным ускорением. Глава 13 посвящена палитрам, квантованию цветов и полутоновым операциям. В главе 17 рассматривается декодирование и печать графики в формате JPEG. В главе 18 прямой доступ к пикселам используется применительно к поверхностям DirectDraw. Примеры программ К главе 12 прилагается всего одна программа Imaging, иллюстрирующая весь изложенный материал (табл. 12.1). Таблица 12.1. Программа главы 12 Каталог проекта Описание Samples\Chapt_12\Imaging Демонстрация прямого доступа к пикселам, преобразования цветных изображений в оттенки серого, гамма- коррекции, аффинных преобразований растров, преобразований цветов и пикселов и различных пространственных фильтров. Откройте BMP-файл и поэкспериментируйте с командами меню Color и View
Глава 13 Палитры До настоящего момента мы использовали в своих программах множество цветов; мы говорили о цветных перьях и кистях, 16, 24- и 32-разрядных растрах, градиентных заливках, альфа-наложении и обработке изображений. Но стоит запустить эти программы на экране с 256 цветами, и все богатство красок мгновенно пропадает. Многоцветные изображения тускнеют и заменяются уродливыми имитациями. Проблема связана с палитрой — инструментом, который Windows GDI и разработчики видеоадаптеров позаимствовали у художников. Палитра предназначена для отображения в цвета RGB цветовых индексов в кадровых буферах с палитрой. В этой главе вы узнаете, что произойдет, если полностью игнорировать существование палитры; какие минимальные меры нужны для того, чтобы ваша программа с приемлемым качеством работала в режиме с палитрой, и как извлечь максимум пользы из работы с палитрой. В этой главе также рассматривается квантование цветов — алгоритм преобразования изображений High Color и True Color в индексированные цветные изображения с оптимальной цветовой таблицей. Системная палитра Попробуйте переключить Windows в 256-цветный видеорежим, но для начала выведите на экран какое-нибудь красочное изображение. Например, на рабочем столе имеется несколько многоцветных значков; меню Start (Пуск) тоже выглядит довольно ярко, а в диалоговом окне для выбора цвета должно отображаться множество цветов. Теперь попробуйте угадать, сколько цветов вы в действительности видите на экране в 256-цветном режиме. Чтобы получить правильный ответ, сохраните копию экрана и при помощи графического редактора подсчитайте точное количество цветов в сохраненном растре. Ответ — не более 20 цветов.
698 Глава 13. Палитры В 256-цветном режиме весь пользовательский интерфейс операционной системы Windows строится с использованием всего 20 цветов. Если приложение не работает с палитрой, в его распоряжении обычно оказываются те же 20 цветов. Значки и панели инструментов тоже выводятся в 20 цветах. Функция LoadBitmap преобразует любой цветной растр в 256-цветный DDB-растр, но реально используются только 20 цветов. DIB и DIB-секции тоже выводятся в 20 цветах. Все остальные цвета получаются посредством смешения (dithering), образующего комбинации из этих 20 цветов. Но самое грустное заключается в том, что даже 256-цветные растры, загруженные функцией LoadBitmap, на экране выводятся только в 20 цветах. Чтобы лучше понять сущность проблемы и пути ее решения, необходимо разобраться в том, что такое системная палитра, логическая палитра и что происходит при реализации логической палитры. Параметры экрана Из-за падения цен на память 256-цветный видеорежим встречается очень редко. Впрочем, старые программы еще иногда требуют, чтобы вы переключились в 256-цветный режим. Для тестирования программ этой главы необходимо переключиться в 256-цветный режим. Обычно это делается при помощи приложения Display (Экран) панели управления. Чтобы проверить, поддерживает ли устройство аппаратную палитру, программа должна запросить у устройства флаг RASTERCAPS и проверить в нем бит RCJPALETTE. Если этот бит установлен, графическое устройство работает в режиме с поддержкой палитры. Подробную информацию о текущих параметрах видеоадаптера можно получить при помощи функции EnumDisplaySettings. Если приложению потребуется изменить параметры экрана, вызовите функцию Change- DisplaySettings. Ниже приведена функция Switch8bpp из программы Palette этой главы; если текущий видеорежим не поддерживает аппаратную палитру, программа предлагает пользователю переключиться в 256-цветный режим. B00L Switch8bpp(void) { HDC hDC - GetDC(NULL); int hasPalette = (GetDeviceCaps(hDC, RASTERCAPS) & RC_PALETTE); ReleaseDC(NULL. hDC); if ( hasPalette ) // Палитра поддерживается return TRUE; int rslt = MessageBox(NULL, _T("Switch to 256 color mode?"). _T("Palette"), MBJESNOCANCEL); if ( rslt—IDCANCEL ) return FALSE; if ( rslt==IDYES ) // Выбрано переключение в 256-цветный режим { DEVM0DE dm; dm.dmSize = sizeof(dm); // Важно, предотвращает GPF
Системная палитра 699 dm.dmDriverExtra = 0; EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, & dm); // Текущие параметры dm.dmBitsPerPel = 8; // Перейти к кодировке 8 бит/пиксел ChangeDisplaySettings(&dm. 0); // ПЕРЕКЛЮЧИТЬ } return TRUE; } Функция Switch8bpp при помощи GetDeviceCaps проверяет, поддерживает ли текущий первичный экран палитру, и если не поддерживает — выводит запрос на изменение видеорежима. Если пользователь соглашается на изменение параметра, функция EnumDisplaySettings возвращает структуру DEVMODE с текущими параметрами устройства. Присвоив полю dmBitsPerPel структуры DEVMODE значение 8, функция вызывает ChangeDi spl aySetti ngs и передает измененную структуру DEVMODE для переключения в 256-цветный режим. При этом всем окнам верхнего уровня посылается сообщение WMDISPLAYCHANGE. Получение системной палитры В 256-цветном режиме каждый пиксел представлен в кадровом буфере видеоадаптера одним байтом. Один байт позволяет закодировать до 256 разных цветов, одновременно отображаемых на экране. Точный состав цветов определяется палитрой видеоадаптера, которая представляется пользовательским приложениям в виде системной палитры. Системная палитра в 256-цветном режиме представляет собой таблицу из 256 структур PALETTEENTRY. В GDI предусмотрено несколько функций для получения информации и управления системной палитрой. typedef struct { BYTE peRed; BYTE peGreen; BYTE peBlue; BYTE peFlags; } PALETTEENTRY; UINT GetSystemPaletteEntries(HDC hDC. UINT iStartlndex, UINT nEntries. LPPALETTEENTRY lppe); UINT GetSystemPaletteUse(HDC hDC); UINT SetSystemPaletteUse(HDC hDC. UINT uUsage); Структура PALETTEENTRY определяет цвет по его компонентам RGB. Поле peFlags используется при создании логических палитр (см. следующий раздел). Функция GetSystemPaletteEntries возвращает блок элементов текущей системной палитры графического устройства. Первый параметр определяет манипулятор контекста устройства. Следующие два параметра определяют первый и последний копируемый элемент, а последний параметр содержит указатель, по которому записывается массив. Если точное количество элементов в системной палитре неизвестно, его можно получить вызовом GetSystemPaletteEntries (hDC, 0, 0, NULL). Системная палитра является ресурсом уровня графического устройства, который совместно используется всеми созданными для него контекстами. Для графического видеоадаптера системная палитра всегда одна и та же. Приложе-
700 Глава 13. Палитры ния могут модифицировать системную палитру по определенным правилам, поэтому ее содержимое, вообще говоря, не является чем-то постоянным и жестко заданным. После изменения системной палитры операционная система отправляет сообщение WM_PALETTECHANGED всем окнам верхнего уровня, чтобы дать им возможность отреагировать на изменения. При необходимости окна верхнего уровня должны сами отправить сообщения своим дочерним окнам. Чтобы лучше понять динамическую природу системной палитры, мы создадим маленькое временное окно для вывода системной палитры и отслеживания всех изменений. В листинге 13.1 приведен класс KPaletteWnd. Метод Create- PaletteWindow этого класса создает временное окно для вывода всех цветов системной палитры. При выводе используется инициализированный 256-цветный аппаратно-зависимый растр (DDB). Предполагается, что видеоадаптер использует для представления DDB-растра одну цветовую плоскость с кодировкой 8 бит/пиксел. Данные инициализации DDB состоят из однородных цветных блоков 16 х 16 с цветами в интервале от 0 до 255. Поскольку предполагается, что данные соответствуют внутреннему формату DDB, создание и вывод DDB не требуют преобразований цветов. Следовательно, байт DDB со значением 0 будет соответствовать первому элементу системной палитры. Обработчик сообщения WM_PALETTECHANGED просто обновляет изображение в окне. Листинг 13.1. Класс для наглядного представления изменений в системной палитре class KPaletteWnd : public KWindow { HDC m_hDC; TCHAR m_name[MAX_PATH]; PALETTEENTRY m_Entry[256]; int m_nEntry; int mjnGeneration; virtual LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch ( uMsg ) { case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = BeginPaint(hWnd, & ps); HDC hMemDC = CreateCompatibleDC(hDC); BYTE data[80][80]; // Данные инициализации // для 8-разрядного DDB for (int i=0; i<80; i++) for (int j=0; j<80; j++) { data[i][j] = (i/5) * 16 + (j/5); .if ( ((i%5)—0) || ((jX5)--0) ) data[i][j] = 255; } HBITMAP hBitmap - CreateBitmap(80, 80. 1. 8, data);
Системная палитра 701 HGDIOBJ hOld = SelectObjectChMemDC. hBitmap); StretchBlt(hDC.10.10.256,256.hMemDC.0,0.80,80.SRCCOPY); SelectObjectChMemDC, hOld); DeleteObject(hBitmap); DeleteObject(hMemDC); EndPaint(hWnd, & ps); } return 0; case WM_PALETTECHANGED; { InvalidateRect(hWnd, NULL, TRUE); return 0; case WMJCDESTROY: ReleaseDC(m_hWnd. m_hDC); return 0; } return DefWindowProc(hWnd, uMsg, wParam. lParam); } public: void CreatePaletteWindow(HINSTANCE hlnst) { if ( ! Switch8bpp() ) // Отмена return; CreateEx(0, _T("SysPalette"). _T("System Palette"), WSJMRLAPPEDWINDOW | WS_CLIPCHILDREN. CWJJSEDEFAULT, CWJJSEDEFAULT. 290. 340, NULL, NULL, hlnst); ShowWindow(nShow); UpdateWindowO; } }: Код на компакт-диске несколько сложнее того, что приведен в листинге 13.1. При обработке сообщения WM_PALETTECHANGED используется вызов функции Get- SystemPaletteEntries, возвращающий все содержимое системной палитры, чтобы при получении сообщения об изменении палитры можно было проанализировать новые цвета палитры и сравнить их с предыдущими цветами палитры. На рис. 13.1 изображены два окна с содержимым системной палитры. Если запустить программу с открытым окном системной палитры, вы убедитесь, что сначала системная палитра состоит из цветов, более или менее равномерно распределенных в цветовом пространстве RGB. Цветовое пространство RGB состоит из трех каналов. Для равномерного распределения цветов каждый канал должен содержать приблизительно 2561/3=6,34 уровня; ближайшее целое значение равно 6. Значения каждого канала RGB лежат в интервале от 0 до 255; таким образом, интервал аппроксимируется 6 уровнями: 0, 51, 102, 153, 204 и 255. Если каждый канал RGB будет состоять из этих 6 уровней, в нашем распоряжении окажется 216 цветов. Эти цвета называются «web-цветами», поскольку они поддерживаются браузерами как Microsoft, так и Netscape. Изображения, состоящие из этих цветов, отображаются в обоих типах браузеров без смешения.
702 Глава 13. Палитры Кроме web-цветов исходная палитра также содержит дополнительные оттенки серого (то есть цвета с одинаковыми значениями красной, зеленой и синей составляющих). . System Palette [0] liil - System Palette [1] Original 217 web colors, 7 grayscale colors ■IIHIi ■iiilM№i:il 208 entries changed by windowfl 80212) Cj 15 web colors, 19 grayscale colors lllllllii1 ■■■■■■■»■■■■■■■ ■iiiiiiiiii^ mi H£ Ш& Ш& ШШ Ш ШШ 888 Ш& <Ш ъ& ШЬ ШЬ &Ё& "'% m§r- j ■■■■ шшштшшштшшштш mm ■iiiiiiiiiiiiin ■ ШШШ1Ш1Ш ■■■■■■ mmmf iiiiiiiiiiiiiiiii шштжшшшшштжтшжшж ШШШШШШШтШШтШШШШШ штшшшштттшшшшшшт tuiiiiiiiiiiiii lIMMIIMIIiili ЯЯЯЯЯ ЯЯЯЯК ЗбНБ ЭТЯ» iffl» Зййййй ¥№fi Щ WW ЯЯЯЯЖ ЙНЙЙ ЯЯЙР* ЯВЙ» $888s «wS* S^jP* шштшы^шшт^шшшт^т шшшшшш шшшш mm № Рис. 13.1. Анализ изменений в системной палитре На рисунке окно с системной палитрой показано в двух состояниях. В первом состоянии выводится системная палитра с 217 web-цветами (один цвет дублируется) и 7 дополнительными оттенками серого. После запуска приложения с красочной заставкой палитра драматически изменяется: 208 цветов системной палитры изменились так, чтобы палитра позволяла как можно лучше отобразить заставку. Палитра часто изменяется и при запуске и закрытии других приложений, даже при переключении между дочерними окнами в приложениях MDI. Статические цвета Похоже, цвета в начале и в конце палитры никогда не изменяются. В этих двух частях палитры хранятся системные статические цвета, зарезервированные операционной системой для пользовательского интерфейса. Операционная система Windows обычно резервирует 20 статических цветов, хотя их количество можно уменьшить. Функция GetSystemPalettellse возвращает флаг, который обозначает количество статических цветов, используемых системой. Если возвращается значение SYSPAL_N0STATIC, система использует два статических цвета — черный и белый; если возвращаемое значение равно SYSPALSTATIC, используются 20 статических цветов. В Windows 2000 появился новый флаг SYSPALN0STATIC256, означающий полное отсутствие зарезервированных статических цветов. Функция SetSystemPalettellse изменяет текущий режим статических цветов при помощи описанных выше флагов. Статические цвета используются такими
Системная палитра 703 функциями API, как GetSysColor и SetSysColor. Следовательно, если приложение хочет уменьшить количество статических цветов до SYSPAL_NOTSTATIC, оно должно сохранить всех текущие системные цвета, изменить количество статических цветов, заменить системные цвета своими, а потом восстановить исходные системные цвета. Количество статических цветов следует изменять только в крайних случаях. Например, если «медицинское» приложение захочет отобразить 256 оттенков серого цвета рентгеновского снимка в 256-цветном режиме, ему придется использовать режим SYSPALNOSTATIC или SYSPAL_N0STATIC256. Расположение 20 статических цветов выглядит довольно любопытно. Шестнадцать цветов взяты из 16-цветной палитры VGA; еще четыре определяются текущей цветовой схемой. В табл. 13.1 перечислены статические цвета для двух цветовых схем: традиционной и схемы Spruce («Ель» в русской версии Windows). Таблица 13.1. Статические цвета Индекс 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0xF6 Значение RGB 0x00, 0x00, 0x00 0x80, 0x00, 0x00 0x00, 0x80, 0x00 0x80, 0x80, 0x00 0x00, 0x00, 0x80 0x80, 0x80, 0x80 0x00, 0x80, 0x80 0хС0, ОхСО, ОхСО OxCO, OxDC, ОхСО 0x59, 0x97, 0x64 ОхАб, OxCA, 0xF0 0хА2, 0хС8, 0хА9 OxFF, OxFb, 0xF0 0xD0, ОхЕЗ, 0xD3 Название Черный Темно-красный Темно-зеленый Темно-желтый Темно-синий Темно-малиновый Темно-голубой Светло-серый Денежный зеленый Небесный Кремовый Применение по умолчанию C0L0R_WIND0WFRAME, C0L0R_MENUTEXT, C0L0R_WIND0WTEXT, COLORJDDKSHADOW, C0L0RJNF0TEXT C0L0R_ACTIVECAPTI0N, COLORJIGHLIGHT, C0L0R_BTNSHAD0W, C0L0R_GRAYTEXT COLORJENU, C0L0R_ACTIVEB0RDER, C0L0RJNACTIVEB0RDER, C0L0R_BTNFACE, COLORJDLIGHT C0L0R_SCR0LLBAR, C0L0R_APPW0RKSPACE, C0L0RJNACTVECAPTI0NTEXT, C0L0R_BTNHIGHLIGHT Продолжение &
704 Глава 13. Палитры Таблица 13.1. Продолжение Индекс Значение RGB Название Применение по умолчанию 0xF7 ОхЗА, ОхбЕ, 0хА5 0x21, 0x3F, 0x21 0xF8 0x80, 0x80, 0x80 0xF9 OxFF, 0x00, 0x00 OxFA 0x00, OxFF, 0x00 OxFB OxFF, OxFF, 0x00 OxFC 0x00, 0x00, OxFF OxFD OxFF, 0x00, OxFF OxFE 0x00, OxFF, OxFF OxFF OxFF, OxFF, OxFF Темно-серый Красный Зеленый Желтый Синий Малиновый Голубой Белый COLOR BACKGROUND C0L0R_WIND0W, C0L0R_CAPTI0NTEXT, COLORJIGHLIGHTTEXT, COLOR INFOBK Из 20 статических цветов 8 темных цветов размещаются в первых 8 позициях системной палитры, а 8 светлых цветов — в последних 8 позициях. Эти позиции используются для «чистых» цветов: красного, зеленого, синего, малинового, желтого, их темных вариантов и четырех оттенков серого. При использовании 20 статических цветов эти 16 всегда находятся на одних и тех же местах. Они размещаются в разных концах палитры для того, чтобы придать смысл базовым растровым операциям между ними. Очень важно, чтобы черный цвет занимал позицию 0, а белый — позицию 255, поскольку от этого зависит работа растровых операций с применением маски. Позицию 1 занимает темно-красный цвет (RGB(0x80, OxFF, 0x00)); если инвертировать индекс, он переходит в OxFE, что соответствует голубому цвету (RGBCOxOO, OxFF, OxFF)). Он не совсем совпадает с дополняющим цветом темно-красного в цветовом пространстве RGB (RGB(0x7F, OxFF, OxFF)), но достаточно близок к нему. При объединении красного и зеленого цвета получается чистый желтый цвет, поскольку 0xF9 | OxFA = OxFB. Четыре цвета в средней части палитры могут изменяться в соответствии с выбранной цветовой схемой. В табл. 13.1 приведены их RGB-цвета для двух цветовых схем. Эти цвета широко используются в пользовательском интерфейсе Windows. В последнем столбце таблицы показано, каким системным цветам они соответствуют по умолчанию. В 256-цветном режиме операционная система Windows всегда пытается использовать статические цвета в качестве системных. Логическая палитра Хотя системная палитра состоит из 256 цветов, если не предпринять особых мер, ваше приложение может работать лишь с 20 статическими цветами. Сие-
Логическая палитра 705 темная палитра является ресурсом уровня системы, а не контекста устройства. В контекстах устройств системной палитре соответствуют логические палитры. Логическая палитра управляет преобразованием цветов, используемых в графических командах GDI, в цветовые индексы кадрового буфера графической поверхности. Логическая палитра является одним из атрибутов контекста устройства. Логические палитры, как и логические кисти, логические перья, логические шрифты и т. д., образуют отдельный класс объектов GDI. Ниже приведены описания структур данных и функций, предназначенных для работы с логическими палитрами. UINT GetPaletteEntries(HPALETE hpal, UINT iStartlndex. UINT nEntries. LPPALETTEENTRY Ippe); HPALETTE CreateHalftonePalette(HDC hDC); HPALETTE SelectPalette(HDC hDC. HPALETTE hpal. BOOL bForceBackground); UINT RealizePalette(HDC hDC); BOOL ResizePaletteCHPALETTE hpal. UINT nEntries); BOOL UnrealizeObject(HGDIOBJ hgdiObj); BOOL ResizePaletteCHPALETTE hpal. UINT nEntries); typedef struct tagLOGPALETTE { WORD pal Version; WORD palNumEntries; PALETTEENTRY palPalEntry[l]; } LOGPALETTE; HPALETTE CreatePaletteCCONST LOGPALETTE * Iplgpl); Палитра по умолчанию Для получения логической палитры, связанной с контекстом устройства, следует воспользоваться функцией GetCurrentObjectChDC, OBJPAL). Новому контексту устройства по умолчанию назначается стандартная логическая палитра, возвращаемая функцией GetStockObject(DEFAULT_PALETTE). Палитра по умолчанию содержит ровно 20 статических цветов, приведенных в табл. 13.1; это ограничивает количество цветов, доступных приложению. Например, если использовать макрос PALETTE INDEX или PALETTERGB для определения цвета в контексте устройства с палитрой по умолчанию, из всех однородных цветов останутся доступными только 20 статических. Приведенная ниже функция WebColors выводит 216 web-цветов. На рис. 13.2 показан результат применения этой функции для палитры по умолчанию. На верхнем рисунке, в котором цвета задаются макросами RGB, большинство элементов получено смешением (кроме черного, красного, зеленого, синего, желтого, голубого, малинового и белого цветов, присутствующих в системной палитре). На нижнем рисунке, в котором использовались макросы PALETTERGB, для каждого цвета из 20 цветов палитры выбирается наиболее подходящий. В обоих случаях результат далек от ожидаемого.
706 Глава 13. Палитры наш! шшшшж тшшшт ПППППП ПППГПГ "' тшшшл ■мни* шшшш? шштш^ ■■■■№ ._ ■■■■«! вниз ■■■■■! ни» ми» мни» ШИШН1$8ё$Ш£$Ш Ш&Ш&Шк ИНИН ' II ' i| I'liMil ■ ниц ши« цма| х««й*й>ив Я! ИЯНШ ЭТИ ;% ^ MM*' ■«■£'»' ■ш К»! III Ш% \Я иь ■ffiffim MMffiffi ■шиш шттш ■■яка ■■яи мш ■ян 1111 Рис. 13.2. Вывод web-цветов с применением палитры по умолчанию void WebColors(HDC hDC. int x. int у. int crtyp) for (int r=0 for (int g*0 for (int b=0 r<6; g<6; b<6; r++) g++) b++) COLORREF cr; switch ( crtyp ) case 0 case 1 case 2 cr = R6B(r*51. g*51. b*51); break cr = PALETTERGB(r*51. g*51. b*51): break cr = PALETTEINDEX(r*36+g*6+b); break HBRUSH hBrush - CreateSolidBrush(cr); RECT rect = { r * 110 + g*16+ x, b*16+ y. r * 110 + g*16+15+x. b*16+15+y}; FillRectChDC. & rect, hBrush): DeleteObject(hBrush); Полутоновая палитра Чтобы увеличить количество цветов, можно воспользоваться полутоновой палитрой. Логическая полутоновая палитра создается функцией CreateHalftonePalette. Странно, что полутоновые палитры не входят в число стандартных объектов GDI — в этом случае они могли бы совместно использоваться всеми процессами в системе. Созданная полутоновая палитра подчиняется тем же правилам, что и другие объекты GDI; после завершения работы ее необходимо удалить. Логическая палитра выбирается в контексте устройства функцией Select- Palette. Обратите внимание: родовая функция SelectObject не подходит, поскольку при выборе палитры передается дополнительный параметр — признак фоновой палитры. Если последний параметр SelectPalette равен TRUE, палитра
Логическая палитра 707 считается фоновой (background); в противном случае, при выполнении ряда других условий, палитра считается основной (foreground). Перед использованием палитру необходимо «реализовать». Реализацией логической палитры называется процесс заполнения системной палитры в соответствии с требованиями приложения и построения таблицы соответствия между индексами логической и системной палитр. При реализации основной палитры нестатические цвета удаляются из системной палитры; цвета, отсутствующие среди статических, включаются в системную палитру вплоть до заполнения всех 256 элементов. Затем строится таблица соответствия цветов логической палитры индексам системной палитры, по которой цвета пикселов будут преобразовываться в индексы цветов кадрового буфера. Фоновой палитре уделяется меньше внимания; из системной палитры ничего не удаляется, а запрашиваемые цвета включаются лишь в неиспользованные позиции системной палитры. Основная палитра предназначена для текущего активного окна, обладающего фокусом ввода, а фоновая палитра обеспечивает сколько-нибудь приемлемый вид остальных окон, находящихся на экране. Код следующего фрагмента создает полутоновую палитру, выбирает ее, реализует и снова выводит диаграмму web-цветов. void TestHalftonePaletteCHDC hDC, HINSTANCE hlnstance) { HPALETTE hPal = CreateHalftonePalette(hDC); break; HPALETTE hOld = SelectPalette(hDC. hPal. FALSE); RealizePalette(hDC); WebColors (hDC. 10, 10. 0): WebColors (hDC. 10. 130. 1); SelectPalette(hDC. hOld. TRUE); DeleteObject(hPal); } Результат показан на рис. 13.3. На верхнем рисунке, использующем макросы RGB, цвета смешиваются из небольшого количества однородных цветов. GDI по- прежнему ограничивается 20 статическими цветами. На нижнем рисунке, использующем макрос PALETTERGB, все 216 цветов выводятся однородными. 1111: НИ ЕВР ИЯК111 тШ яя98 15Ш ЗШ? # 1BS1 1ШИЯ1 «Si ■КЛЯП! шшшшт тшшш ■■■! "МНЯ 1ШВ1 iffl ■Mi wmmi ■ШЯ1 Mia;; ■■■! ШШШшш ИМИ! та шшшь штт ESS ■■■1 ■■■I ■■111 ills ■■■■I ■■■■I Рис. 13.3. Вывод web-цветов с применением полутоновой палитры
708 Глава 13. Палитры Если флаг bForceBackground равен TRUE, а цвета полутоновой палитры отсутствуют в текущей системной палитре, системная палитра не изменяется. GDI всего лишь пытается аппроксимировать запросы ближайшими цветами, найденными в системной палитре. Как и в случае с системной палитрой, GDI позволяет приложениям получить информацию о содержимом существующей логической палитры. Для этого используется функция GetPaletteEntries, которая, как и GetSystemPaletteEntries, возвращает массив структур PALETTEENTRY. Полутоновая палитра состоит из 256 цветов. 216 из них входят в семейство web-цветов (цвета с составляющими RGB, кратными 51, включая 6 оттенков серого). Полутоновая палитра содержит еще 25 оттенков серого, поэтому с ее помощью можно представить 31 уровень серого. Оставшиеся 13 цветов относятся к статическим цветам, используемым цветовыми схемами. Создание специализированной палитры Возможности приложения не ограничиваются использованием палитры по умолчанию и полутоновой палитры. Приложение может создать собственный вариант палитры функцией CreatePalette. Функция CreatePalette возвращает манипулятор логической палитры, которая затем выбирается в контексте устройства и реализуется по аналогии с логической палитрой. Функция CreatePalette получает указатель на структуру LOGPALETTE, содержащую номер версии, количество элементов и массив структур PALETTEENTRY переменного размера. Номер версии палитры не изменился со времен его появления в Windows 3.0 и по-прежнему равен 0x0300. Количество элементов в структуре LOGPALETTE может изменяться в очень широких пределах. Если вы создаете палитру для монохромного DIB-растра, достаточно всего двух цветов, а для DIB с 8-разрядным цветом требуется 256 цветов. В системах семейства Windows NT количество элементов ограничивается 1024. Каждый цвет палитры описывается структурой PALETTEENTRY. Первые три поля структуры обычно описывают интенсивность компонентов RGB, а поле peFlags указывает, как данный элемент должен интерпретироваться при реализации палитры. Четыре допустимых значения поля peFlags перечислены в табл. 13.2. Таблица 13.2. Поле peFlags структуры PALETTEENTRY Значение Описание 0 Стандартная процедура. Искать цвет RGB в системной палитре. Если цвет отсутствует, включить его в палитру PCRESERVED Зарезервировать в системной палитре одну позицию, которая может использоваться для анимации. Не сопоставлять другие цвета с зарезервированной позицией PXEXPLICIT Не изменять системную палитру. Первые два байта структуры PALETTEENTRY образуют индекс в системной палитре PCN0C0LLAPSE Искать соответствие в системной палитре лишь при отсутствии свободных элементов; в противном случае использовать новый элемент
Логическая палитра 709 Как говорилось выше, системная палитра содержит 20 статических цветов, которые не могут заменяться приложением. Следовательно, если приложение захочет реализовать логическую палитру из 256 элементов, вполне возможно, что некоторые цвета не будут включены в палитру. GDI обрабатывает запрос в порядке следования элементов в структуре LOGPALETTE. Важные цвета следует размещать в первых позициях структуры LOGPALETTE. В следующем фрагменте создается 256-цветная палитра оттенков серого цвета без каких-либо специальных требований. Если выбрать и реализовать такую палитру, когда система исцользует 20 статических цветов, 16 цветов в конце таблицы не удастся реализовать однородными цветами. Если, например, «медицинское» приложение захочет вывести рентгеновский снимок в 256 оттенках серого, оно должно уменьшить количество статических цветов до двух (черный и белый) вызовом SetSystemPalettellseChDC, SYSPALNOSTATIC). После этого весь пользовательский интерфейс Windows будет выводиться в оттенках серого. Чтобы содержимое экрана нормально воспринималось, приложение должно позаботиться о правильной настройке системных цветов. HPALETTE CreateGraysea 1еРа1ette(voi d) { LOGPALETTE * pLogPal = (LOGPALETTE *) new BYTE[sizeof(LOGPALETTE) + 255 * sizeof(PALETTEENTRY)]; pLogPal->palVersion = 0x0300; pLogPal->palNumEntries = 256; for (int i=0; i<256; i++) { PALETTEENTRY entry - { i. i. i. 0 }: pLogPal->palPalEntry[i] = entry; HPALETTE hPal = CreatePalette(pLogPal); delete [] (BYTE *) pLogPal; return hPal; } Логическая палитра с флагом PCEXPLICIT — случай довольно интересный. Она предназначена не для изменения системной палитры, а для того, чтобы цвета системной палитры могли использоваться в качестве индексов логической палитры. При выборе и реализации логической палитры с флагом PCEXPLICIT цвета, определяемые макросом PALETTE INDEX, ассоциируются с индексами системной палитры, заданными в структуре LOGPALETTE; даже макрос PALETTERGB работает аналогично PALETTEINDEX. После создания палитры можно увеличить или уменьшить количество цветов в ней при помощи функции ResizePalette. При уменьшении размера палитры удаляемые элементы использоваться не могут, но остальные элементы остаются без изменений. При увеличении размера палитры новые элементы заполняются черным цветом. Для инициализации новых элементов применяется функция SetPaletteEntries.
710 Глава 13. Палитры Сообщения палитры Когда окно реализует основную логическую палитру, из системной палитры удаляются нестатические цвета, а на их место записываются новые цвета из логической палитры. Если на экране остается окно приложения, использовавшего нестатические цвета старой системной палитры, изображение в нем сильно искажается. Например, красный цвет может превратиться в зеленый, а зеленый становится желтым. Чтобы системная палитра могла нормально использоваться сразу несколькими окнами, Windows рассылает окнам верхнего уровня сообщения, информирующие о важных изменениях в палитре. WM_QUERYNEWPALETTE Пока окно находится в неактивном состоянии, другие окна могут изменить содержимое системной палитры, что приводит к искажению изображения. Если окно готово к получению фокуса клавиатуры, Windows отправляет ему сообщение WMQUERYNEWPALETTE, чтобы окно могло восстановить свой нормальный вид. Если окно использует нестандартную палитру, оно должно реализовать ее как основную и перерисовать все окно, чтобы восстановить его в оптимальном виде. Палитра, задействованная приложением, должна быть создана заранее и храниться в переменной класса окна или в глобальной переменной. Следующая функция показывает, как обрабатывается сообщение WM_QUERYNEWPALETTE. LRESULT KWindow::OnQueryNewPalette(void) { if ( mJiPa1ette-»NULL ) return FALSE; HDC hDC = GetDC(mJiWnd); HPALETTE h01d= SelectPalette(hDC, mJiPalette. FALSE); BOOL changed - RealizePalette(hDC) !- 0; SelectPalette(hDC, hOld, FALSE): ReleaseDC(m_hWnd. hDC); if ( changed ) { InvalidateRect(m_hWnd. NULL. TRUE); // Перерисовать } return changed; } В наш класс окна верхнего уровня добавляется новая переменная mJiPalette, равная NULL, если только производное окно не захочет использовать палитру. При работе в режимах High Color и True Color, а также в том случае, если вы ограничиваетесь статическими цветами, переменная m_hPa1 ette остается равной NULL. Получив сообщение WMQUERYNEWPALETTE, функция окна вызывает Kwindow: :0nQuery- NewPalette или переопределенную функцию. Метод OnQueryNewPalette создает новый манипулятор контекста устройства, выбирает палитру в качестве основной и реализует ее. Если реализация палитры прошла успешно (это означает,
Сообщения палитры 711 что устройство поддерживает палитру), клиентская область окна объявляется недействительной, что обеспечивает ее перерисовку правильными цветами. Функция возвращает TRUE, если палитра была реализована, и FALSE в противном случае. WM_PALETTEISCHANGING Непосредственно перед тем, как приложение реализует свою логическую палитру, Windows рассылает окнам верхнего уровня сообщение WMPALETTE ISCHANGING, сообщая тем самым о предстоящих изменениях в системной палитре. Впрочем, это вовсе не означает, что реализация палитры откладывается в ожидании подтверждения. Когда активное окно реализует свою палитру, из-за изменений в системной палитре окна на заднем плане могут сильно исказиться. Предполагается, что сообщение WMPALETTE ISCHANGING позволяет окнам заднего плана подготовиться к изменениям в системной палитре. Например, приложение может просто стереть свое окно одним из статических цветов, чтобы изображение не менялось при модификации палитры, а затем перерисовать его снова с применением фоновой палитры. В одной из статей MSDN сказано, что сообщение WM_P ALETEI SCHANG I NG является пережитком устаревшей архитектуры, и его следует просто игнорировать. Эксперименты показали, что в Windows 2000 это сообщение не рассылается. Даже в профессиональных пакетах переключение палитры сопровождается кратковременным искажением цветов. WM_PALETTECHANGED Изменение системной палитры может сопровождаться полным искажением цветов во всех окнах, кроме активного, поэтому всем перекрывающимся (overlapped) и всплывающим (popup) окнам в системе рассылается сообщение WM_PALETTECHANGED. Окна должны отреагировать на это сообщение и попытаться по мере возможности восстановить свое изображение. Параметр wParam сообщения WMPALETTECHANGED содержит манипулятор окна, изменившего системную палитру. Окно, обрабатывающее это сообщение, должно проверить этот манипулятор и убедиться в том, что палитра была изменена не им самим, а каким-то другим окном, поскольку в противном случае ничего делать не нужно. Существует два способа восстановить содержимое окна. Первый, более быстрый способ — реализовать свою логическую палитру как фоновую и вызвать функцию UpdateColors GDI, чтобы улучшить изображение на уровне пикселов. BOOL UpdateColors(hDC); Функция UpdateColors перебирает все пикселы поверхности устройства и отображает их цветовые индексы исходной системной палитры на наиболее подходящие индексы новой системной палитры. Вероятно, во внутренней реализации UpdateColors строит таблицу отображения старой системной палитры на новую, а затем перебирает пикселы и осуществляет замену по таблице.
712 Глава 13. Палитры Поскольку UpdateColors работает с кадровым буфером устройства, содержащим приближенное представление рисунка, многократное применение UpdateColors приведет к постепенному ухудшению изображения. Например, если исходный рисунок отображался в цвете, то после того, как приложение переключается на палитру оттенков серого, UpdateColors отображает все пикселы в оттенках серого. Но когда другое окно реализует полутоновую палитру, функция UpdateColors не может нормально восстановить цветное изображение по оттенкам серого. Второй способ обработки сообщения WMPALETTECHANGED заключается в перерисовке окна с реализацией фоновой палитры. Как было сказано выше, фоновая палитра не удаляет из системной палитры ни одного элемента, а лишь пытается использовать свободные элементы и подогнать свои логические цвета под существующий набор. Если новая системная палитра хорошо сбалансирована, можно добиться вполне приличного качества. Ниже приведен пример обработчика сообщения WM_PALETTECHANGED. Мы проверяем, не были ли изменения в палитре внесены текущим окном, для чего сравниваем манипулятор окна с wParam. Если манипуляторы не совпадают и окно использует палитру, эта палитра выбирается и реализуется. Программа подсчитывает, сколько раз была вызвана функция UpdateColors. При небольшом значении счетчика вызывается функция UpdateColors, обеспечивающая ускоренное обновление; в противн&м случае окно перерисовывается заново, чтобы улучшить качество изображения. LRESULT KWindow::OnPaletteChanged(HWND hWnd. WPARAM wParam) { if ( ( hWnd != (HWND) wParam ) && mJiPalette ) { HDC hDC = GetDC(hWnd); HPALETTE hOld = SelectPaletteChDC. mJiPalette, FALSE); if ( RealizePalette(hDC) ) if ( m_nUpdateCount >=2 ) { InvalidateRect(hWnd. NULL. TRUE); m_nUpdateCount = 0; } else { UpdateColors(hDC); m_nUpdateCount ++; } SelectPalette(hDC, hOld, FALSE); ReleaseDCChWnd, hDC); } return 0; } Тестовая программа Давайте объединим все сказанное в небольшом классе окна, предназначенного для вывода DIB с помощью полутоновой палитры. Класс KDIBWindow показывает,
Сообщения палитры 713 как создать логическую палитру, реализовать ее и использовать для отображения растра и как организовать обработку сообщений палитры с помощью описанных выше функций. В листинге 13.2 приведен полный код класса окна DIB, производного от KWindow. Метод CreateDIBWindow, получающий среди прочих параметров неупакованный DIB-растр, создает временное окно. Параметр option позволяет сравнить работу программы с палитрой и без нее, при обработке сообщений палитры и при блиттинге с масштабированием. Обработчик сообщения WMCREATE создает полутоновую палитру, если на это указывает значение параметра option. Обработчик WMPAINT использует палитру для вывода растра. Обработчик WM_PALETTECHANGED восстанавливает поврежденное изображение, также руководствуясь значением параметра option. Обработчик WM_QUERYNEWPALETTE реализует полутоновую палитру. Созданная палитра уничтожается в обработчике сообщения WM_NCDESTROY. Листинг 13.2. Вывод растров с учетом палитры typedef enum { pal_no = 0x00, // Без палитры paljialftone = 0x01. // Использовать полутоновую палитру pal_bitmap = 0x02, // Использовать палитру DIB/DIB-секции pal_react = 0x04, // Реагировать на сообщение WM_PALETTECHANGED pal_stretchHT= 0x08 // Использовать режим STRETCH_HALFTONE HPALETTE CreateDIBSectionPalette(HDC hDC. HBITMAP hDIBSec); class KDIBWindow : public KWindow { const BITMAPINFO * m_pBMI; const BYTE * m_pBits; int mjnOption; virtual LRESULT WndProcCHWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam) { switch ( uMsg ) { case WM_CREATE: m_hWnd = hWnd; { HDC hDC = GetDC(m_hWnd); if ( (mjiOption & 3)==pal_bitmap ) mJiPalette - CreateDIBPalette(m_pBMI); else if ( (mjnOption & 3)==pal_halftone ) mJiPalette = CreateHalftonePalette(hDC); else mJiPalette = NULL; ReleaseDC(m_hWnd. hDC); } return 0; Продолжение^
714 Глава 13. Палитры Листинг 13.2. Продолжение case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = BeginPaintChWnd. & ps); HPALETTE hOld - SelectPalette(hDC, mJiPalette. FALSE); RealizePalette(hDC); if ( mjiOption & pal_stretchHT ) { SetStretchBltMode(hDC. STRETCH_HALFTONE); } else SetStretchBltModeChDC, STRETCH_DELETESCANS); StretchDIBits(hDC. 10. 10. m_pBMI->bmi Header.biWi dth. m_pBMI->bmiHeader.bi Hei ght. 0. 0. m_pBMI->bmiHeader.biWidth. m_pBMI->bmi Header.bi Hei ght, mj)B1ts. m_pBMI. DIB_RGB_COLORS. SRCCOPY); EndPaintChWnd. & ps); } return 0; case WM_PALETTECHANGED: if ( mjiOption & pal__react ) return OnPaletteChanged(hWnd. wParam); break; case WM_QUERYNEWPALETTE: return OnQueryNewPalette(); case WMJCDESTROY: DeleteObject(m_hPalette); mJiPalette - NULL; return 0; } return DefWindowProc(hWnd. uMsg. wParam. IParam); } public: void CreateDIBWindow(HINSTANCE hinst. const BITMAPINFO * pBMI. const BYTE * pBits. int option) { if ( pBMI==NULL ) return; mjiOption - option; m_pBMI - pBMI; m_pBits = pBits:
Сообщения палитры 715 TCHAR title[32]: wsprintf(title. _T("DIB Window Ud)"). mjiOption): CreateEx(0. _T("DIBWindow"), title, WSJ3VERLAPPEDWIND0W | WS_CLIPCHILDREN. CW_USEDEFAULT. CWJJSEDEFAULT. m_pBMI->bmiHeader.biWidth + 28, m_pBMI->bmiHeader.biHeight + 48. NULL. NULL, hlnst); ShowWindow(SW_NORMAL); UpdateWindowO; } }: На рис. 13.4 показаны два варианта изображения. В левом окне изображение получено без применения полутоновой палитры (option - paljw). Цветные пикселы изображения заменяются 20 статическими цветами, поэтому изображение получается серым и скучным. В правом окне была использована полутоновая палитра (option = pal _half tone); рисунок стал очень красочным, с плавными переходами цветов. Хотя на страницах книги цвета не различаются, о качестве изображения можно судить даже по оттенкам серого. Рис. 13.4. Вывод DIB с полутоновой палитрой и без нее Рисунок 13.5 иллюстрирует последствия обработки сообщений палитры. Левое окно (option = pal_halftone) игнорирует сообщение WM_PALETTECHANGED, упуская шанс восстановить изображение при модификации системной палитры. Правое окно (option = palhalftone| palreact) обновляет цвета или перерисовывает изображение с фоновой палитрой. Вероятно, комментарии излишни. На экране очевидны некоторые различия, обусловленные различиями фоновой и основной палитр, хотя на бумаге рисунки снова выглядят почти одинаково. Если у окна верхнего уровня имеются дочерние окна (особенно в приложениях MDI), вы должны правильно организовать пересылку сообщений палитры, поскольку без этого сообщения не дойдут до дочерних окон.
716 Глава 13. Палитры Рис. 13.5. Вывод DIB без обработки WM_PALETTECHANGED Сообщение WMQUERYNEWPALETTE посылается окну верхнего уровня лишь при получении им фокуса. Главное окно MDI должно пересылать это сообщение активному дочернему окну MDI. Если дочерние окна MDI используют разные палитры, любое дочернее окно при получении фокуса должно иметь возможность реализовать свою палитру в качестве основной. Сообщение WM_PALETTECHANGED тоже рассылается только окнам верхнего уровня. Главное окно MDI должно переслать его всем своим дочерним окнам, чтобы все они имели возможность отреагировать на изменения системной палитры. Палитра и растры По сравнению с векторной графикой, использующей перья и кисти, растровая графика порождает больше проблем в видеорежимах с палитрой. Например, обычная загрузка растра функцией LoadBitmap уже не подходит, поскольку изображение, которое может содержать тысячи цветов, будет аппроксимироваться несколькими статическими цветами. Если растр содержит цветовую таблицу, для получения оптимального результата ее следует преобразовать в логическую палитру Windows и правильно использовать. При выводе растров High Color и True Color в 256-цветном режиме приемлемый результат достигается только построением оптимальной палитры и полутоновой обработкой изображения в соответствии с содержимым палитры. В этом разделе рассматриваются стандартные проблемы, возникающие при выводе растров в видеорежимах с палитрой.
Палитра и растры 717 Аппаратно-зависимые растры и палитры Самый простой способ преобразования растра формата BMP в DDB-растр основан на применении функции LoadBitmap или Loadlmage. Функция LoadBitmap преобразует BMP-файл, подключенный к EXE/DLL в виде ресурса, в DDB. Функция Loadlmage преобразует в DDB либо растровый ресурс, либо внешний ВМР-файл, хотя Loadlmage также позволяет загрузить изображение в DIB-секцию. Среди параметров этих функций не передается ни контекст устройства, ни логическая палитра. При построении DDB функции LoadBitmap и Loadlmage используют только 20 статических цветов. Все цветные пикселы растра заменяются ближайшим подходящим цветом из этого маленького набора. Пример показан на рис. 13.4 слева. Для построения многоцветного DDB-растра необходима логическая палитра, управляющая преобразованием цветов из DIB в DDB. Палитра может быть полутоновой, специализированной или сгенерированной на базе системной палитры. В листинге 13.3 приведена новая функция загрузки растра с поддержкой палитры. Листинг 13.3. Загрузка DDB-растра с поддержкой палитры static BYTE * GetDIBPixelArrayCBITMAPINFO * pDIB) { return (BYTE *) & pDIB->bm1Colors[GetDIBColorCount(pDIB->bmiHeader)]: } // Создание логической палитры, содержащей все цвета // текущей системной палитры HPALETTE CreateSystemPalette(void) { L0GPALETTE * pLogPal = (LOGPALETTE *) new char[sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * 255]; pLogPal->palVersion - 0x300; pl_ogPal->palNumEntries «• 256; HDC hDC - GetDC(NULL); GetSystemPaletteEntries(hDC, 0. 256. pLogPal->pa1Pal Entry); ReleaseDCCNULL, hDC); HPALETTE hPal - CreatePalette(pLogPal); delete [] (char *) pLogPal; return hPal; } // Загрузка DIB из ресурса или из файла BITMAPINFO * LoadDIB(HINSTANCE hlnst. LPCTSTR pBitmapName. bool & bNeedFree) { HRSRC hRes - FindResource(hInst, pBitmapName. RT_BITMAP); BITMAPINFO * pDIB; Продолжение #
718 Глава 13. Палитры Листинг 13.3. Продолжение if ( hRes ) { HGLOBAL hGlobal = LoadResource(hInst, hRes); pDIB - (BITMAPINFO *) LockResource(hGlobal); bNeedFree - false; } else { HANDLE handle - CreateFile(pBitmapName. GENERIC_READ. FILE_SHARE_READ. NULL. OPENJXISTING. FILE_ATTRIBUTE_NORMAL. NULL); if ( handle — INVALIDJANDLEJALUE ) return NULL: BITMAPFILEHEADER bmFH; DWORD dwRead * 0; ReadFileChandle. & bmFH. sizeof(bmFH). & dwRead. NULL); if ( (bmFH.bfType — 0x4D42) && (bmFH.bfSize<= GetFileSize(handle. NULL)) ) { pDIB = (BITMAPINFO *) new BYTE[bmFH.bfSize]; if ( pDIB ) { bNeedFree = true; ReadFileChandle. pDIB. bmFH.bfSize. & dwRead. NULL); } } CloseHandle(handle); } return pDIB; } // Загрузка ресурса или файла под управлением палитры HBITMAP PaletteLoadBitmap(HINSTANCE hlnst. LPCTSTR pBitmapName. HPALETTE hPalette) { bool bDIBNeedFree; BITMAPINFO * pDIB - LoadDIB(hInst. pBitmapName. bDIBNeedFree); int width - pDIB->bmiHeader.biWidth; int height - pDIB->bmiHeader.biHeight; HDC hMemDC - CreateCompatibleDC(NULL): HBITMAP hBmp - CreateBitmap(width. height. GetDeviceCaps(hMemDC. PLANES). GetDeviceCaps(hMemDC. BITSPIXEL). NULL); HGDIOBJ hOldBmp - SelectObject(hMemDC. hBmp);
Палитра и растры 719 HPALETTE hOld = SelectPaletteChMemDC, hPalette. FALSE); RealizePalette(hMemDC); SetStretchBltModeChMemDC. HALFTONE); StretchDIBitsChMemDC. 0. 0, width, height, 0. 0. width, height. GetDIBPixelArray(pDIB), pDIB. DIB_RGB_C0L0RS. SRCCOPY); SelectPaletteChMemDC. hOld, FALSE): SelectObject(hMemDC. hOldBmp); DeleteObject(hMemDC); if ( bDIBNeedFree ) delete [] (BYTE *) pDIB; return hBmp; } По сравнению с LoadBitmap функция PaletteLoadBitmap получает дополнительный параметр — манипулятор логической палитры. Логическая палитра выбирается в совместимом контексте устройства перед преобразованием загруженного DIB-растра в DDB, поэтому сгенерированный DDB-растр может использовать все цвета логической палитры. Функция LoadDIB загружает растр из ресурса или внешнего файла в виде упакованного DIB-растра. Вспомогательная функция CreateSystemPalette создает логическую палитру, содержащую все цвета текущей системной палитры. Манипулятор, переданный PaletteLoadBitmap, должен соответствовать логической палитре, используемой при выводе растра. Например, если приложение является игровой программой, работающей с полутоновой палитрой, то растры в игре должны загружаться с полутоновой палитрой. Главное окно программы должно обрабатывать сообщения палитры, чтобы обеспечить выбор полутоновой палитры при выводе растров. DDB-растры широко применяются при выводе графики на панелях инструментов, кнопках, элементах управления, в меню и т. д. Обычно вывод происходит под управлением операционной системы, хотя также возможен вариант с прорисовкой владельцем. Операционная система применяет при выводе DDB палитру по умолчанию, поэтому если приложение хочет использовать более 20 статических цветов, цвета растра должны соответствовать содержимому текущей системной палитры. Другими словами, при каждом изменении системной палитры эти растры приходится восстанавливать заново. Ниже приведена функция, позволяющая вывести панель инструментов более чем с 20 цветами. Она реализуется в классе KToolbarB, производном от класса KToolbar. Функция KToolbar: :SetBitmap должна вызываться при каждом изменении системной палитры. Она загружает растр с применением текущей системной палитры и использует сообщение TB_REPLACEBITMAP для замены текущего растра панели инструментов. Теперь вы сможете задействовать больше цветов на панелях инструментов в 256-цветном режиме. B00L KToolbarB::SetBitmap(HINSTANCE hlnstance. int resourcelD) { HPALETTE hPal - CreateSystemPaletteO:
720 Глава 13. Палитры HBITMAP hBmp - PaletteLoadBitmapChlnstance. MAKEINTRESOURCE(resourcelD). hPal); DeleteObject(hPal): if ( hBmp ) { TBREPLACEBITMAP rp; rp.hlnstOld = m_ResInstance; rp.nlDOld = m_ResId: rp.hlnstNew - NULL; rp.nlDNew - (UINT) hBmp; rp.nButtons = 40; SendMessage(m_hWnd, TB_REPLACEBITMAP, 0, (LPARAM) & rp); if ( m_ResInstance==NULL ) DeleteObject( (HBITMAP) m_ResId): m_ResInstance = NULL; m_ResId - (UINT) hBmp; return TRUE; } else return FALSE; } Аппаратно-независимые растры и палитры В отличие от аппаратно-зависимых растров, каждый аппаратно-независимый растр (DIB) содержит полную цветовую информацию, что позволяет вывести его на любом устройства. В режимах High Color и True Color каждый пиксел содержит полные данные цвета; в других режимах индексы отображаются на значения RGB по цветовой таблице. Главная проблема при выводе DIB в системах с палитрой заключается в выборе палитры, используемой при выводе растра. Вывод DIB с палитрой по умолчанию позволяет использовать только 20 статических цветов. Полутоновая палитра хорошо подходит для вывода деловой графики с насыщенными и равномерно распределенными цветами. Для растров с неравномерным распределением цветов в пространстве RGB специализированная палитра подходит лучше, чем палитры общего назначения (такие, как полутоновая палитра). Если количество цветов в растре не превышает 256, цветовая таблица растра легко преобразуется в логическую палитру. Для растров High Color или True Color Windows позволяет задать цветовую таблицу для вывода на устройствах с палитрой (хотя вряд ли удастся вспомнить хоть одно приложение, которое бы пользовалось этой возможностью). В листинге 13.4 приведена функция для построения логической палитры на базе цветовой таблицы DIB.
Палитра и растры 721 Листинг 13.4. Преобразование цветов DIB в логическую палитру HPALETTE CreateDIBPalette(const BITMAPINFO * pDIB) { BYTE * pRGB; int nSize; int nColor; if ( pDIB->bmiHeader.biSize==sizeof(BITMAPCOREHEADER) ) // OS/2 { pRGB = (const BYTE *) pDIB + sizeof(BITMAPCOREHEADER); nSize = sizeof(RGBTRIPLE); nColor - 1 « ((BITMAPCOREHEADER *) pDIB)->bcBitCount; } else { nColor = 0; if ( pDIB->bmiHeader.biBitCount<=8 ) nColor = 1 « pDIB->bmiHeader.biBitCount; if ( pDIB->bmiHeader.biClrUsed ) nColor - pDIB->bmiHeader.biCIrUsed; if ( pDIB->bmiHeader.biClrImportant ) nColor - pDIB->bmiHeader.biCIrImportant; pRGB - (BYTE *) & pDIB->bmiColors; nSize - sizeof(RGBQUAD); if ( pDIB->bmiHeader.biCompression-=BI_BITFIELDS ) pRGB +- 3 * sizeof(RGBQUAD); } if ( nColor>256 ) nColor - 256; if ( nColor—0 ) return NULL; LOGPALETTE * pLogPal - (LOGPALETTE *) new BYTE[sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * (nColor-1)]: HPALETTE hPal; if ( pLogPal ) { pLogPal->palVersion - 0x0300; pLogPal->palNumEntries - nColor; for (int i-0: i<nColor; i++) { pLogPal->palPalEntry[i].peBlue - pRGB[0]: pLogPal->palPalEntry[i].peGreen - pRGB[l]; pLogPal->palPa!Entry[i].peRed - pRGB[2]; pLogPal ->pal Pal Entry [1 ]. peFl ags - 0; Продолжение^
722 Глава 13. Палитры Листинг 13.3. Продолжение pRGB +- nSize; } hPal = OeatePalette(pLogPal); } delete [] (BYTE *) pLogPal; return hPal; Рис. 13.6. Вывод DIB с полутоновой палитрой, с полутоновой палитрой в режиме HALFTONE и со специализированной палитрой
Палитра и растры 723 Функция ищет в DIB цветовую таблицу и определяет количество цветов, необходимых для вывода растра. Учитывая, что в нормальных условиях можно реализовать только 236 цветов, отличных от статических, в цветовой таблице DIB не рекомендуется использовать более 236 нестатических цветов. Поле bid rImportant предусмотрено специально для сокращения количества необходимых цветов. Цвета в таблице желательно отсортировать по частоте использования. Если некоторые из них не войдут в палитру, исключение должно начинаться с наименее используемых цветов. Функция CreateDIBPalette использует цветовую таблицу DIB только для построения логической палитры. Вопрос построения оптимальной палитры для изображений High Color и True Color рассматривается в следующем разделе, посвященном более общей теме — сокращению количества цветов в растре. А пока в том случае, если DIB не содержит цветовой таблицы, наша программа будет использовать полутоновую палитру. Эффект от заполнения палитры данными из цветовой таблицы DIB может быть очень заметным. Взгляните на рис. 13.6; первое изображение выведено с полутоновой палитрой без режима полутонового масштабирования (см. главу 10). Второе изображение выводилось с полутоновой палитрой и полутоновым масштабированием; качество рисунка заметно улучшилось. Последний рисунок был получен с применением специализированной палитры, построенной на основе цветовой таблицы, без полутонового масштабирования. Возможно, вас удивит то, что при использовании палитры, построенной на базе цветовой таблицы, режим полутонового масштабирования совершенно не улучшает качества изображения. Результат получается практически таким же, как при использовании полутоновой палитры с включением полутонового масштабирования. Индекс палитры в цветовой таблице DIB При выводе DIB таким функциям, как StretchDIBits, обычно передается флаг DIB_RGB_C0L0RS. Этот флаг сообщает GDI, что цветовая таблица DIB действительно содержит значения RGB. GDI ассоциирует значения RGB из цветовой таблицы с цветами логической палитры, а затем преобразует индексы логической палитры в индексы системной палитры, записываемые в кадровый буфер. Поиск подходящих цветов в палитре проходит довольно медленно. В GDI предусмотрены две функции, позволяющие приложениям самостоятельно подбирать цвета: UINT GetNearestPalettelndexCHPALETTE hPal. C0L0RREF crColor); C0L0RREF GetNearestColor(HDC hDC, C0L0RREF crColor); Функция GetNearestPalettelndex просматривает все цвета логической палитры в поисках ближайшего совпадения для заданного эталона. Степень близости определяется расстоянием между двумя цветами в цветовом пространстве RGB. Для двух цветов RGB(rl,gl,bl) и RGB(r2,g2,b2) расстояние вычисляется по формуле
724 Глава 13. Палитры С целью нахождения ближайшего совпадения GDI может просто использовать квадрат расстояния, чтобы обойтись без медленного вычисления квадратного корня. Функция GetNearestColor находит для заданного эталона ближайший цвет из системной палитры и возвращает его. Конечно, GDI не подбирает цвета для каждого пиксела. При выводе DIB с флагом DIBRGBCOLORS GDI подбирает замену для всех цветов цветовой таблицы и использует результат для вывода всех пикселов растра. Если текущая логическая палитра построена на базе цветовой таблицы растра, GDI позволяет исключить первый этап поиска. Чтобы воспользоваться этой оптимизацией, приложение должно заменить значения RGB в цветовой таблице DIB индексами логической палитры, а затем при использовании DIB передать флаг DIBPALC0L0RS вместо DIBRGBCOLORS. С флагом DIB_PAL_C0L0RS цветовая таблица DIB интерпретируется как массив индексов логической палитры. Следующая функция создает структуру BITMAPINFO с цветовой таблицей, содержащей индексы палитры. BITMAPINFO * IndexColorTableCBITMAPINFO * pDIB. HPALETTE hPal) { int nSize; int nColor; const BYTE * pRGB - GetColorTable(pDIB, nSize, nColor); if ( pDIB->bmiHeader.biBitCount>8 )// Без изменений return pDIB; // Создать новую структуру BITMAPINFO для модификации BITMAPINFO * pNew - (BITMAPINFO *) new BYTE[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*nColor]; pNew->bmiHeader = pDIB->bmiHeader; WORD * plndex - (WORD *) pNew->bmiColors; for (int i=0; i<nColor; i++. pRGB+=nSize) if ( hPal ) plndex[i] - GetNearestPalettelndexChPal. RGB(pRGB[2]. pRGB[l], pRGB[0])): else plndex[i] - i; return pNew; } Функция получает указатель на BITMAPINFO и логическую палитру. Она создает новую структуру BITMAPINFO, копирует данные формата и размеров, после чего строит цветовую таблицу с индексами палитры. Если манипулятор логической палитры не задан, предполагается, что растр будет выводиться с логической палитрой, созданной на базе цветовой таблицы, поэтому мы просто отображаем элементы цветовой таблицы растра в соответствующие позиции новой таблицы. Если логическая палитра задана, в ней ищутся элементы, ближайшие к значени-
Палитра и растры 725 ям RGB исходной цветовой таблицы. Функция оставляет исходную цветовую таблицу без изменений, создавая в памяти новую структуру BITMAPINFO; она может использоваться для обработки DIB-растров, загруженных из ресурсных файлов и доступных только для чтения. В этом случае вызывающая сторона должна проверить, успешно ли завершилось создание новой структуры BITMAPINFO, и освободить структуру после завершения работы с ней. DIB-секции и палитра При создании DIB-секции функциями CreateDIBSection или Loadlmage возвращается манипулятор объекта DIB-секции. Если для DIB нам всегда известен указатель на структуру BITMAPINFO, по которому можно найти цветовую таблицу, процесс поиска цветовой таблицы DIB-секции по ее манипулятору не столь очевиден. Получить доступ к цветовой таблице можно лишь одним способом — выбрать DIB-секцию в совместимом контексте устройства и воспользоваться следующими двумя функциями: UINT GetDIBColorTable(HDC hDC. UINT uStartlndex. UINT cEntries. RGBQUAD * pColors); UINT SetDIBColorTable(HDC hDC, UINT uStartlndex. UINT cEntries. RGBQUAD * pColors); Функция GetDIBColorTable копирует цветовую таблицу DIB-секции в заданный массив RGBQUAD. Функция SetDIBColorTable решает противоположную задачу — она загружает цветовую таблицу из пользовательского массива RGBQUAD. Трудно понять, почему вместо манипуляторов контекстов устройств этим двум функциям не передаются манипуляторы DIB-секций. Следующий фрагмент показывает, как построить логическую палитру после получения цветовой таблицы. HPALETTE CreateDIBSectionPaletteCHDC hDC. HBITMAP hDIBSec) { HDC hMemDC - CreateCompatibleDC(hDC); HGDIOBJ hOld = SelectObject(hMemDC. hDIBSec); RGBQUAD Col or[256]: int nEntries - GetDIBColorTable(hMemDC. 0. 256. Color); HPALETTE hPal - LUTCreatePaletteUBYTE *) Color, sizeof(RGBQUAD). nEntries); SelectObjectChMemDC. hOld); DeleteObjectChMemDC); return hPal; } Если для создания DIB использовалась функция CreateDIBSection, приложение не располагает действительной структурой BITMAPINFO, которая могла бы быть задействована для построения логической палитры.
726 Глава 13. Палитры Квантование цветов Информационные заголовки растров в режимах High Color и True Color обычно не содержат цветовых таблиц. До настоящего момента мы использовали для их вывода полутоновую палитру, которая редко обеспечивает оптимальное качество изображения. Процесс построения оптимальной палитры по цветному изображению называется квантованием цветов (color quantization). Квантование представляет собой процесс построения ограниченного набора цветов, с которыми результат вывода оказывается наиболее близким к исходному изображению. Если количество цветов в наборе не превышает 2N, каждый пиксел исходного изображения представляется N битами информации. Таким образом, квантование цветов также представляет собой методику сжатия изображений, приводящую к уменьшению их размеров (часто — с потерей данных). Например, файловый формат GIF поддерживает не более 256 цветов. Изображения High Color и True Color приходится конвертировать в 8-разрядный формат с использованием оптимальной палитры. В настоящее время в области квантования цветов ведутся активные исследования, поэтому существует множество разных алгоритмов, но нет единого оптимального решения. М. Герваутц (М. Gervautz) и В. Пургатхофер (W. Purgathofer) из Австрии в 1988 году опубликовали доклад о квантовании цветов с применением октантных деревьев. Этот простой, обеспечивающий отменное качество способ построения палитры получил широкое распространение. Алгоритм квантования с применением октантных деревьев состоит из трех этапов. На первом этапе строится дерево для сбора информации о распределении цветов в изображении. На втором этапе дерево оптимизируется объединением мелких узлов в более крупные, пока количество узлов не станет ниже отведенного предела. На последнем этапе цветовая таблица строится перебором узлов дерева. Корень дерева представляет все цветовое пространство RGB; для наших целей это означает совокупность точек RGB(r,g,b), где г, g и b лежат в интервале [0...255]. Корневой узел имеет 8 потомков, каждый из которых представляет 1/8 цветового пространства RGB. Деление осуществляется разбиением плоскостей R, G и В на две равные половины. На рис. 13.7 продемонстрировано деление корневого узла на 8 подузлов. Решение принимается на основании первого бита компонентов RGB, Все пикселы, у которых старшие биты составляющих равны 0, относятся к первому подузлу и обозначаются пометкой «OR, OG, OB», где R, G и В ограничиваются 7 битами. Все пикселы, у которых старшие биты RGB равны 1, относятся к последнему подузлу «1R, 1G, 1В». Деление узлов дерева продолжается до тех пор, пока не будет достигнут девятый уровень. Узлы второго уровня, находящиеся непосредственно под корнем, делятся по второму биту составляющих RGB; узлы третьего уровня делятся по третьему биту и т. д. Представление 24-разрядного пространства RGB октантным деревом теоретически связано с огромными затратами памяти. Дерево содержит 1 корень, 8 узлов второго уровня, 64 узла третьего уровня и 16,7 миллиона узлов девятого уровня. Полное октантное дерево состоит из 19 173 961 узла. Если каждый узел представляется 50 байтами, для хранения дерева потребуется 914 Мбайт памяти
Квантование цветов 727 (точнее — места на жестком диске). Фокус заключается в том, чтобы увеличивать дерево только в случае необходимости, и сокращать его при нехватке памяти. Собственно, именно по этой причине мы и используем дерево; работать с громадным массивом было бы гораздо проще. Синий со 0R,0G,0B 1R,0G,0B 0R,1G,0B xi: 1R,1G,0B 0R,0G,1B 1R,0G,1B 1R,1G,1B ч: Рис. 13.7. Представление цветового пространства RGB октантным деревом Помимо ссылок, образующих структуру дерева, каждый узел содержит информацию о представляемых им пикселах. Новый листовой узел, включаемый в дерево, представляет один пиксел. Со временем в него могут добавляться другие пикселы с такими же составляющими RGB. При нехватке памяти или сокращении дерева для построения палитры узлы могут укрупняться, поэтому один узел может представлять несколько пикселов с разными составляющими RGB. Класс KNode приведен в листинге 13.5. Листинг 13.5. Класс KNode для представления узлов октантного дерева class KNode { public: bool KNode * IsLeaf; Child[8]; unsigned Pixels; unsigned SigmaRed; unsigned SigmaGreen; unsigned SigmaBlue; KNode(bool leaf) { IsLeaf = leaf; Pixels =0; SigmaRed = 0; SigmaGreen = 0; Продолжение &
728 Глава 13. Палитры Листинг 13.5. Продолжение SigmaBlue = 0; memsetCChild. 0. sizeof(ChiId)): } RemoveAll(void); int PickLeaves(RGBQUAD * pEntry. int * pFreq, int size); }: KNode::RemoveAll(void) { for (int i»0; i<8; i++) if ( Child[i] ) { Child[i]->RemoveA110: Child[i] - NULL; } delete this; } int KNode::PickLeaves(RGBQUAD * pEntry, int * pFreq, int size) { if ( size==0 ) return 0; if ( IsLeaf ) { * pFreq pEntry->rgbRed pEntry->rgbGreen pEntry->rgbBlue pEntry->rgbReserved return 1: } else { int sum = 0; for (int i=0; i<8; i++) if ( ChildCi] ) sum += Child[i]->PickLeaves(pEntry+sum, pFreq+sum, size-sum); return sum; } } Переменная IsLeaf указывает, является ли узел листовым. Листовой узел определяется как узел, не имеющий потомков. В исходном состоянии дерева все листовые узлы находятся на девятом уровне. В процессе слияния узлы более высокого уровня тоже могут стать листовыми. Массив Child содержит 8 указателей на 8 потомков узла, не являющегося листовым. В остальных переменных = Pixels; = ( SigmaRed + Pixels/2 ) / Pixels = ( SigmaGreen + Pixels/2 ) / Pixels = ( SigmaBlue + Pixels/2 ) / Pixels - 0;
Квантование цветов 729 хранится количество пикселов и суммы их компонентов RGB для всех пикселов поддерева, корнем которого является текущий узел. Например, переменная Pixels корневого узла содержит общее количество пикселов во всем дереве. Следует учитывать, что сумма хранится в 32-разрядном целом без знака, поэтому октант- ное дерево позволяет хранить не более 224 пикселов. Конструктор класса KNode устроен очень просто — он ограничивается инициализацией переменных класса. Метод RemoveAl 1 удаляет все узлы текущего поддерева, используя обычную рекурсию. Метод PickLeaves собирает итоговую информацию, накопленную в дереве. Он заполняет массив структур PALETTEENTRY значениями RGB и заносит в целочисленный массив сведения о распределении цветов. Для этого мы просто перебираем узлы дерева и преобразуем каждый листовой узел в структуру PALETTEENTRY, значение RGB которой вычисляется усреднением значений RGB всех пикселов. Количество пикселов, представляемых каждым узлом, также сохраняется в массиве частот. Эта дополнительная величина может использоваться для сортировки массива PALETTEENTRY по частоте цветов. Класс октантного дерева KOctree приведен в листинге 13.6. Листинг 13.6. Класс октантного дерева, используемого для квантования цветов class KOctree { typedef enum { MAXMODE = 65536 }; KNode * pRoot; int Total Node; int TotalLeaf; void ReduceCKNode * pTree. unsigned threshold); public: KOctreeО { pRoot = new KNode(false): Total Node = 1; TotalLeaf = 0; } -KOctreeО { if ( pRoot ) { pRoot->RemoveA110; pRoot = NULL; } } void AddColor (BYTE r. BYTE g. BYTE b); void ReduceLeaves(int limit); int GenPalette(RGBQUAD *entry, int * Freq, int size); Продолжение^
730 Глава 13. Палитры Листинг 13.6. Продолжение void Merge(KNode * pNode, KNode & target); }: void KOctree::AddColor (BYTE r. BYTE g, BYTE b) { KNode * pNode = pRoot; for (BYTE mask=0x80; mask!=0; mask»=l) // Следовать до листового узла { // Добавить пиксел pNode->Pixels ++; pNode->SigmaRed += r; pNode->SigmaGreen += g; pNode->SigmaBlue +- b; if ( pNode->IsLeaf ) break; // Взять по одному биту от каждой составляющей // для формирования индекса int index = ( (г & mask) ? 4 : 0 ) + ( (g & mask) ? 2 : 0 ) + ( (b & mask) ? 1 : 0 ): // Создать новый узел, если это новая ветвь if ( pNode->Child[index]==NULL ) { pNode->Child[index] = new KNode(mask==2); Total Node ++; if ( mask==2 ) Total Leaf ++; } // Следовать дальше pNode - pNode->Child[index]; for (int threshold^; TotalNode>MAXMODE; threshold** ) Reduce(pRoot, threshold); // Объединить узел с листовыми узлами-потомками // и количеством пикселов, не превышающим threshold // Объединить листовой узел с количеством пикселов, // не превышающим threshold, с ближайшим соседом void KOctree::Reduce(KNode * pTree. unsigned threshold) { if ( pTree==NULL ) return; bool childallleaf = true;
Квантование цветов 731 // Рекурсивно вызвать для всех не-листовых потомков for (int i=0; i<8; i++) if ( pTree->Child[i] && ! pTree->Child[i]->IsLeaf ) { Reduce(pTree->Child[i], threshold); if ( ! pTree->Child[i]->IsLeaf ) childallleaf = false; } // Если все потомки являются листовыми узлами, // а количество пикселов не превышает порогового - объединить if ( childallleaf & (pTree->Pixels<=threshold) ) { for (int i=0; i<8; i++) if ( pTree->Child[i] ) { delete pTree->Child[i]; pTree->Child[i] = NULL; Total Node --; Total Leaf --; } pTree->IsLeaf = true; Total Leaf ++; return; } // Объединить листовых потомков // с небольшим количеством пикселов for (i-0: i<8; i++) if ( pTree->Child[i] && pTree->Child[i]->IsLeaf && (pTree->Child[i]->Pixels<=threshold) ) { KNode temp = * pTree->Child[i]; delete pTree->Child[i]; pTree->Child[i] - NULL; Total Node --; Total Leaf --; for (int j=0; j<8; j++) if ( pTree->Child[j] ) { Merge(pTree->Child[j], temp); break; } } } void KOctree;:Merge(KNode * pNode, KNode & target) { while ( true ) i Продолжение х£
732 Глава 13. Палитры Листинг 13.6. Продолжение pNode->Pixels += target.Pixels; pNode->SigmaRed += target.SigmaRed; pNode->SigmaGreen += target.SigmaGreen; pNode->SigmaBlue += target.SigmaBlue; if ( pNode->IsLeaf ) break; KNode * pChild = NULL; for (int i=0; i<8; i++) if ( pNode->Child[i] ) { pChild - pNode->Child[i]; break; } if ( pChild==NULL ) { assert(FALSE); return; } else pNode = pChild; } } void KOctree;;ReduceLeaves(int limit) { for (unsigned thresholds; TotalLeaf>limit; threshold**) Reduce(pRoot, threshold); } int KOctree;:GenPalette(RGBQUAD entry[], int * pFreq. int size) { ReduceLeaves(size); return pRoot->PickLeaves(entry, pFreq, size); } Переменные класса KOctree весьма просты. Переменная pRoot ссылается на корневой узел, от которого ссылки ведут ко всем остальным узлам. Общее количество узлов и листовых узлов в дереве хранится в переменных Total Node и Total Leaf. В начальном состоянии дерево состоит из корневого узла, созданного в конструкторе. Удаление всех узлов производится в деструкторе. Метод AddColor выполняет основную работу по построению дерева. Он получает красную, зеленую и синюю составляющие пиксела в пространстве RGB. Цвет сначала добавляется в корневой узел, после чего по первым битам составляющих RGB формируется индекс узла второго уровня. Пиксел добавляется на всех уровнях до тех пор, пока мы не встретим листовой узел. Если в процессе перебора оказывается, что подузел еще не был создан, метод создает его. Обра-
Квантование цветов 733 тите внимание: объединенные листовые узлы не подвергаются повторному делению. Максимальное количество узлов в классе KOctree устанавливается константой MAXN0DE. В настоящее время эта константа равна 65 536; обычно этого хватает для точного представления 16-разрядных изображений. Максимально допустимое дерево занимает около 3 Мбайт памяти. Если дерево содержит слишком много узлов, AddColor вызывает метод Reduce, чтобы произвести сокращение. Сокращение выполняется с постепенным повышением порога, начальное значение которого равно 1. На первом проходе объединяются все листовые узлы, содержащие один пиксел. Если после первого прохода по-прежнему остается слишком много узлов, порог увеличивается и процесс повторяется. Метод Reduce реализует алгоритм сокращения в три этапа. Сначала все не листовые подузлы сокращаются рекурсивным вызовом Reduce. Если после этого все подузлы текущего узла являются листовыми, а общее количество пикселов не превышает порога, все подузлы удаляются, а текущий узел помечается как листовой. Вспомните, что говорилось выше: AddColor добавляет информацию на каждый уровень дерева, поэтому каждый узел содержит сводные данные обо всех своих подузлах. На последнем этапе Reduce проверяет все листовые подузлы с небольшим количеством пикселов и объединяет их с одним из соседних узлов. Слияние соседних узлов выполняется методом Merge. Метод просто находит ветвь к листовому узлу и включает в нее данные RGB. Более рациональный алгоритм должен обеспечивать поиск ближайшего совпадения. Рассмотренные функции строят дерево и выполняют усечение, необходимое в том случае, если дерево становится слишком большим. После того как дерево построено, метод ReduceLeaves постепенно сокращает его до тех пор, пока количество листовых узлов не окажется ниже допустимого. Для сокращения дерева с увеличивающимся пороговым значением применяется уже знакомый метод Reduce. Мелкие узлы постепенно сливаются в большие узлы более высокого уровня, а большие узлы не участвуют в слиянии до тех пор, пока порог не поднимется до достаточно большой величины. Идея заключается в том, чтобы ограниченное число листовых узлов как можно точнее представляло распределение цветов в изображении. Таким образом, узлы с большим количеством пикселов попадут в итоговый набор цветов с большей вероятностью, нежели узлы с малым количеством пикселов. Метод GetPalette завершает квантование, заполняя массив структур PALETTEENTRY и массив частот. Он вызывает метод ReduceLeaves, чтобы уменьшить количество листовых узлов до заданной величины, и метод KNode: :PickLeaves для заполнения двух массивов. Нам остается лишь передать все пикселы растра классу KOctree для построения дерева, а затем сгенерировать палитру по данным листовых узлов. Класс KPaletteGen приведен в листинге 13.7. Листинг 13.7. Класс KPaletteGen: построение палитры с применением октантного дерева class KPaletteGen : public KPixelMapper {
734 Глава 13. Палитры KOctree octree: // Вернуть true, если данные изменились virtual boo! MapRGB(BYTE & red. BYTE & green. BYTE & blue) { octree.AddColor(red. green, blue); return false; } public: void AddBitmap(KImage & dib) { dib.Pixe!Transform(* this); } int GetPalette(RGBQUAD * pEntry. int * pFreq. int size) { return octree.GenPalette(pEntry, pFreq. size); int GenPalette(BITMAPINFO * pDIB. RGBQUAD * pEntry. int * pFreq. int size) { KImage dib; KPaletteGen pal gen; dib.AttachDIBCpDIB, NULL. 0); palgen.AddBitmap(dib); return palgen.GetPalette(pEntry. pFreq. size); } Класс KPaletteGen является производным от класса KPixelMapper, созданного в главе 12 для преобразования пикселов DIB. Вероятно, вы еще не забыли, что класс KPixel Mapper должен только реализовать метод MapRGB, который будет вызываться для каждого пиксела растра. Метод KPaletteGen::MapRGB просто добавляет цветной пиксел в экземпляр класса KOctree. Метод AddBitmap перебирает все пикселы растра и вызывает MapRGB для каждого пиксела. Метод GetPalette возвращает окончательную цветовую таблицу. Глобальная функция GenPalette генерирует цветовую таблицу для упакованного DIB-растра, для чего она использует классы KImage и KPaletteGen. Палитра, сгенерированная алгоритмом квантования по октантному дереву, обеспечивает очень хорошее качество даже в сравнении с профессиональными графическими пакетами. Ниже приведена 16-цветная цветовая таблица, построенная для изображения тигра с рис. 13.4. Для каждого элемента цветовой таблицы приведены значения RGB и количество пикселов, представляемых данным элементом. Как видите, количества представляемых пикселов неплохо сбалансированы.
Квантование цветов 735 PALETTEENTRY Ра116[] - // Для изображения тигра с рис. 13.4 { 59. { 55. { 76. { 99. { 101. { 113. { 153. { 140. { 166. { 206. { 170. { 173. { 212. { 234. { 232. { 250. 52. 41. 51. 77. 97. 108. 113. 119. 136. 148. 154. 149. 173. 207. 222. 244. 47 } 41 } 42 } 54 } 87 } 84 } 84 } 110 } 113 } 115 } 150 } 142 } 148 } 170 } 209 } 235 } . // . // . // . // . // . // . // . // . // . // . // . // . // . // . // , // 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10, 11. 12. 13. 14. 15. 3874 1792 2893 2823 5567 1652 5417 2475 4136 2521 2312 1899 3749 1610 2659 2781 На рис. 13.8 представлено изображение тигра с палитрами из 16, 64 и 236 цветов, сгенерированными алгоритмом квантования по октантному дереву без полутонирования. Рис. 13.8. Вывод растра с палитрой из 16, 64 и 236 цветов Алгоритм квантования по октантному дереву применяется и для других целей. В графических редакторах часто предусматривается возможность подсчета цветов в растре, чтобы выбрать способ сжатия. Для изображений True Color подсчет точного количества цветов является непростой задачей, поскольку существует 16,7 миллиона возможных вариантов. Октантное дерево является удобной структурой данных для решения этой задачи. Количество цветов в изображении совпадает с количеством листовых узлов в представлении дерева, если только у нас хватит памяти для полного сканирования изображения. Если класс KNode используется лишь для подсчета цветов, его можно оптимизировать для уменьшения затрат памяти. В альтернативном способе подсчета цветов строится массив 256x256x256 бит, в котором каждый бит представляет цвет в пространстве RGB 8x8x8. Общие затраты цамяти равны 2 Мбайт.
736 Глава 13. Палитры Сокращение цветовой глубины растра Итак, у нас имеется хороший алгоритм для построения «оптимальной» палитры. Следующим шагом будет преобразование растров High Color и True Color в индексный растр или вообще сокращения цветовой глубины растра. Например, мы можем преобразовать растр True Color в формат с кодировкой 8 бит/пиксел, что приведет к его сокращению до трети исходного размера, а также возможному выигрышу от сжатия RLE. Кроме того, можно преобразовать 8-разрядный растр в 4-разрядный. При работе с цветовой таблицей или палитрой простейший способ сокращения цветовой глубины сводится к алгоритму поиска ближайшего подходящего цвета. Цвет каждого пиксела в растре сравнивается со всеми цветами в таблице; индекс ближайшего совпадения принимается за новое значение пиксела в новом растре. В листинге 13.8 приведен класс KColorMatch, реализующий линейный поиск цветов методом «грубой силы». Метод KColorMatch::ColorMatch ищет в массиве структур RGBQUAD цвет, ближайший к заданному в цветовом пространстве RGB. Листинг 13.8. Класс KColorMatch: простой подбор цветов class KColorMatch { public: RGBQUAD *m_Colors; int mjiEntries; int squarednt i) { return i * i; } public: BYTE ColorMatch(int red, int green, int blue) { int dis • 0x7FFFFFFF; BYTE best = 0; if ( red<0 ) red=0; else if ( red>255 ) red=255: if ( greenO ) green=0: else if ( green>255 ) green=255: if ( blue<0 ) blue=0: else if ( blue>255 ) blue=255; for (int i-0; i<m_nEntries; i++) { int d - square(red - m_Colors[i].rgbRed): if ( d>dis ) continue: d +- square(green - m_Colors[i].rgbGreen): if ( d>dis ) continue: d +- square(blue - m_Colors[i].rgbBlue):
Сокращение цветовой глубины растра 737 } V0 { if ( d < { dis = best } } return best; dis ) : d; = i; id Setup(int nEntry. RGE mjiEntries m_Colors = nEntry; = pColor; В листинге 13.9 приведен простой класс для сокращения цветовой глубины растра, основанный на классах KColorMatch и KPixelMapper. Класс KColorReduction поддерживает только построение 8-разрядных DIB-растров, однако он легко расширяется для работы с другими форматами. Его главный метод, Convert8bpp, создает новый 8-разрядный растр, строит оптимальную цветовую таблицу с помощью алгоритма квантования по октантному дереву, а затем использует метод KImage: :PixelTransform для обращения к алгоритму подбора цветов. Листинг 13.9. KColorReduction: сокращение цветовой глубины поиском ближайшего цвета class KColorReduction : public KPixelMapper { protected: i nt mjiBPS; BYTE * m_pBits; BYTE *m_pPixel; KColorMatch m__Matcher; // Вернуть true, если данные изменились virtual bool MapRGB(BYTE & red. BYTE & green, BYTE & blue) { *m_pPixel ++ = m_Matcher.ColorMatch(red. green, blue); return false; virtual bool StartLine(int line) { m_pPixel = m_pBits + line * mjiBPS; // первый пиксел строки развертки return true; public: BITMAPINFO * Convert8bpp(BITMAPINF0 * pDIB); BITMAPINFO * KColorReduction::Convert8bpp(BITMAPINF0 * pDIB) Продолжение &
738 Глава 13. Палитры Листинг 13.9. Продолжение { mjiBPS « (pDIB->bmiHeader.biWidth + 3) / 4 * 4; // 8-разрядная // строка развертки int headsize = sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD); BITMAPINFO * pNewDIB - (BITMAPINFO *) new BYTE[headsize + mjiBPS * abs(pDIB->bmiHeader.biHeight)]; memsetCpNewDIB. 0, headsize); pNewDIB->bmiHeader.biSize - sizeof(BITMAPINFOHEADER); pNewDIB->bmiHeader.biWidth = pDIB->bmiHeader.biWidth; pNewDIB->bmiHeader.biHeight = pDIB->bmiHeader.biHeight; pNewDIB->bmiHeader.biPlanes = 1; pNewDIB->bmiHeader.biBitCount = 8; pNewDIB->bmiHeader.biCompression = BI_RGB; memset(pNewDIB->bmiColors, 0. 256 * sizeof(RGBQUAD)); int freq[236]; m_Matcher.Setup(GenPalette(pDIB, pNewDIB->bmiColors( freq. 236), pNewDIB->bmiColors); m_pBits = (BYTE*) & pNewDIB->bmiColors[256]: if ( pNewDIB==NULL ) return NULL; KImage dib; dib.AttachDIB(pDIB, NULL. 0): dib.PixelTransform(* this); return pNewDIB; } Класс KColorReduction обеспечивает почти тот же результат, что и при выводе растра средствами GDI без применения режима HALFTONE. Удивляться не приходится, поскольку GDI использует практически такой же алгоритм, хотя и лучше оптимизированный. В режиме HALFTONE GDI может использовать полутонирование для создания плавных переходов между оттенками цвета. Алгоритм поиска ближайших совпадений подбирает цвет для каждого пиксела независимо от других, тогда как полутоновый алгоритм пытается генерировать блоки пикселов, средний цвет которых аппроксимирует цвет исходного изображения. Режим масштабирования HALFTONE поддерживается только в системах семейства NT. Полутоновый алгоритм, используемый GDI, основан на простом смешении цветов. Существуют и более качественные алгоритмы — например, алгоритм рассеивания ошибок (error-diffusion) Флойда—Стейнберга. В этом алгоритме цвет каждого пиксела суммируется с накапливаемой ошибкой, изначально равной 0. Для полученного цвета обычным образом подбирается соответствие в цветовой таблице, а возвращаемый индекс сохраняется в итоговом растре. Основное от-
Сокращение цветовой глубины растра 739 личие от алгоритма GDI заключается в распределении расхождения между исходным и найденным цветом по соседним пикселам, что влияет на подбор цветов для этих пикселов. В алгоритме Флойда—Стейнберга ошибка делится на четыре неравные части (3/16, 5/16, 1/16 и 7/16), прибавляемые к четырем соседним пикселам. Для уменьшения количества сетчатых узоров, возникающих при смешивании, четные и нечетные строки развертки сканируются в противоположных направлениях. На рис. 13.9 изображена схема распределения ошибок в алгоритме Флойда—Стейнберга. 7/16 1/16 iiiiiiiijiiMiiiiiiiiiiii 5/16 3/16 3/16 lllllllSiBllllllllll 5/16 7/16 1/16 Прямое сканирование Обратное сканирование Рис. 13.9. Распределение ошибок в алгоритме Флойда—Стейнберга В листинге 13.10 приведена наша реализация алгоритма распределения ошибок. Класс KErrorDiffusionColorReduction является производным от класса KColor- Reduction, что позволяет нам использовать готовый код подбора цветов и построения 8-разрядного растра. Вместо функции отображения пикселов переопределяется механизм обработки 24-разрядных строк развертки. Алгоритму рассеяния ошибок нужны дополнительные переменные для хранения накапливаемой ошибки и флага, управляющего направлением сканирования строки. Реализация алгоритма на уровне строк развертки выглядела бы гораздо проще и работала бы быстрее, но для полноты решения мы должны предоставить реализации для строк развертки в других форматах. Листинг 13.10. Алгоритм рассеяния ошибок Флойда—Стейнберга class KErrorDiffusionColorReduction : public KColorReduction { int * red_error; int * green_error; int * blue_error; bool m_bForward; virtual bool StartLine(int line) { m_pPixel = m_pBits + line * m_nBPS; // Первый пиксел строки m_bForward = (line & 1) == 0; return true; } Продолжение &
740 Глава 13. Палитры Листинг 13.10. Продолжение virtual void Map24bpp(BYTE * pBuffer. int width); public: BITMAPINFO * Convert8bpp(BITMAPINF0 * pDIB): }: inline void ForwardDistribute(int error, int * curerror. int & nexterror) { if ( (error<@060>-2) || (error>2) ) // Ошибка -2..2 не распределяется { nexterror = curerror[l] + error * 7 / 16; curerror[-l] += error * 3 / 16; curerror[ 0] += error * 5 / 16; curerror[ 1] += error / 16; } else nexterror = curerror[l]; // // 3/16 X 5/16 7/16 1/16 } inline void BackwardDistribute(int error, int * curerror, int & nexterror) { if ( (error<-2) || (error>2) ) // Ошибка -2..2 не распределяется { nexterror = curerror[-l] + error * 7 / 16; // 7/16 X // 1/16 5/16 3/16 curerror[ 1] += error * 3 / 16 curerror[ 0] += error * 5 / 16 curerror[-l] += error / 16 } else nexterror = curerror[-l]; BITMAPINFO * KErrorDiffusionColorReduction::Convert8bpp(BITMAPINF0 * pDIB) { int extwidth = pDIB->bmiHeader.biWidth + 2; int * error = new int[extwidth*3]: memset(error. 0. sizeof(int) * extwidth * 3); red_error = error + 1; green_error = red_error + extwidth; blue_error = green_error + extwidth; BITMAPINFO * pNew = KColorReduction::Convert8bpp(pDIB); delete [] error; return pNew;
Сокращение цветовой глубины растра 741 void KErrorDiffusionColorReduction::Map24bpp(BYTE * pBuffer, int width) { int next_red. next_green, next_blue; if ( m_bForward ) { next_red = red_error[0] next_green = green_error[0] next_blue = blue_error[0] for (int i=0; i<width; i++) { int red = pBuffer[2] int green = pBuffer[l] int blue = pBuffer[0] BYTE match = m_Matcher.ColorMatch( red+next_red, green+next_green, blue+next_blue ); ForwardDistribute(red - m_Matcher.m_Colors[match].rgbRed . red__error +i. next_red); ForwardDistribute(green - m_Matcher.m_Colors[match].rgbGreen. green_error+i. next_green); ForwardDistribute(blue - m_Matcher.m_Colors[match].rgbBlue, blue_error +i. next_blue); * m_pPixel ++= match; pBuffer += 3; else next_red = red_error[width-l] next_green = green_error[width-l] next_blue = blue_error[width-l] pBuffer += 3 * width - 3; m_pPixel += width - 1; for (int i=width-l; i>=0; i--) { int red = pBuffer[2] int green = pBuffer[l] int blue = pBuffer[0] BYTE match = m_Matcher.ColorMatch( red+next_red. green+next_green, blue+next_blue ); BackwardDistribute(red - m_Matcher.m_Colors[match].rgbRed . red_error +i. next^red); BackwardDistribute(green - m_Matcher.m_Colors[match].rgbGreen, green_error+i. next_green); BackwardDistribute(blue - m_Matcher.m_Colors[match].rgbBlue. blue_error +i, next_blue); Продолжение^
742 Глава 13. Палитры Листинг 13.10. Продолжение * m_pPixel --= match; pBuffer -= 3; } } } Класс рассеяния ошибок содержит четыре дополнительные переменные. В трех из них хранятся массивы ошибок для каналов RGB. Функция Convert8bpp выделяет память под массивы из кучи и инициализирует ее нулями. Обратите внимание: инициализация обеспечивает возможность индексации red_error[-l] и red_error[width], чтобы избежать проверки границ при распределении ошибки. Переменная m_bForward указывает направление сканирования строки развертки (прямое или обратное), ее значение присваивается функцией StartLine. Две подставляемые (inline) функции, ForwardDistribute и BackwardDistribute, распределяют ошибку по трем каналам. Они получают текущую ошибку и указатель на текущую позицию в массиве ошибок, а возвращают следующее значение ошибки. В каждой строке развертки функция Мар24Врр суммирует составляющие цвета каждого пиксела с ошибками каналов, подбирает цвет, после чего распределяет ошибки и переходит к следующему пикселу. Алгоритм рассеяния ошибок обеспечивает гораздо лучший результат, чем алгоритм подбора ближайшего цвета, а в большинстве случаев — лучший, чем полутоновый алгоритм GDI. Одним из дополнительных преимуществ является то, что он может использовать любую палитру, тогда как полутоновый алгоритм GDI обычно работает с меньшим количеством цветов. Итоги Эта глава посвящена проблеме получения качественных цветных изображений на графических устройствах с ограниченным набором цветов. Для решения этой задачи приложению приходится иметь дело с палитрами, использовать их совместно с другими приложениями, строить палитры по цветовой таблице растра, производить квантование и сокращение цветовой глубины. В ближайшем будущем палитры по-прежнему останутся актуальными для приложений, ориентированных на массового потребителя. Если приложение использует более 20 цветов, при проектировании и реализации следует принимать во внимание палитру. С векторной графикой обычно бывает меньше проблем, чем с растрами, поскольку в ней обычно используется меньшее количество цветов. В обычных приложениях полутоновая палитра с равномерным распределением цветов, как правило, обеспечивает достаточно хороший результат. Однако в приложениях, работающих с высококачественной графикой или одновременно отображающих большое количество цветов, оптимальная специализированная палитра способна значительно улучшить качество графики по сравнению с полутоновой.
Итоги 743 Палитры поддерживаются и для поверхностей DirectDraw, что позволяет игровым программам создавать специальные эффекты анимации, основанной на изменении палитры, снижает затраты памяти или просто улучшает быстродействие на маломощных компьютерах. Наше знакомство с растрами и палитрами подошло к концу. В следующей главе мы переходим к совершенно новой теме — шрифтам и работе с текстом. Пример программы К главе 13 прилагается программа Palette, иллюстрирующая весь изложенный материал (табл. 13.3). Таблица 13.3. Программа главы 13 Каталог проекта Описание Samples\Chapt_13\Palette Демонстрация работы с системной палитрой, обработки сообщений палитры, применения полутоновых палитр, web-цветов и оттенков серого цвета, изменения видеорежима, построения палитры на базе растра, квантования цветов, распределения ошибок и т. д.
Глава 14 Шрифты С этой главы начнется наше знакомство со шрифтами и текстовыми операциями в графическом программировании Windows. Шрифты и их применение в печати имеют долгую и интересную историю. Давно, в 2400 году до нашей эры, индусы освоили изготовление резных штампов. Около 450 года нашей эры китайцы научились оставлять на бумаге оттиски штампов, намазанных чернилами, положивших начало современному книгопечатанию. В 1049 году китайцы разработали методику печати с применением глиняных литер, а в 1241 году корейцы перешли на металлические литеры. Еще два века спустя, в 1452 году, Гутенберг открыл новую эпоху в книгопечатании. С его изобретения — печатного станка — начался массовый выпуск типографских литер, используемых при наборе страниц текста. С этого времени полный набор символов одной гарнитуры и кегля стал называться в печатном деле «шрифтом». В 1976 году некий профессор решил выпустить второе издание своей книги, опубликованной за несколько лет до этого с применением тех же свинцовых матриц, что и у Гутенберга. К своему удивлению, он узнал, что старая технология постепенно уходит в прошлое, а новая — фотооптические наборные машины — еще не обеспечивает приемлемого качества. Профессор отказался использовать столь несовершенную технологию для представления плодов своего 15-летнего упорного труда и взялся за решение старых типографских проблем на базе компьютерных технологий. Четыре года спустя он разработал новый способ описания шрифтов математическими формулами, что привело к появлению полноценных наборных систем. С помощью одной из таких систем он и опубликовал свою работу, издание которой задержалось на 4 года. Профессора звали Дональд Кнут (Donald E. Knuth), шрифтовая программа называлась METAFONT, а для верстки использовался пакет ТеХ. Более того, все плоды труда Кнута вместе с полными исходными текстами были доступны для всех желающих, поэтому пользователи всего мира могли конструировать шрифты для любого языка и создавать электронные макеты книг. Шрифты и текст традиционно считаются весьма сложной темой. Эта глава посвящается шрифтам, а следующая — операциям с текстом. В этой главе мы
Что такое шрифт? 745 рассмотрим наборы символов, кодировки, глифы, шрифты вообще и их конкретную разновидность — шрифты TrueType, а также технологию внедрения шрифтов. Что такое шрифт? Компьютерная верстка всегда считалась одной из главных областей применения персональных компьютеров. В школьные годы и на протяжении всей жизни всем нам приходится создавать всевозможные документы и готовить к публикации книги. Процесс компьютерной верстки сильно зависит от поддержки шрифтов и текстовых операций на уровне операционной системы. Впрочем, шрифты и текст не относятся к базовым функциям систем компьютерной графики — в некоторых книгах, посвященных теоретическим основам компьютерной графики, они вообще не упоминаются. Скорее, шрифты и текст следует рассматривать как объекты применения общих принципов для решения целого класса практических задач. Как правило, шрифтовые и текстовые средства операционной системы реализуются с применением базовых графических примитивов (пикселы, линии, кривые, фигуры и растры). Вы даже можете создать собственные средства для работы со шрифтами и текстом на базе этих примитивов. Одним из основных инструментов компьютерной верстки являются шрифты — своего рода шаблоны для представления символов языка, с которым вы работаете. Традиционно шрифт определяется как полный набор литер одной гарнитуры и одного кегля, что соответствует специфике применения шрифта в типографском деле. Литерой называется прямоугольный блок (обычно металлический), на лицевой поверхности которого находится рельефное изображение символа. Цифровые технологии заметно расширили смысл термина и возможности шрифтов. В этом разделе мы рассмотрим базовые концепции и термины, относящиеся к работе со шрифтами в контексте графического программирования Windows. Наборы символов и кодировки Набор символов (character set) в системе Windows определяется... просто как совокупность символов. У каждого набора есть имя и числовой идентификатор. Например, стандартный набор символов Windows называется ANSICHARSET, его идентификатор равен 0, и он содержит символы 7-разрядной стандартной кодировки ANSI, определенной в Windows для западных языков. В окне DOS-сеанса используется набор OEM_CHARSET с идентификатором 255; он содержит те же 7-разрядные символы ANSI с дополнительными символами, которые были определены компанией IBM на ранних порах существования DOS. Наборы символов с однобайтовыми идентификаторами вряд ли можно считать хорошим решением, особенно в эпоху глобальных электронных коммуникаций в Интернете. На смену им пришла концепция кодировок, или кодовых страниц (code pages). Кодировкой называется схема представления символов из
746 Глава 14. Шрифты заданного набора одним или несколькими байтами информации. Таким образом, с формальной точки зрения кодировка представляет собой отображение последовательности битов в набор символов. Кодировки, в отличие от наборов символов, обозначаются двухбайтовыми числовыми идентификаторами, что обеспечивает поддержку большего количества языков. В табл. 14.1 перечислены наборы символов и соответствующие им кодировки, поддерживаемые операционной системой Windows. Первые 14 наборов, от SHIFJISCHARSET до EASTEUROPESET, связаны с кодировками однозначным соответствием. Например, для набора SHI FT JISCHARSET используется кодировка 932 (сокращение JIS означает Japanese Industry Standard, то есть «японский промышленный стандарт»). Набор символов 6B2312CHARSET соответствует кодировке 932 (GB — сокращение китайского национального стандарта). Последним трем наборам, ANSICHARSET, OEM_CHARSET и MAC_CHARSET, соответствуют разные кодировки в зависимости от локального контекста системы/процесса. Они отображаются на разные кодировки в зависимости от того, где действительно находится ваш компьютер или, по крайней мере, где компьютер «думает», что находится. Если в стандартном локальном контексте используется английский язык, то набор ANSICHARSET соответствует кодировке 1252, OEM_CHARSET соответствует кодировке 437, a MAC_CHARSET — кодировке 10000. Таблица 14.1. Наборы символов и кодировки Имя набора символов SHIFTJIS_CHARSET HANGUL_CHARSET J0HAB_CHARSET GB2312_CHARSET CHINESEBIG5_CHARSET GREEK_CHARSET TURKISH_CHARSET VIETNAMESE_CHARSET HEBREW_CHARSET ARABIC_CHARSET BALTIC_CHARSET Идентификатор набора символов 128 129 130 134 136 161 162 163 177 178 186 Кодировка 932, японский 949, корейский 1361 936, китайский (упрощенное письмо) 950, китайский (традиционное письмо) 1253, греческий (Windows) 1254, турецкий (Windows) 1258, вьетнамский (Windows) 1255, иврит (Windows) 1256, арабский (Windows) 1257, прибалтийский (Windows) Применение Япония Корея Китай, Сингапур Тайвань, Гонконг Турция
Что такое шрифт? 747 Имя набора символов RUSSIANCHARSET THAI_CHARSET EASTEUROPE_CHARSET ANSI_CHARSET OEM_CHARSET MAC_CHARSET Идентификатор набора символов 204 222 238 0 255 77 Кодировка 1251, кириллица (Windows) 874, тайский 1250, Windows Latin 2 1252, Windows Latin 1 1250, Windows Latin 2 1256, арабский (Windows) 437, MS_DOS Latin 1 852, MS_DOS Latin 2 864, MS_DOS арабский 10000, Mac (англоязычные страны) 10029, Mac (Центральная Европа) 10007, Mac (кириллица) Применение Славянские страны Центральная Европа США, Великобритания, Канада и т. д. Венгрия, Польша и т.д. Ирак, Египет, Йемен и т. д. США, Великобритания, Канада и т. д. Венгрия, Польша и т. д. Ирак, Египет, Йемен и т. д. США, Великобритания, Канада и т. д. Венгрия, Польша и т.д. Украина, Россия и т. д. Большинство кодировок содержит 256 символов. Один символ в них представляется всего одним байтом, поэтому эти кодировки называются однобайтовыми. Первые 128 символов однобайтовой кодировки обычно совпадают с символами 7-разрядного стандарта ANSI. Первые 32 символа соответствуют не- отображаемым управляющим кодам, за ними следует пробел, знаки математических операций и служебные символы, цифры и буквы английского алфавита в верхнем и нижнем регистре. Содержимое следующих 128 символов сильно изменяется в зависимости от кодировки. Именно здесь хранятся буквы национальных алфавитов, дополнительные знаки, символы псевдографики и даже недавно появившийся знак «евро». На рис. 14.1 приведено содержимое кодировки 1252 (Windows Latin l). Как видно из рисунка, вторая половина кодировки содержит символы национальных алфавитов, денежные знаки, апострофы и кавычки и т. д. Некоторые символы, помеченные пустыми прямоугольниками, не используются. Первый символ, 0x80, недавно был закреплен за знаком «евро». Для сравнения на рис. 14.2 изображена кодировка Windows для работы с кириллицей (1251). Обратите внимание: знак «евро» находится в другой позиции, поскольку символ с кодом 0x80 уже занят.
748 Глава 14. Шрифты 00 10 20 30 40 ЬО 60 70 80 90 АО ВО СО DO ЕО F0 П п 0 (А Р Р € D о А D а б П П 1 1 А 0 а q □ с i ± А N а п П п II о R R b г 1 ' Ф 2 А 0 а 6 П п # ч с я с s / (.С £ 3 А О а 6 П П $ 4 D Т d t ■>■> 55 п ' А 0 а 6 П П % S F, тт е 11 • ¥ l-i А 6 0 а б D □ _ 6 F V f V t - 1 1 1 Ж 0 ае 6 П □ i 7 G W g w + + — § Q X 9 -s- D П ( 8 Н X h X * ~ t Е 0 ё 0 D □ ) 9 I Y 1 У %0 тм © 1 Б и ё и D □ * J Z J z S s a о E U ё u D П + > к [ k { < > « » Ё U ё u D П •> < L \ 1 | (E oe -■ Va I \] i ii П П - = M ] 111 } □ D - '/a I Y i У D > N Л n ~ Z z ® 3/4 I Ь i t D / ? 0 о a a Y i I В i У Рис. 14.1. Кодировка Windows Latin 1 (1252) 00 10 20 30 40 50 60 70 80 90 АО ВО CO DO EO FO □ D 0 @ P " p ъ Ъ о A P a P □ D 1 1 A Q a q г i У ± Б С б с □ D II 2 В R b г ? ? У i в т в т D □ # 3 С S с s г (.(. J i Г У г У D D $ 4 D Т d t 55 55 п г Д Ф д ф □ D % 5 Е и е и • Г И Е X е X □ D _ б F V f V t - 1 1 1 Ж Ц Ж Ц □ □ i 7 G W g w + + — § 3 Ч 3 ч □ D ( 8 Н X h X € D Ё ё И Ш и ш □ D ) 9 I Y 1 У %о тм © № И щ й Щ □ D * J Z J z л> л> е е К Ъ к ъ □ □ + 5 К [ к { < > « » Л ы л ы □ □ 5 < L \ 1 1 Е> н> - J М Ь м ь □ □ - = М ] m } К К - S н э н э □ _ > N А 11 ~ Tl h ® S 0 Ю о ю D / ? О о □ ц ц I i п я п я Рис. 14.2. Кодировка Windows Cyrillic (1251)
Что такое шрифт? 749 Хотя однобайтовых кодировок хватает для представления символов большинства мировых языков, три восточных языка содержат слишком большое количество символов, не укладывающееся в границы однобайтовой кодировки. В китайской письменности используются тысячи иероглифов, часть из которых была позаимствована в Японии и Корее. Символы больших иероглифических наборов представляются несколькими байтами, поэтому такие кодировки обычно называются двухбайтовыми или многобайтовыми. В многобайтовом наборе символов (MultiByte Character Set, MBCS) символы 7-разрядного набора ASCII представляются одним байтом, а иероглифы китайского, японского и корейского языка — двумя байтами. Текстовая строка в кодировке MBCS всегда анализируется слева направо. Если первый (префиксный) байт меньше 128 (0x80), значит, перед нами однобайтовый символ из первой половины кодировки Windows Latin l (1252). Если префиксный байт равен 128 и выше, необходима дополнительная проверка, поскольку в двухбайтовых символах могут использоваться только байты из определенных интервалов. Если префиксный байт принадлежит к допустимому интервалу, происходит дополнительная проверка второго байта. Для кодировки 936, используемой в Китае и Сингапуре, оба байта должны лежать в интервале [0xA1...0xFE], что позволяет представить до 8836 двухбайтовых символов. Кодировка 949, используемая в Корее, устроена чуть сложнее. В ней префиксный байт принадлежит интервалу [0x81..0xFE], а второй байт должен входить в один из интервалов [0x41..0x5а], [0x61..0x7а] и [0x81..OxFE]. Количество допустимых символов увеличивается до 19 278. На рис. 14.3 приведен небольшой фрагмент традиционной китайской кодировки. Обратите внимание: китайские иероглифы в кодировке 950 сортируются по количеству черт. На рис. 14.3 изображены относительно простые иероглифы, содержащие не более четырех черт1. А440 А450 А460 А470 А4А0 А4В0 А4С0 A4D0 А4Е0 A4F0 — L t: Ф 7 if Я £ •х Ш Z, + % % в. iT «J ^ Г № Т ь я г щ ib м ^ ¥ л_ -ь X *} U] тр 01 % X *L ft 73 '. 1"- JH tp № Ъ ^k X Ж_ Л. т X X ф 4- я ?1 X 'Л 7 3t р а ft ъ и /> 4 Ж . ± ± В 3L IK ш X ft я Л Y ± В р 7G ¥ R Ъ & А % $ Ф ^ % fr * В Я Л я * -т -. й м В в JF А А & )\- # тЧ -к а я ф А <L т X Ж ъ Е -н- * Л 73 Ш ? Щ £ £ ж ъ X ЗЕ <| Ъ J. Я я. % Д §1 ± й Л ^ ^ ? t И £ jfr 3? ? Рис. 14.3. Фрагмент кодировки 950 (китайский, упрощенное письмо) На первый взгляд может показаться, что количество черт в некоторых иероглифах больше четырех, однако это связано со специфическими правилами подсчета. — Примеч. перев.
750 Глава 14. Шрифты При работе с разными кодировками (особенно многобайтовыми) в программах возникает немало сложностей. Например, даже для решения простейших задач вроде перехода к следующему символу приходится вызывать функцию Windows API CharNext вместо того, чтобы просто увеличить указатель на 1. С переходом к предыдущему символу дело обстоит еще сложнее — об этом свидетельствует передача дополнительного параметра (начального адреса строки) функции CharPrev. Большие хлопоты возникают и с преобразованием символов между кодировками. Для решения этих проблем и был предложен стандарт Unicode. Разработка, сопровождение и продвижение стандарта двухбайтовой кодировки символов Unicode осуществляется Консорциумом Unicode. В этот консорциум входят Apple, Hewlett-Packard, IBM, Microsoft, Oracle, Sun, Xerox и другие компании. Стандарт Unicode позволяет представить большую часть символов письменности практически всех языков мира. В нем используется 16-разрядное представление без префиксов или переключения режимов, что обеспечивает возможность выражения до 65 536 символов. В Unicode символ представляется 16-разрядным значением от 0000 до FFFF (в шестнадцатеричной записи). Символы группируются на логические зоны. Например, зона 01 соответствует базовым символам латинского алфавита с кодами от 0000 до 007F. В зоне 29 находятся общие знаки препинания с кодами от 2000 до 206F. Самая большая зона 54 содержит 29 902 китайских иероглифов, используемых в Китае, Японии и Корее. Вторая по величине зона 55 содержит И 172 иероглифа хангыль, используемых в Корее. На рис. 14.4 изображены символы, входящие в зону условных знаков Unicode. 2600 2610 2620 2630 2640 2650 2660 D Д ZHZ * ф ф * 0 z ЛИЛ 6 V5 V 1* И V МММ с? О i?i X <& j^jU ч 00 * # □ т ш ш к Ф ♦ • □ t — ¥ © v * □ t т т т т W 1 ♦ < □ f т т а т Е JL * ГС □ f Щ Г й ё> 0 □ * © V & J а •ш О © Ж ® р V яг Ф • © ш л 6 "S3 ф а SI 1 Л <? Ь £ 3> ИР к ь ^ GT ® С -Л. й *1 Ф Р © 5 щ i # Рис. 14.4. Зона условных знаков Unicode Хотя операционная система Windows проектировалась для поддержки разных кодировок и языков, для работы с конкретными кодировками и языками нужны дополнительные файлы, которые могут отсутствовать в стандартном варианте установки вашей системы. Дополнительные пакеты устанавливаются при помощи приложения Regional Settings (Язык и стандарты) панели управления либо с компакт-диска операционной системы, либо с web-сайта Microsoft. Функция EnumSystemCodePages API перечисляет все кодовые страницы, поддерживаемые или установленные в вашей системе.
Что такое шрифт? 751 Глифы Наборы символов и кодовые страницы определяют лишь логическую группировку и представление символов, а не их внешний вид. Символ — всего лишь абстрактная концепция, а не конкретное представление. Нарисованный на бумаге символ обретает графическую форму, которая называется глифом (glyph). Например, в кодировке Windows Latin 1 английская прописная буква А имеет индекс 0x41, однако она может выглядеть по-разному, как показано на рис. 14.5. АААААААААаААЛАлААИЛл Рис. 14.5. Различные глифы для буквы А Взаимосвязь между глифом и символом Между символами и глифами в шрифте обычно существует однозначное соответствие. Один символ представляется ровно одним глифом, а один глиф представляет ровно один символ. Впрочем, это не всегда так. Встречаются символы, которые представляются комбинацией нескольких глифов, а один и тот же глиф может использоваться в разных символах. Такие глифы характерны для китайских или корейских иероглифов, которые часто состоят из нескольких частей, хотя в качественных шрифтах лучше использовать несколько версий одного глифа. Глифы символов также могут изменяться в зависимости от контекста, в котором записывается символ. Например, символы, находящиеся в начале или в конце предложения, могут оформляться специальными глифами. В частности, контекстные формы глифов широко используются в арабских языках, а при вертикальной записи китайского текста изменяется ориентация скобок. Если некоторые комбинации символов расположены по соседству, они могут быть преобразованы в один глиф, называемый лигатурой. В общем случае символ представляется одним или несколькими глифами, которые могут использоваться несколькими символами; также допускается объединение нескольких символов в лигатуру по специальным правилам. На рис. 14.6 продемонстрирована связь между символами и глифами. В первой строке приведена буква О с разными диакритическими знаками, за которой следуют китайские иероглифы с общим левым ключом. Эти примеры показывают, что один символ может соответствовать нескольким глифам. Во второй строке приведены некоторые лигатуры, используемые в датском, норвежском, французском и английском языках. Третья строка показывает, как круглые и квадратные скобки преобразуются в вертикальные глифы при традиционном вертикальном китайском письме, которое продолжает использоваться в особых случаях (например, в свадебных приглашениях). В последней строке изображены четыре группы глифов для трех арабских символов. Каждый арабский символ может иметь до четырех контекстных глифов, для изолированной, начальной, конечной и промежуточной форм.
752 Глава 14. Шрифты \ г j\ ооооо л$щшщш*& А+Е=/Е C+E=QE f+i=fl f+l=fl \1У<.) к1>Ч Л ту. vV. V V V So* \щ+ V V Рис. 14.6. Связь между символами и глифами Элементы глифа Глифы с постоянными атрибутами обычно группируются. Для букв латинского алфавита к таким атрибутам относятся толщина черт, стиль штриха, применение засечек, выравнивание по базовой линии, форма овалов и петель, величина надстрочных и подстрочных частей и т. д. Базовой линией (baseline) называется воображаемая линия, предназначенная для вертикального выравнивания глифов. Латинские буквы обычно выравниваются по базовой линии; исключение составляют буквы с подстрочными элементами (например, f, g, j и Q). Высота строчной буквы х называется х-высотой и обычно определяет высоту основной части всех глифов строчных букв. Некоторые строчные глифы поднимаются над высотой буквы х; их выносные элементы называются надстрочными (ascender). Некоторые строчные глифы спускаются ниже базовой линии; соответствующие элементы глифов называются подстрочными (descender). Кроме того, глифы могут обладать засечками (serifs ) — маленькими поперечными черточками на концах основных линий. Маленький шарик на конце черты (как в буквах а, с, f и у) называется каплевидным элементом (ball, или ball terminator). Внутрибуквенным просветом (counter) называется область, полностью или частично окруженная глифом (как в буквах р, d или е). Термин «полуовал» (bowl) относится к базовой форме таких букв, как С, G и D. На рис. 14.7 изображены некоторые элементы глифов с засечками. Надстрочный выносной элемент Засечка . i _^^^ Полуовал Базовая линия ^^ wmww или ^^ ^ I i\jj |y\JOC*i I GtyfllrDesigm- Каплевидный Внутрибуквенный Подстрочный элемент просвет выносной элемент Рис. 14.7. Структурные элементы глифа для латиницы
Что такое шрифт? 753 Глифы других языков могут иметь аналогичную структуру или содержать другие элементы, унаследованные по историческим причинам. Шрифт После знакомства с наборами символов, кодировками и глифами можно дать определение шрифта. Шрифтом называется совокупность глифов, обладающих сходным графическим стилем, для которой определено отображение символов поддерживаемых кодировок в глифы. Шрифт может поддерживать одну или несколько кодировок; для каждого символа каждой кодировки он устанавливает соответствие с группой глифов, образующих графическое представление символа. Глифы и правила отображения символов в глифы относятся к базовым компонентам шрифта. Шрифты обладают множеством других атрибутов. Так, у каждого шрифта имеется полное имя (например, Times New Roman Bold или Courier New Italic). Имена шрифтов обычно защищаются авторским правом. Например, компания Microsoft обладает правами на шрифт Wingdings, а шрифт Courier New Italic принадлежит Monotype Corp. Шрифты обычно хранятся в физических файлах в подкаталоге шрифтов системного каталога. На панели управления имеется приложение Fonts (Шрифты) для просмотра, установки и удаления шрифтов в системе. Чтобы получить список всех шрифтов, установленных в системе, необходимо перебрать ключи реестра. Код приведенного ниже фрагмента перечисляет все шрифты в системе и использует собранные данные для заполнения списка. void ListFontsCKListView * pList) { const TCHAR Key_Fonts[] « J'CSOFTWAREWMicrosoftWWindows NT" "WCurrentVersionWFonts"); HKEY hKey; if ( RegOpenKeyEx(HKEY_LOCAL_MACHINE. Key_Fonts, 0, KEY_READ, & hKey)==ERROR_SUCCESS ) { for (int i=0; ; i++) { TCHAR szValueName[MAX_PATH]; BYTE szValueData[MAX_PATH]; DWORD nValueNameLen = MAX_PATH; DWORD nValueDataLen = MAX_PATH; DWORD dwType; if ( RegEnumValueChKey. i. szValueName, & nValueNameLen, NULL. & dwType. szValueData, & nValueDataLen) != ERROR_SUCCESS ) break; pList->AddItem(0. szValueName); pList->AddItem(l. (const char *) szValueData); }
754 Глава 14. Шрифты RegCloseKey(hKey); } } Семейство шрифтов и начертание Имя шрифта определяет семейство, к которому он принадлежит, и его начертание. Семейством называется группа шрифтов, обладающих сходными характеристиками и объединенных общим названием. Например, семейство Times New Roman состоит из четырех разных шрифтов: Times New Roman, Times New Roman Italic, Times New Roman Bold и Times New Roman Bold Italic. Видоизменение шрифта в семействе называется начертанием. К числу распространенных начертаний относятся нормальное, полужирное, курсивное, сжатое, с подчеркиванием и перечеркиванием символов и т. д. Вместо создания новых шрифтов начертание может имитироваться изменением параметров глифа. Например, шрифты, созданные программой METAFONT уже упоминавшегося Кнута, зависят от десятка с лишним параметров, позволяющих изменить размер засечек, толщину черт и т. д. Подчеркивание и перечеркивание в Windows обычно имитируется средствами GDI. Семейство шрифтов является удобной абстракцией, но как приложение узнает, к какому семейству относится тот или иной шрифт? GDI поддерживает 8 флагов для классификации семейств шрифтов по базовым характеристикам глифов. Эти флаги перечислены в табл. 14.2. Таблица 14.2. Флаги семейств и шага шрифта Флаг DEFAULT_PITCH FIXED_PITCH VARIABLE_PITCH FF_DONTCARE FF_R0MAN FFJWISS FF_M0DERN FFJCRIPT FF_DECORATIVE Значение 1 2 4 0«4 1«4 2«4 3«4 4«4 5«4 Описание Произвольный шаг шрифта Моноширинный шрифт Пропорциональный шрифт Шрифт с произвольными атрибутами Шрифт с переменной толщиной линий и засечками Шрифт с переменной толщиной линий без засечек Шрифт с постоянной толщиной линий Рукописный шрифт Затейливый оформительский шрифт В моноширинных шрифтах все глифы имеют одинаковую ширину. Моноширинные шрифты обычно применяются в окнах DOS-сеансов, при выводе листингов и вообще всюду, где необходимо обеспечить выравнивание по вертикали. В пропорциональных шрифтах глифы обладают разной шириной; буквы i или 1 занимают гораздо меньше места, чем т. Текст, выведенный пропорциональным шрифтом, лучше воспринимается человеческим глазом, поэтому в книгах, элек-
Что такое шрифт? 755 тронной документации и на web-страницах используются пропорциональные шрифты. Шрифты семейства Roman обладают переменной толщиной линий и засечками. В семействе Swiss используется переменная толщина линий, но без засечек. Шрифты семейств Roman и Swiss обычно являются пропорциональными. Семейство Modern содержит шрифты с постоянной толщиной линий, как правило — моноширинные. Шрифты семейства Script имитируют рукописный текст. Все остальные экзотические шрифты отнесены к семейству Decorative. На рис. 14.8 приведены примеры шрифтов некоторых семейств. Roman Roman Roman Roman Swiss Swiss Swiss Swiss Modem Modern Modern Modern Qtfwfpt Script Script Script Sct0tatiut Secorative $йШга&Ь<& DECORATIVE Рис. 14.8. Классификация семейств шрифтов В приложениях обычно удобнее работать с семействами шрифтов, нежели с отдельными шрифтами, поскольку семейств меньше и из них удобнее выбирать. В GDI существует функция EnumFontFamiliesEx для перечисления всех семейств шрифтов, доступных в системе. int EnumFontFamiliesEx (HDC hDC. LPLOGFONT IpLogFont. FONTENUMPROC IpEnumFontFamExProc, LPARAM IParam. DWORD dwFlags); В первом параметре передается контекст устройства. Некоторые графические устройства (например, лазерные принтеры или принтеры PostScript) могут поддерживать аппаратные шрифты, предназначенные только для данного устройства. Второй параметр указывает на структуру LOGFONT, поля которой 1 fCharset и 1 fFaceName определяют набор символов и гарнитуру, интересующие приложение. Если указать набор символов DEFAULT_CHARSET, семейства шрифтов, поддерживающие несколько наборов, будут многократно включены в список. При указании конкретного набора символов в перечислении участвуют только семейства шрифтов, содержащие заданную категорию глифов (например, для набора SYMB0L_ CHARSET — глифы символических знаков). Поле IfPitchAndFamily структуры LOGFONT должно быть равно нулю. Параметр IpEnumFontFamExProc указывает на глобальную функцию, вызываемую для каждого перечисляемого семейства шрифтов — такое решение плохо соответствует стилю C++. Впрочем, у нас есть параметр IParam с данными, передаваемыми вызывающей стороной функции косвенного вызова; этим параметром можно воспользоваться для стыковки C++ с Win32. Последний параметр dwFl ags должен быть равен 0. Функция EnumFontFamiliesEx играет ключевую роль при заполнении списков доступных шрифтов в диалоговых окнах приложений. С ее помощью можно получить перечень всех семейств, поддерживающих конкретный набор символов, или всех наборов, поддерживаемых для конкретной гарнитуры. В листинге 14.1
756 Глава 14. Шрифты приведен вспомогательный класс для работы с этой функцией. Реализация по умолчанию сохраняет результаты перечисления в списке. Листинг 14.1. Перечисление семейств шрифтов class KEnumFontFamily { KListView * m_pList; int static CALLBACK EnumFontFamExProc(ENUMLOGFONTEX *lpelfe. NEWTEXTMETRICEX *lpntme, int FontType, LPARAM lParam) { if ( lParam ) return ((KEnumFontFamily *) lParam)->EnumProc(lpelfe. Ipntme, FontType); else return FALSE; } publi с: LOGFONT m_LogFont[MAX_LOGFONT]; int mjiLogFont; unsigned mjiType; virtual int EnumProc(ENUMLOGFONTEX *lpelfe. NEWTEXTMETRICEX *lpntme. int FontType) { if ( (FontType & m_nType)==0 ) return TRUE; if ( mjiLogFont < MAX_L0GF0NT ) m_LogFont[m_nLogFont ++] = 1 pelfe->elfLogFont; m_pList->AddItem(0. (const char *) I pelfe->elfFullName); m_pList->AddItem(l. (const char *) 1 pelfe->elfScript): m_pList->AddItem(2, (const char *) lpelfe->elfStyle); m_pList->AddItem(3. (const char *) lpelfe->elfLogFont.lfFaceName); m_pLi st->AddItem(4. 1 pelfe->elfLogFont.1fHeight); m_pLi st->AddItem(5. 1 pelfe->elfLogFont.1fWidth); m_pLi st->AddItem(6, 1 pelfe->elfLogFont.1fWeight); return TRUE; void EnumFontFamilies(HDC hdc, KListView * pList, BYTE charset = DEFAULT_CHARSET, TCHAR * FaceName = NULL, unsigned type - RASTER JONTTYPE | TRUETYPEJONTTYPE | DEVICEJONTTYPE) { m_pList = pList; m_nType = type; LOGFONT If; memset(& If, 0, sizeof(lf)):
Что такое шрифт? 757 If.lfCharSet = charset; lf.lfFaceName[0] = 0; lf.lfPitchAndFamily = 0; if ( FaceName ) _tcscpy(If.IfFaceName. FaceName); Enum FontFamiliesEx(hdc. & If, (FONTENUMPROC) EnumFontFamExProc, (LPARAM) this, 0); На рис. 14.9 сопоставлены результаты перечисления шрифтов и их семейств. Перечисление шрифтов, основанное на просмотре системного реестра, выводит список всех физических шрифтов в системе. Мы видим четыре шрифта семейства Arial, четыре шрифта семейства Courier New и т. д. При перечислении семейств некоторые семейства встречаются в списке многократно, если они поддерживают разные наборы символов. Например, семейство шрифтов Arial поддерживает 9 разных наборов. j^SpS^IV^^^^^'ft.f.'t.ic.Vt'tr.'.Ti t 1.НИШ J Tahoma (TrueType) 1 Microsoft Sans Serif Regular (TrueType) I SimSun 8c NSimSun (TrueType) 1 SimHei (TrueType) 1 MingLiU 8c PMingLiU (TrueType) I Roman (All res) 1 Script (All res) 1 Modern (All res) 1 Arial (TrueType) 1 Arial Bold (TrueType) 1 Arial Bold Italic (TrueType) I Arial Italic (TrueType) | Courier New (TrueType) 1 Courier New Bold (TrueType) 1 Courier New Bold Italic (TrueType) 1 Courier New Italic (TrueType) | Lucida Console (TrueType) лМШ] ' im ш TAHOMATTF i MICROSS.TTF 1 simsun.ttc jj simheLttf II mingliu.ttc J ROMAN.FON SCRIPT.FON J MODERN.FON ARIALTTF 1 ARIALBD.TTF j ARIALBI.TTF i ARIALI.TT? j COUR.TTF i COURBD.TTF ; COURBI.TTF ! COURI.TTF J! LUCONTTF^| tfeiitew» l&te. [ Anal Western ] Anal Hebrew ] Anal Arabic 1 Anal Greek 1 Anal Turkish | Anal Baltic ] Anal Central European | Anal Cyrillic ] Anal Vietnamese | Courier New Western j Courier New Hebrew ] Courier New Arabic 1 Courier New Greek j Courier New Turkish j Courier New Baltic 1 Courier New Central European и i '- iMe Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular .■l.&Bfo&MMlI Anal Arial Arial Arial Arial Arial Arial Arial Arial Courier New Courier New Courier New Courier New Courier New Courier New Courier New мШЩ 15531 36 f 36 1 36 j 36 $ 36 *£ 36 1 36 1 36 1 36 1 36 I 36 I 36 1 36 [ 36 f 36 j зб Л . j£3 Рис. 14.9. Сравнение шрифтов и семейств шрифтов При поддержке двухбайтовых кодировок (для японского, китайского и корейского языков) функция EnumFontFamiliesEx возвращает два семейства. Например, семейство шрифтов Gulim поддерживает набор HANGUL_CHARSET; в процессе перечисления для него будут указаны семейства Gulim и ©Gulim. Семейства шрифтов, имена которых начинаются с символа @, обладают особыми средствами для поворота двухбайтовых глифов, что позволяет имитировать вертикальную письменность, используемую в Китае, Японии и Корее. С помощью флагов семейств и шага шрифта, перечисленных в табл. 14.2, пытаются классифицировать шрифты и семейства всего одним байтом, что, конечно, не обеспечивает необходимой точности. Значительно более точный способ описания шрифта предоставляет структура PANOSE, в 10 байтах которой кодируются сведения обо всех важнейших характеристиках шрифтов — типе семейства, наличии засечек, насыщенности, пропорциональности, контрасте и т. д.
758 Глава 14. Шрифты Растровые шрифты Существуют разные способы представления глифов шрифта. В простейшем варианте пикселы, образующие глиф, представляются в виде растрового изображения. Такие шрифты называются растровыми. Возможны и другие решения — например, описывать контуры глифа прямыми линиями (векторные шрифты). Большинство шрифтов, используемых в системе Windows в наши дни, относятся к категории TrueType или ОрепТуре. В этих шрифтах для представления контура глифа и управления процессом вывода применяются значительно более сложные средства. В этом разделе мы познакомимся с растровыми шрифтами. Другие категории шрифтов рассматриваются в разделах «Векторные шрифты» и «Шрифты TrueType». Растровые шрифты давно применяются при выводе информации. В эпоху DOS в памяти BIOS хранились растровые шрифты для разных разрешений экрана. Когда приложение выдавало программное прерывание на вывод символа в графическом режиме, система BIOS производила выборку данных глифа и отображала его в заданной позиции. В ранних версиях Windows до появления Windows 3.1 никакие другие шрифты, кроме растровых, вообще не поддерживались. Впрочем, и в наши дни растровые шрифты широко применяются при выводе таких элементов пользовательского интерфейса, как меню, диалоговые окна и сообщения-подсказки, не говоря уже об окнах DOS-сеансов. Даже в новейших операционных системах Windows по-прежнему используются десятки растровых шрифтов. Для разных разрешений экрана требуются разные наборы растровых шрифтов, соответствующих разрешению. Например, файл sserife.fon представляет шрифт MS Sans Serif для режима с разрешением 96 dpi и с аспектным отношением 100 %, тогда как шрифт sseriff.fon предназначен для разрешения 120 dpi. При переходе от мелкого системного шрифта (96 dpi) к крупному (120 dpi) вместо sserife.fon задействуется шрифт sseriff.fon. Смена системного шрифта влияет на преобразование единиц, используемых в процессе конструирования диалоговых окон, в экранные координаты, поэтому элементарное переключение шрифта способно испортить ваши тщательно сконструированные диалоговые окна. Некоторые растровые шрифты абсолютно необходимы для нормальной работы системы, поэтому для предотвращения случайного удаления они хранятся в скрытых файлах. Файлы растровых шрифтов обычно имеют расширение .fon. Они хранятся в 16-разрядном исполняемом формате NE, первоначально использовавшемся в 16-разрядных версиях Windows. В FON-файле хранится текстовая строка с описанием характеристик шрифта. Например, для courf.fon описание имеет вид «FONTRES 100,120,120:Courier 10,12,15(8514/a res)»; в нем содержится имя шрифта, аспектное отношение (100), DPI (120 х 120) и поддерживаемые кегли (10, 12, 15). Каждому кеглю, поддерживаемому растровым шрифтом, соответствует один ресурс растрового шрифта, обычно хранящийся в файле с расширением .fnt. Ресурсы растровых шрифтов могут включаться в итоговый файл растрового шрифта в виде ресурса типа FONT. В Platform SDK входит утилита FONTEDIT, предназначенная для редактирования существующих файлов шрифтовых ресурсов (распространяется с исходным текстом).
Растровые шрифты 759 Несмотря на свою старомодность, ресурсы растровых шрифтов заслуживают внимания, поскольку они дают хорошее представление о том, как проектируются и используются шрифты. Ресурсы растровых шрифтов существуют в двух версиях: версии 2.00, используемой в Windows 2.0, и версии 3.00, предназначавшейся для Windows 3.00. Возможно, вы не поверите, но даже Windows 2000 работает с растровыми шрифтами в формате 2.00. Специфические возможности версии 3.00 были реализованы для шрифтов TrueType. Каждый шрифтовой ресурс начинается с заголовка фиксированного размера, содержащего информацию о номере версии, размере, авторских правах, поддерживаемом разрешении, наборе символов и метриках шрифта. Для шрифтов версии 2.00 поле Version равно 0x200. Младший бит поля Туре для растровых шрифтов равен 1. Каждый шрифтовой ресурс рассчитан на одно стандартное разрешение, но допускает и другие возможные разрешения. На современных мониторах вертикальное разрешение обычно совпадает с горизонтальным — например, 96 х 96 dpi. Высота шрифта кегля 10 пунктов на мониторе с разрешением 96 dpi составляет приблизительно 13 пикселов (10 х 96/72). Ресурс растрового шрифта поддерживает только один однобайтовый набор символов. Он содержит глифы всех символов из интервала, заданного полями FirstChar и LastChar. В каждом шрифтовом ресурсе определяется символ по умолчанию, используемый при выводе символов, не принадлежащих поддерживаемому интервалу (поле Default - Char). Поле BreakChar содержит символ разделителя слов. typedef struct { WORD DWORD CHAR WORD WORD WORD WORD WORD WORD WORD BYTE BYTE ByTE WORD BYTE WORD WORD BYTE WORD WORD BYTE BYTE BYTE BYTE DWORD DWORD DWORD DWORD Version; Size; Copyright[60] Type; Points; VertRes; HorizRes; Ascent; IntLeading; ExtLeading; Italic; Underline; StrikeOut; Weight; CharSet; PixWidth; PixHeight; Family; AvgWidth; MaxWidth; FirstChar; LastChar; DefaultChar; WidthBytes; Device; Face; BitsPointer; BitsOffset; // 0x200 для версии 2.0. 0x300 для версии 3. // Размер всего ресурса // Для растровых шрифтов Туре & 1 == О // Номинальный размер в пунктах // Номинальное вертикальное разрешение // Номинальное горизонтальное разрешение 00 // 0 для пропорционального шрифта // Семейство // Ширина символа 'х' // Максимальная ширина // Первый символ, определенный в шрифте // Последний символ, определенный в шрифте // Замена для символов, не входящих в интервал // Количество байт на строку растра // Смещение строки с именем устройства // Смещение строки с именем гарнитуры // Адрес загруженного растра // Смещение графических данных
760 Глава 14. Шрифты BYTE Reserved; // 1 байт, не используется } FontHeader20; После заголовка ресурса шрифта следует таблица символов (вернее, таблица глифов). Для растровых шрифтов версии 2.0 каждому символу из поддерживаемого интервала в таблице символов соответствует два 16-разрядных целых: для ширины и для смещения глифа. В этом проявляется серьезный недостаток архитектуры шрифтовых ресурсов версии 2.00: из-за 16-разрядного смещения объем ресурса ограничивается 64 килобайтами. Таблица символов содержит (LastChar- FirstChar+2) элементов. Лишний элемент остается пустым. typedef struct { SHORT Glwidth; SHORT Gloffset; } GLYPHINFO_20; Версия 2.00 поддерживала только монохромные глифы. Хотя версия 3.00 рассчитана на поддержку глифов с 16 и 256 цветами и даже глифов в формате True Color, на практике такие шрифты не встречаются. В монохромных глифах для представления одного пиксела достаточно одного бита. С другой стороны, порядок этих битов в глифах не имеет ничего общего с теми растровыми форматами, о которых говорилось выше. Первый байт глифа содержит первые 8 пикселов первой строки развертки, второй байт — первые 8 пикселов второй строки развертки и т. д. до завершения первого столбца из 8 пикселов. Затем следуют данные второго столбца из 8 пикселов, третьего столбца и т. д. до полной ширины глифа. Подобная структура когда-то считалась стандартным элементом оптимизации, ускоряющим вывод символов. Ниже приведена функция для вывода одного глифа растрового шрифта. Функция находит таблицу GLYPHINF0 после заголовка, вычисляет индекс глифа в таблице, а затем преобразует глиф в монохромный DIB-растр и выводит его функциями, предназначенными для работы с DIB. int CharOutCHDC hDC. int x, int y, int ch. KFontHeader20 * pH, int sx=l. int sy=l) { GLYPHINFO_20 * pGlyph = (GLYPHINFO_20 *) ( (BYTE *) & pH-> BitsOffset + 5); if ( (ch<pH->FirstChar) || (ch>pH->LastChar) ) ch = pH->DefaultChar; ch -= pH->FirstChar; int width = pGlyph[ch].Glwidth; int height = pH->PixHeight; struct { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[2]: } dib { { sizeof(BITMAPINFOHEADER). width, -height. 1. 1. BI_RGB }. { { OxFF. OxFF. OxFF. 0 }. { 0. 0. 0. 0 } } int bpl = ( width + 31 ) / 32 * 4;
Растровые шрифты 761 BYTE data[64/8*64]; // Достаточно для 64x64 const BYTE * pPixel = (const BYTE *) pH + pGlyph[ch].GIoffset; for (int i=0; i<(width+7)/8; i++) for (int j=0; j<height; j++) data[bpl * j + i] = * pPixel ++; StretchDIBits(hDC. x, y, width * sx, height * sy. 0. 0, width, height, data. (BITMAPINFO *) & dib. DIB_RGB_COLORS. SRCCOPY); return width * sx; } Если преобразовать шрифтовой ресурс в один из растровых форматов, поддерживаемых GDI, мы сможем выводить символы самостоятельно, без применения текстовых функций GDI. На рис. 14.10 изображены глифы ресурса шрифта MS Sans Serif 8 и 10 пунктов для разрешения 96 dpi. D:\WINNT50\Fonts\SERIFE.FON 8 pts, 96x96 dpi, 0x13 pixel avgw 5, maxw 11, charset 0 !" tt$%8r'()x + .-- /0123456789: ;< = >? ©ABCDEFGHIJKLMN0PQRSTUVWXYZ[\] ~_ 4 abcdef ghi j k 1 minopqrstuvwxyz{| }~| I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I i С i * ¥ | §"©*«— ®~*±гэ'>М- . * ! » УаЫУаЬ kkklLkE^ltttX 1 i I DffDdOOcix0tJtrOu?t>B аааааааздёёёё! i i i И о 6 о о И в ii H ti j> t у 10 pts, 96x96 dpi, 0x16 pixel, avgw 6, maxw 14, charset 0 ! " # $ % h ' ( ) * + , - . / 0 1 2 3 A 5 6 7 8 9 : ; < = > ? ©ABCDEFGHI J KLMNOPQRSTUVWXYZ[ \ ] Л_ s abcdef ghi j kl mnopqr st u v w x у z { | } "I I I I I I I I I I I I I I I I [ I ' I I I I I I I I I I I I I i 0 i * 4 \ § " © * « - - ® * ± г * ' Ц 1 ■ , * ! » У*УгУ* i ААААААддЁЁЁЁ! i t I dn6606o=< 0HOutH aaaaaa«$eeeei i i i &поб6бмвййййу|)у Рис. 14.10. Глифы в растровом формате Из приведенного примера становится ясно, что же такое шрифт. Растровый шрифт в формате Windows 2.00 представляет собой набор шрифтовых ресурсов, разработанных для разных кеглей. Каждый шрифтовой ресурс состоит из монохромных растровых глифов, однозначно отображаемых на символы заданного однобайтового набора. Растровые шрифты поддерживают простое отображение символов набора в индексы глифов в интервале поддерживаемых символов. Глифы легко конвертируются в растровые форматы, поддерживаемые на уровне GDI, и выводятся на графических устройствах. В растровых шрифтах также хранятся простейшие текстовые метрики.
762 Глава 14. Шрифты Растровые шрифты хорошо подходят (как по качеству, так и по быстродействию) для вывода небольших символов на экран; в этом и состоит одна из причин, по которой они еще существуют. Для разных кеглей растровый шрифт должен содержать разные шрифтовые ресурсы. Например, растровые шрифты Windows обычно содержат ресурсы для кеглей 8, 10, 12, 14, 18 и 24 пункта. Для других кеглей или устройств с другим разрешением глифы приходится масштабировать по нужным размерам. Масштабирование растров всегда порождает проблемы, поскольку увеличение приводит к появлению новых пикселов. На рис. 14.11 показан результат масштабирования глифа растрового шрифта. лаА ааААА Рис. 14.11. Масштабирование глифа растрового шрифта В этом примере по обеим осям выполняется целочисленное масштабирование, то есть каждый пиксел глифа просто дублируется нужное количество раз. На рисунке четко видны возникающие дефекты. Масштабирование с дробным коэффициентом может привести к появлению черт разной толщины, поскольку одни пикселы будут дублироваться п раз, а другие — п + 1 раз. Конечно, масштабирование растровых шрифтов не позволяет добиться хорошего качества при выводе на экран и печати. Приходится искать другие способы кодировки шрифтов, обеспечивающие плавное масштабирование без дефектов. Векторные шрифты В растровых шрифтах глифы представляются растровыми изображениями и потому не могут нормально масштабироваться до больших размеров. В другом простом способе представления глиф описывается последовательностью линейных отрезков, которые затем рисуются при помощи пера. Такие шрифты называются векторными. В системе Windows векторные шрифты используют тот же формат шрифтовых ресурсов .fnt и структуру заголовка шрифта. В современных векторных шрифтах поле Version структуры FontHeader20 равно 0x100, а поле Туре равно 1. Главные различия между растровым и векторным шрифтами заключаются в формате данных глифа. В векторном шрифте каждый глиф описывается серией координат, начиная с точки (0,0). При небольших размерах сетки для хранения одной точки достаточно двух байт со знаком. Специальный маркер 0x80 сообщает о начале нового отрезка. Выражаясь более формально, описание векторного глифа в синтаксисе BNF выглядит следующим образом:
Векторные шрифты 763 <векторный_глиф> :: = <отрезок> { <отрезок> } <отрезок> ::= <маркер> { <отн_смещение> <отн_смещение> } <маркер> :: = 0x80 <отн_смещение> :; = <знаковый_байт> Располагая этой информацией, мы можем написать собственную функцию вывода глифов векторных шрифтов средствами GDI: int VectorCharOut(HDC hDC, int x, int y, int ch, const KFontHeader20 * pH. int sx=l, int sy=l) { typedef struct { short offset; short width; } VectorGlyph; const VectorGlyph * pGlyph = (const VectorGlyph *) ( (BYTE *) & pH->BitsOffset + 4); if ( (ch<pH->FirstChar) || (ch>pH->LastChar) ) ch = pH->DefaultChar; else ch -= pH->FirstChar; int width = pGlyph[ch].width; int length = pGlyph[ch+l].offset - pGlyph[ch].offset; signed char * pStroke - (signed char *) pH + pH->BitsOffset + pGlyph[ch].offset; int dx = 0; int dy = 0; while ( length>0 ) { bool move = false; if ( pStroke[0]==-128 ) { move = true; pStroke++; length --; } if ( (pStroke[0]==0) && (pStroke[l]«0) && (pStroke[2]==0) ) break; dx += pStroke[0]; dy += pStroke[l]; if ( move ) MoveToEx(hDC. x + dx*sx. у + dy*sy. NULL); else LineTo(hDC. x + dx*sx, у + dy*sy); pStroke +- 2; length -= 2; } return width * sx;
764 Глава 14. Шрифты В современных версиях ОС Windows используются три векторных шрифта: Roman, Script и Modern. Векторные шрифты обычно занимают меньше места, чем растровые, поскольку отрезки легко масштабируются и для них необходим всего один шрифтовой ресурс. На рис. 14.12 приведена первая половина глифов векторного шрифта Script. D:\WINNT50\Fonts\SCRIPT.FON 36 pts, 2x3 dpi, 0x37 pixel, avgw 17, / 0 1 @A (P£ i Cb p f maxw 33, charset 255 23^56789: ; < = > ? (В С £ £ & t КЗ § X £ МП G (%& и uv wxy $ [ \ ] - _ v- с d <ъ £ а, гь о I hi ггь гь о- *Ъ Л/ t Us О UJ- 00 f f \ I * r^j ♦ Рис. 14.12. Глифы векторного шрифта По сравнению с глифами растровых шрифтов, векторные глифы хорошо масштабируются, хотя с увеличением символа стыки между прямолинейными отрезками становятся все более заметными. На рис. 14.13 показан результат масштабирования глифа А. лА лЛЛЛЛ аЛ, Рис. 14.13. Масштабирование глифа векторного шрифта Толщина использованного на рисунке пера пропорциональна размеру глифа; в GDI глифы векторных шрифтов рисуются пером толщиной один пиксел. Хотя векторные шрифты повышает качество масштабирования при больших размерах символов, они все равно не позволяют выводить высококачественные глифы на графических устройствах высокого разрешения. Для качественного вывода текста применяются шрифты TrueType.
Шрифты TrueType 765 Шрифты TrueType До выхода Windows 3.0 в Windows поддерживались только растровые и векторные шрифты. При масштабировании растровых шрифтов возникали дефекты, а векторные шрифты были слишком тонкими, поэтому ни одна из этих технологий не обеспечивала качественного вывода текста при высоких разрешениях (особенно при печати на принтере). Компания Adobe, обладавшая глубокими технологическими разработками в области языка и шрифтов PostScript, запустила в мир Windows «чужеродное тело» — ATM (Adobe Type Manager). ATM хакерскими приемами вмешивается в работу Windows GDI и позволяет всем приложениям Windows работать с плавно масштабируемыми шрифтами. Рваные края и тонкие линии словно по волшебству заменились ровными, профессиональными глифами, которые одинаково выглядели на экране и на принтере. В Microsoft быстро уловили преимущества новых шрифтовых технологий и, начиная с Windows 3.1, в Windows была внедрена поддержка шрифтовой технологии TrueType компании Apple. В шрифтах TrueType контуры глифа определяются линиями и кривыми, что позволяет масштабировать их до произвольных размеров с сохранением формы глифа. Между шрифтами TrueType и векторными шрифтами существует два главных различия. Во-первых, кривые шрифтов TrueType при масштабировании остаются плавными, а в векторных шрифтах при больших размерах становятся видны пересечения отрезков. Во-вторых, в векторных шрифтах определяются линии, а в шрифтах TrueType определяются контуры глифа. Структура глифа значительно усовершенствуется, поэтому в шрифтах TrueType хранится дополнительная информация, обеспечивающая их преимущества перед старыми шрифтовыми технологиями Windows. Начнем с рассмотрения азов технологии TrueType. Формат файлов шрифтов TrueType Шрифт TrueType обычно хранится в одном файле с расширением .TTF. В операционной системе Windows недавно появилась поддержка шрифтов ОрепТуре, которые представляют собой шрифты PostScript, закодированные в формате, аналогичном TrueType. Файлы шрифтов ОрепТуре имеют расширение .OTF. Технология ОрепТуре также позволяет объединить несколько шрифтов ОрепТуре в один файл. Для таких шрифтов, называемых «коллекциями TrueType», используется расширение .ТТС. Шрифт TrueType кодируется в формате ресурсов контурных шрифтов Macintosh с уникальным тегом «sfnt». Формат ресурсов растровых шрифтов Macintosh (тег «NFNT») в Windows не используется. Шрифт TrueType начинается с небольшого шрифтового каталога с информацией о десятках таблиц, следующих за ним. Шрифтовой каталог содержит номер версии формата шрифта, количество таблиц и одну структуру TableEntry для каждой таблицы. В структуре TableEntry хранится тег ресурса, контрольная сумма, смещение и размер каждой таблицы. Ниже приведено определение шрифтового каталога TrueType на языке С. typedef structs { char tag[4];
766 Глава 14. Шрифты ULONG ULONG ULONG checksum; offset; length; } TableEntry; typedef struct 1 Fixed USHORT USHORT USHORT USHORT sfntversion: numTables; searchRange; entrySelector rangeShift; // 0x10000 для версии 1.0 Tableentry entries[l]; // Переменное количество TableEntry } TableDirectory; Многие программисты Windows даже не знают о том, что шрифты TrueType первоначально разрабатывались компанией Apple для операционных систем, работающих на процессорах Motorola вместо Intel. Во всех данных шрифтов TrueType используется кодировка, при которой старший байт стоит на первом месте. Если шрифт TrueType начинается с 00 01 00 00 00 17, мы знаем, что перед нами ресурс контурного шрифта («sfnt») в формате версии 1.0 с 23 таблицами. В последнем поле структуры TableDi rectory хранится массив структур TableEntry переменной длины, по одной структуре для каждой таблицы в шрифте. Каждая таблица шрифта TrueType содержит логически обособленную информацию — например, данные глифа, отображение символов на глифы, данные кернинга и т. д. Одни таблицы необходимы, присутствие других не обязательно. В табл. 14.3 перечислены самые распространенные таблицы, встречающиеся в шрифтах TrueType. Таблица 14.3. Основные таблицы шрифтов TrueType Тег Название Описание head Заголовок шрифта Глобальная информация о шрифте стар Таблица соответствия между Отображение кодов символов в индексы кодами символов и глифами глифов glyf Таблица глифов Определение контура глифа и инструкции по его размещению в сетке тахр Максимальный профиль Сводные данные шрифта для выделения памяти mmtx Горизонтальные метрики Горизонтальные метрики глифа loca Индексная таблица Преобразование индекса глифа в смещение данных в таблице глифов name Таблица имен Информация об авторских правах, имя шрифта, имя семейства, стиль и т. д. hhea Горизонтальная структура Горизонтальная структура глифов: надстрочный интервал, подстрочный интервал и т. д.
Шрифты TrueType 767 Тег hmtx kern post PCLT OS/2 Название Горизонтальные метрики Таблица кернинга Данные PostScript Данные PCL 5 Метрики, специфические для OS/2 и Windows Описание Полная ширина и левый отступ Массив кернинговых пар Элемент таблицы PostScript Fontlnfo и имена PostScript для всех глифов Данные шрифта для языка принтеров HP PCL 5: номер шрифта, шаг, стиль и т. д. Обязательный набор метрик для шрифта TrueType Все структуры TableEntry в структуре Tab! eDi rectory должны быть отсортированы по именам тегов. Например, структура «стар» должна предшествовать «head», а последняя, в свою очередь, должна располагаться перед структурой «glyf». Расположение самих таблиц в файле шрифта TrueType может быть произвольным. В Win32 API существует функция, при помощи которой приложение может запросить данные шрифта TrueType: DWORD GetFontData(HDC hDC, DWORD dwTable, DWORD dwOffset. LPVOID IpvBuffer, DWORD cbData); Функция GetFontData возвращает информации о шрифте TrueType, соответствующем текущему логическому шрифту, выбранном в контексте устройства, поэтому вместо манипулятора логического шрифта ей передается манипулятор контекста устройства. Вы можете запросить информацию либо обо всем файле TrueType, либо об одной из его таблиц. Чтобы запросить информацию обо всем файле, передайте 0 в параметре dwTable; для получения информации об одной таблице передается ее тег, состоящий из 4 символов, в формате DWORD. Параметр dwOffset содержит начальное смещение таблицы или 0 для всего файла. В параметре IpvBuffer передается адрес, а в параметре cbData — размер буфера. Если в двух последних параметрах передаются NULL и 0, GetFontData возвращает размер шрифтового файла или таблицы; в противном случае данные копируются в буфер, предоставленный приложением. Следующая функция запрашивает служебные данные шрифта TrueType: TableDirectory * GetTrueTypeFont(HDC hDC, DWORD & nFontSize) { // Запросить размер шрифта nFontSize = GetFontData(hDC. 0. 0. NULL. 0); TableDirectory * pFont = (TableDirectory *) new BYTE[nFontSize]; if ( pFont==NULL ) return NULL; GetFontData(hDC. 0. 0. pFont. nFontSize); return pFont; }
768 Глава 14. Шрифты Функция GetFontData ориентирована на приложения, внедряющие шрифты TrueType в свои документы, чтобы их можно было прочитать на другом компьютере, где данный шрифт может отсутствовать. Предполагается, что приложение запрашивает данные шрифта, сохраняет их в составе документа и устанавливает шрифт при открытии документа. В результате документ выглядит так же, как и на том компьютере, где он был создан. Например, спулер Windows NT/ 2000 при печати на сервере внедряет шрифты TrueType в спулинговые файлы, чтобы документ был правильно напечатан на другом компьютере. После получения служебных данных шрифта TrueType анализ заголовочной структуры TableDi rectory не вызовет никаких проблем. Достаточно проверить версию и количество таблиц, после чего можно переходить к проверке отдельных таблиц. Мы рассмотрим самые важные и интересные таблицы. Заголовок шрифта Заголовок шрифта (таблица «head») содержит глобальную информацию о шрифте TrueType. Определение структуры заголовка приведено ниже. typedef struct { Fixed Table; // 0x00010000 для версии 1.0 Fixed fontRevision; // Задается разработчиком шрифта ULONG checkSumAdjustment; ULONG magicNumber; // Равно 0x5F0F3CF5 USH0RT unitsPerEm; // Интервал допустимых значений 16..16384 longDT created; // Дата в международном формате (8 бит) longDT modified; // Дата в международном формате (8 бит) FWord xMin; // Для всех ограничивающих блоков глифов FWord yMin; // Для всех ограничивающих блоков глифов FWord xMax; // Для всех ограничивающих блоков глифов FWord yMax; // Для всех ограничивающих блоков глифов USH0RT macStyle; USH0RT lowestRecPPEM; // Минимальный читаемый размер в пикселах SHORT fontDirecti onHi nt; SHORT indexToLocFormat; // 0 - короткое смещение, 1 - длинное SHORT glyphDataFormat; // 0 для текущего формата } Tablejiead; История шрифта (номер версии, даты создания и последней модификации) хранится в трех полях. Даты хранятся в 8-байтовых полях в виде количества секунд, прошедших с полуночи 1 января 1904 года, поэтому нам никогда не придется беспокоиться о «проблеме Y2K» (и даже «проблеме Y2M»). Шрифт конструируется на эталонной сетке, называемой em-квадратом; глифы шрифта описываются координатами этой сетки. Следовательно, размер эталонной сетки влияет на масштабирование шрифта и его качество. В заголовке шрифта хранятся размеры em-квадрата и данные ограничивающих блоков всех глифов. Размеры em-квадрата могут лежать в интервале от 16 до 16 384, хотя обычно используются значения 2048, 4096 и 8192. Например, для шрифта Wing- ding размер em-квадрата равен 2048, а ограничивающий блок глифа описывается четверкой [0,-432, 2783,1841].
Шрифты TrueType 769 Среди других данных в таблице заголовка шрифта хранится минимальный читаемый размер шрифта в пикселах, хинт направления шрифта, индекс глифа в формате индексной таблицы, формат данных глифа и т. д. Максимальный профиль Шрифт TrueType обладает весьма динамичной структурой. Он может содержать переменное количество глифов, описываемых разным количеством контрольных точек, и неизвестное количество инструкций. Таблица максимального профиля (таблица «тахр») содержит данные о затратах памяти на растеризацию шрифтов, чтобы перед использованием шрифта можно было выделить достаточный объем памяти. Поскольку при растеризации шрифтов важнейшим фактором является быстродействие, динамические структуры вроде массива САггау MFC, нуждающиеся в частом копировании данных, для этого не подходят. Ниже приведена структура, описывающая максимальный профиль шрифта. typedef struct { Fixed Version; // 0x00010000 для версии 1.0 USHORT numGlyphs; // Количество глифов в шрифте USHORT maxPoints; // Макс.кол-во точек в простом глифе USHORT maxContours; // Макс.кол-во контуров в простом глифе USHORT maxCompositePoints; // Макс.кол-во точек в составном глифе USHORT maxCompositeContours; // Макс.кол-во контуров в составном глифе USHORT maxZones: USHORT maxTwilightPoints; USHORT maxStorage; // Количество блоков хранения данных USHORT maxFunctionDefs; // Количество FDEF USHORT maxInstructionDefs; // Количество IDEF USHORT maxStackElements; // Максимальная глубина стека USHORT maxSizeOflnstructions; // Макс.байт в инструкциях глифа USHORT maxComponentElements; // Макс.кол-во компонентов верхнего уровня USHORT maxComponentDepth; // Максимальная глубина рекурсии } Tablejnaxp; В поле numGlyphs хранится общее количество глифов в шрифте, определяющее размер индекса глифа в индексной таблице, а также используемое для проверки индексов. Глифы шрифтов TrueType делятся на составные (composite) и простые (noncomposite). Простой глиф состоит из одного или нескольких контуров, каждый из которых определяется несколькими контрольными точками. Составной глиф определяется как результат объединения других глифов. В полях maxPoints, maxContours, maxCompositePoints и maxCompositeContours хранятся данные о сложности определений глифов. Помимо определений глифов, в шрифтах TrueType используются инструкции для растеризации шрифтов. Инструкции регулируют положение контрольных точек, чтобы растеризованные глифы были сбалансированными и хорошо выглядели. Инструкции глифов также могут храниться на глобальном уровне в программной таблице шрифта («fpgm») и в программной таблице контрольных величин («prep»). Инструкции глифов TrueType пишутся на байт-коде стековой псевдомашины (вроде виртуальной машины Java). Поля maxStackElements
770 Глава 14. Шрифты и maxSizeOflnstructions сообщают стековой машине степень сложности этих инструкций. Пример: шрифт Wingding содержит 226 глифов, максимальное количество контуров в глифе равно 47, а максимальное количество точек в простом глифе равно 268. Составные глифы содержат до 141 точки и 14 контуров. В худшем случае для вывода потребуется 492 уровня в стеке, а самая длинная инструкция состоит из 1119 байт. Отображение символов в индексы глифов Таблица отображения символов в глифы (таблица «стар») определяет соответствие между символами разных кодовых страниц и индексом глифа — ключевой характеристикой для получения информации о глифе в шрифте TrueType. Таблица «стар» может состоять из нескольких подтаблиц для поддержки разных платформ и кодировок символов. Ниже приведено описание структуры таблицы «стар». typedef struct { USHORT Platform; // Идентификатор платформы USHORT EncodinglD; // Идентификатор кодировки ULONG TableOffset; // Смещение таблицы кодировки } submap; typedef struct { USHORT TableVersion; // Версия О USHORT NumSubTable; // Количество таблиц кодировки submap TableHead[l]; // Заголовки таблиц кодировки } Table_cmap; typedef struct { USHORT format; // Формат: О. 2. 4. 6 USHORT length: // Размер USHORT version: // Версия BYTE map[l]; // Данные отображения } Table_Encode; Таблица «стар» (структура Tab1e_cmap) начинается с номера версии, количества подтаблиц и заголовков всех подтаблиц. Каждая подтаблица (структура submap) содержит идентификатор платформы, идентификатор кодировки и смещение данных подтаблицы для заданной платформы и кодировки. В операционных системах Microsoft идентификатор платформы равен 3, а рекомендуемый идентификатор кодировки равен 1 (Unicode). Существуют и другие идентификаторы кодировок — 0 для кодировки Symbol, 2 для Shift-JIS (Japanese Industrial Standard), 3 для Big5 (китайский, традиционное письмо), 4 для PRC (китайский, упрощенное письмо) и т. д. Собственно таблица кодировки (TableJEncode) начинается с полей формата, длины и версии, за которыми следуют данные отображения. В настоящее время определены четыре разных формата таблицы. Формат 0 используется для про-
Шрифты TrueType 771 стой кодировки, позволяющей отображать до 256 символов. В формате 2 используется 8/16-разрядная кодировка для японского, китайского и корейского языков. Формат 4 является стандартным для систем Microsoft. Формат определяет усеченное табличное отображение. Типичный шрифт TrueType, используемый в Windows, содержит две таблицы кодировки — однобайтовую таблицу формата 0 для отображения символов ANSI на индексы глифов и таблицу формата 4 для отображения символов Unicode на индексы глифов. С концептуальной точки зрения таблица отображения представляет собой простую структуру данных, устанавливающую соответствие между парами целых чисел, однако формат 4 слишком сложен, чтобы его можно было описать в нескольких абзацах. При отображении кода символа на индекс глифа по таблице отсутствующие символы отображаются на специальный глиф с индексом 0. Таблица «стар» обычно остается скрытой от приложения, если только вы не захотите получить ее данные функцией GetFontData. В Windows 2000 появились две новые функции, упрощающие доступ к этой информации в приложениях. typedef struct { WCHAR wcLow; USHORT cGlyphs; } typedef struct { DWORD cbThis; // sizeof(GLYPHSET) + sizeof(WCRANGE) * (cRanges-1) DWORD flAccel; DWORD cGlyphsSupported; DWORD cRanges; WCRANGE ranges[l]; // ranges[cRanges]; } GLYPHSET; DWORD GetFontUnicodeRanges(HDC hDC, LPGLYPHSET Ipgs); DWORD GetGlyphlndicesCHDC hDC. LPCTSTR lpstr. int c. LPWORD pgi. DWORD f1); Обычно шрифт содержит глифы лишь для некоторого подмножества символов кодировки Unicode, причем эти символы могут группироваться по интервалам. Функция GetFontUnicodeRanges заносит в структуру GLYPHSET количество поддерживаемых глифов, количество интервалов Unicode и дополнительную информацию об интервалах для текущего шрифта, выбранного в контексте устройства. Структура GLYPHSET имеет переменный размер, зависящий от количества поддерживаемых интервалов Unicode, поэтому функция GetFontUnicodeRanges (как и другие функции Win32 API, поддерживающие структуры переменного размера) обычно вызывается дважды. При первом вызове в последнем параметре передается указатель NULL; GDI возвращает фактический размер структуры. Вызывающая сторона выделяет блок нужного размера и снова вызывает функцию для получения данных. В обоих случаях функция GetFontUnicodeRanges возвращает размер блока, необходимого для хранения всей структуры. В MSDN
772 Глава 14. Шрифты утверждается, что если второй параметр равен NULL, функция GetFontUnicodeTanges возвращает указатель на структуру GLYPHSET. Следующая функция возвращает структуру GLYPHSET для текущего шрифта в контексте устройства. GLYPHSET *QueryUnicodeRanges(HDC hDC) { // Запросить размер DWORD size = GetFontUnicodeRanges(hDC, NULL); if (size==0) return NULL; GLYPHSET * pGlyphSet = (GLYPHSET *) new Byte[size]; // Получить данные pGlyphSet->cbThis = size; size = GetFontUnicodeRanges(hDC. pGlyphSet); return pGlyphSet: } Если вызвать функцию GetFontUnicodeRanges для некоторых шрифтов TrueType системы Windows, выясняется, что эти шрифты часто поддерживают свыше тысячи глифов, сгруппированных по сотням интервалов Unicode. Например, шрифт Times New Roman содержит 1143 глифа в 145 интервалах, первым из которых является интервал 7-разрядных печатных ASCII-кодов 0x20..0x7F. Функция GetFontUnicodeRanges использует лишь часть данных о шрифте TrueType, хранящихся в таблице «стар», — а именно общие сведения об отображении символов Unicode в индексы глифов. Функция GetGlyph Indices выполняет непосредственное преобразование текстовой строки в массив индексов глифов. Она получает манипулятор контекста устройства, указатель на строку, длину строки, указатель на массив WORD и флаг. В массиве WORD сохраняются сгенерированные индексы глифов. Если флаг равен CGIMASKNONEXISTINGGLYPHS, отсутствующие символы заменяются индексом OxFFFF. Индексы глифов, сгенерированные этой функцией, могут передаваться другим функциям GDI — например, функции ExtTextOut. Индексная таблица Конечно, самые важные данные в файле шрифта TrueType хранятся в таблице глифов («glyf»). Для преобразования индекса глифа в смещение данных глифа в таблице используется индексная таблица (таблица «loca»). Индексная таблица содержит п + 1 смещений в таблице глифов, где п — количество глифов, хранящееся в таблице максимального профиля. Дополнительное смещение в конце указывает не на новый глиф, а на конец последнего глифа. Такая структура позволяет шрифтам TrueType обойтись без сохранения длины каждого глифа в шрифте. Вместо этого растеризатор шрифтов вычисляет длину глифа как разность смещений текущего и следующего глифа. Индексы в индексной таблице хранятся в формате unsigned short или unsigned long в зависимости от значения поля indexToLocFormat заголовка шрифта. Глиф должен выравниваться по границе unsigned short; при использовании короткого
Шрифты TrueType 773 формата смещение хранится в таблице в формате WORD вместо BYTE. Это позволяет короткой форме индексной таблицы поддерживать таблицу данных глифов размером до 128 Кбайт. Данные глифов Таблица глифов (таблица «glyf») содержит самую важную информацию во всем шрифте TrueType, поэтому обычно она имеет наибольший размер. Поскольку данные о соответствии между индексами и глифами хранятся в отдельной таблице, таблица данных глифов не содержит ничего, кроме последовательности глифов, каждый из которых начинается со структуры заголовка глифа. typedef struct { WORD numberOfContours: // Число контуров; <0 для составных глифов Fword xMin; // Минимальное значение х для координат Fword yMin; // Минимальное значение у для координат Fword xMax; // Максимальное значение х для координат Fword yMax; // Максимальное значение у для координат } GlyphHeader; Для простых (не составных) глифов поле numberOfContours содержит количество контуров в текущем глифе; для составных глифов поле numberOfContours отрицательно. В последнем случае общее количество контуров вычисляется по данным всех глифов, образующих составной глиф. В следующих четырех полях структуры GlyphHeader хранится ограничивающий блок глифа. Для простых глифов за структурой GlyphHeader следует описание глифа. Описание состоит из нескольких значений: индексов конечных точек всех контуров, инструкций и последовательности контрольных точек. Для каждой контрольной точки указываются координаты (х,у) и флаг. Теоретически для задания контрольных точек достаточно той же информации, что и для функции PolyDraw GDI: массива флагов и массива координат. Впрочем, в шрифтах TrueType контрольные точки кодируются весьма изощренным способом. Ниже приведено обобщенное описание глифа. USH0RT USH0RT BYTE BYTE BYTE BYTE endPtsOfContoursCn]: instructionlength; instruction[i]; flags[]; xCoordinates[]; yCoordinates[]; // n = количество контуров // i = длина инструкции // переменный размер // переменный размер // переменный размер Глиф может содержать один или несколько контуров. Например, буква «о» содержит два контура, внутренний и наружный. Для каждого контура в массиве endPtsOfContours хранится индекс конечной точки, по которому вычисляется количество точек в контуре. Например, endPtsOfContours[0] — количество точек в первом контуре, a (endPtsOfContours[l] - endPtsOfContours[0]) — количество точек во втором контуре. За массивом конечных точек следует длина инструкции глифа и массив инструкций. Впрочем, мы сначала разберемся с контрольными точками. Контрольные точки глифа хранятся в трех массивах: флагов, координат х и координат у. Начало массива флагов находится просто, однако не существует ни поля разме-
774 Глава 14. Шрифты pa массива флагов, ни прямых ссылок на два других массива. Чтобы найти массивы координат и разобраться с ними, вам придется декодировать массив флагов. Выше уже упоминалось, что максимальный размер em-квадрата равен 16 384 единицам, поэтому обычно каждая из координат х и у представляется двумя байтами. Для экономии места (а это главная причина для выбора этого способа кодировки) в описании глифа хранятся относительные координаты. Координаты первой точки задаются относительно (0,0); для всех остальных точек хранятся смещения относительно предыдущей точки. У одних точек эти смещения оказываются достаточно малыми, что позволяет представить их одним байтом; у других точек они равны 0, а у третьих они не помещаются в одном байте. В массиве флагов хранится информация о кодировке отдельных точек в сочетании с другой информацией. Ниже показано, как интерпретируются отдельные биты флагов. typedef enum G ONCURVE G_REPEAT G XMASK G XADDBYTE G XSUBBYTE G XSAME GJADDINT G YMASK G YADDBYTE G YSUBBYTE G YSAME - 0x01, - 0x08. - 0x12, - 0x12. - 0x02. - 0x10. - 0x00. - 0x24. - 0x24. - 0x04. - 0x20. G YADDINT - 0x00. // На кривой или вне кривой // Следующий байт содержит счетчик повторений // X - положительный байт // X - отрицательный байт // X совпадает с прежним значением // X - слово со знаком // Y - положительный байт // Y - отрицательный байт // Y совпадает с прежним значением // Y - слово со знаком }: В главе 8, посвященной линиям и кривым, упоминалось, что сегмент кубической кривой Безье определяется четырьмя точками: начальной точкой кривой, двумя контрольными точками, лежащими за пределами кривой, и конечной точкой кривой. Контур глифа в шрифте TrueType описывается кривой Безье второго порядка, определяемой двумя концами кривой и одной контрольной точкой. Несколько контрольных точек могут стоять подряд — не для того, чтобы определить кубическую или другую кривую Безье более высокого порядка, а просто для сокращения количества контрольных точек. Например, в последовательность четырех точек «точка кривой — контрольная — контрольная — точка кривой» между двумя контрольными точками неявно добавляется еще одна точка кривой, в результате чего данная последовательность определяет два сегмента кривых Безье второго порядка. Если бит G_0NCURVE установлен, точка находится на кривой; в противном случае она является контрольной точкой, лежащей за пределами кривой. Если установлен бит G_REPEAT, следующий байт массива флагов содержит счетчик повторений, а текущий флаг повторяется заданное количество раз (некая разновидность сжатия RLE в массиве флагов). Остальные биты флагов описывают кодировку соответствующих координат х,у; они показывают, совпадает ли относительная координата с предыдущей, кодируется ли положительным или отрицательным байтом или же требует двухбайтовой величины со знаком.
Шрифты TrueType 775 Описание глифа расшифровывается за два прохода. На первом проходе мы перебираем массив флагов, находим его конец и определяем длину массива координат х; в результате определяются начальные точки массивов х и у. На втором проходе мы перебираем каждую точку определения глифа и преобразуем ее к более удобному формату. В листинге 14.2 приведена функция расшифровки глифа TrueType, оформленная в виде метода класса TrueType. Листинг 14,2, KTrueType::DecodeGlyph: расшифровка простого глифа int «TrueType::Decodedyphdnt index, «Curve & curve. XFORM * xm) const { const GlyphHeader * pHeader = GetGlyph(index); if ( pHeader—NULL ) return 0; intnContour - (short) reverse(pHeader->numberOfContours); if ( nContourO ) // Составной глиф f return DecodeCompositeGlyph(pHeader+l. curve); // После заголовка if ( nContour==0 ) return 0; curve.SetBound(reverse((WORD)pHeader->xMin), reverse((WORD)pHeader->yMin), reverse((WORD)pHeader->xMax), reverse((WORD)pHeader->yMax)); const USHORT * pEndPoint - (const USHORT *) (pHeader+1); // Всего точек: конец последнего контура + 1 int nPoints = reverse(pEndPoint[nContour-l]) + 1; // Длина инструкций int nlnst = reverse(pEndPoint[nContour]); // Массив флагов: после массива инструкций const BYTE * pFlag - (const BYTE *) & pEndPoint[nContour] + 2 + nlnst; const BYTE * pX = pFlag; int xlen = 0; // Проанализировать массив флагов для определения // начальной позиции и размера массива координат х for (int i=0: i<nPoints; i++. pX++) { int unit = 0; switch ( pX[0] & GJMASK ) { case GJADDBYTE: case GJSUBBYTE: unit = 1: Продолжение^
776 Глава 14. Шрифты Листинг 14.2. Продолжение break; case GJADDINT: unit = 2; } if ( pX[0] & G_REPEAT ) { xlen += unit * (pX[l]+l): i += pX[l]; pX ++: } else xlen += unit; } const BYTE * pY = pX + xlen;// Массив координат у следует // после массива координат х int х = 0; int у = 0; i = 0; BYTE flag - 0; int rep =0; // Одновременный перебор всех трех массивов for (int j=0; j<nContour; j++) // По одному контуру { int limit = reverse(pEndPoint[j]); while ( i<=limit ) { if ( rep==0 ) { flag = * pFlag++; rep = 1; if ( flag & G_REPEAT ) rep += * pFlag ++; } int dx = 0, dy = 0; switch ( flag & GJMASK ) { case GJADDBYTE: dx - pX[0]: pX ++; break; case GJSUBBYTE; dx = - pX[0]; pX ++; break; case GJADDINT; dx - (short )( (pX[0] « 8) + pX[l]); pX+=2; } switch ( flag & GJMASK ) {
Шрифты TrueType 777 case GJADDBYTE case G_YSUBBYTE case GJADDINT: pY+=2; } x += dx; У += dy; assert(abs(x)<16384); assert(abs(y)<16384); if ( xm ) // Применить преобразование, если оно задано curve.Add((int) ( х * xm->eMll + у * xm->eM21 + xm->eDx ), (int) ( x * xm->eM12 + у * xm->eM22 + xm->eDy ), (flag & G_0NCURVE) ? KCurve::FLAG_0N : 0); else curve.Add(x, y. (flag & GJJNCURVE) ? KCurve::FLAG_0N : 0); rep --: i ++; } curve. CloseO; } return curve.GetLengthO; } Класс KTrueType загружает и расшифровывает шрифты TrueType; его полный код находится на прилагаемом компакт-диске. Метод DecodeGlyph выполняет расшифровку одного глифа по индексу и необязательной матрице преобразования. Параметр класса KCurve предназначен для сбора определения глифа в простой 32-разрядный массив точек и простой массив флагов, которые затем легко выводятся средствами GDI. На основе этого метода даже можно построить простейший редактор шрифтов TrueType. Программа вызывает метод GetGlyph, который по индексной таблице находит структуру GlyphHeader заданного глифа. Из таблицы извлекается количество контуров в глифе. Обратите внимание на перестановку байтов в полученной величине, связанную с обратным порядком следования байтов в шрифтах TrueType. Если значение отрицательно (признак составного глифа), вызывается метод DecodeCompositeGlyph. Затем программа находит массив endPtsOfContours, определяет общее количество точек и пропускает инструкции, переходя к началу массива флагов. Теперь мы должны определить начальную точку массива координат х и длину массива однократным перебором массива флагов. Каждая точка может занимать в массиве координат от 0 до 2 байт в зависимости от того, представляется ли ее относительное смещение 0, одно- или двухбайтовой величиной. По адресу и длине массива координат х определяется адрес массива координат у. Затем программа последовательно перебирает все контуры, расшифровывает данные всех точек, преобразует относительные координаты в абсолютные и затем прибавляет точку к объекту кривой, применяя к ней преобразование (если оно было задано). dy = pY[0]: pY ++; break; dy = - pY[0]; pY ++; break; dy = (short )( (pY[0] « 8) + pY[l]);
778 Глава 14. Шрифты Как говорилось выше, в шрифтах TrueType используются кривые Безье второго порядка, причем между двумя точками кривой может находиться несколько контрольных точек. Чтобы упростить алгоритм вывода кривой, метод KCurve: :Add добавляет лишнюю точку кривой между каждой парой контрольных точек. void AddCint x. int у, BYTE flag) { if ( mjen && ( (flag & FLAG_ON)==0 ) && ( CmJlagOnJen-l] & FLAG__ON)==0 ) ) { Append((m_Point[mJen-l].x+x)/2, (m_Point[mJen-l].y+y)/2. FLAG_0N | FLAGJEXTRA); // Добавить промежуточную точку } Append(x, у. flag); } Разобравшись с простыми глифами, перейдем к составным. Составной глиф определяется последовательностью преобразованных глифов. Каждое определение преобразованного глифа состоит из трех частей: флагов, индекса глифа и матрицы преобразования. Поле флагов описывает кодировку матрицы преобразования (также спроектированную для экономии памяти), а также содержит признак конца последовательности. Полное двумерное аффинное преобразование определяется шестью величинами. Впрочем, для простого смещения достаточно всего двух величин (dx, dy), которые могут храниться в двух байтах или двух словах. Если одновременно со смещением значения хиу масштабируются в одинаковой пропорции, коэффициент масштабирования можно хранить всего в одном экземпляре. В общем случае используются все шесть величин, но в большинстве конкретных ситуаций несколько байт удается сэкономить. Параметры преобразования хранятся в формате 2.14 с фиксированной точкой; исключением являются параметры dx и dy, хранящиеся в виде целых чисел. Составной глиф строится объединением нескольких глифов, каждому из которых сопоставляется матрица преобразования. Например, если глиф представляет собой точное зеркальное отражение другого глифа, он может быть определен как составной глиф, сгенерированный в результате применения зеркального отражения к другому глифу. В листинге 14.3 приведен код расшифровки составных глифов. Листинг 14.3. KTrueType::DecodeCompositeGlyph int «TrueType::DecodeCompositeGlyph(const void * pGlyph, KCurve & curve) const { KDataStream str(pGlyph); unsigned flags; int 1 en - 0; do { flags * str.GetWordO; unsigned glyphlndex - str.GetWordO;
Шрифты TrueType 779 signed short argumenti; signed short argument2; if ( flags & ARG_1_AND_2_ARE_W0RDS ) { argumenti - str.GetWordO; // (SHORT or FWord) argumenti; argument2 - str.GetWordO; // (SHORT or FWord) argument2; } else { argumenti - (signed char) str.GetByteO; argument2 - (signed char) str.GetByteO: } signed short xscale, yscale, scaled. scalelO; xscale - 1; yscale - 1; scaleOl - 0; scalelO - 0; if ( flags & WE_HAVE_A_SCALE ) { xscale - str.GetWordO; yscale - xscale; // Формат 2.14 } else if ( flags & WE_HAVE_AN_X_AND_Y_SCALE ) { xscale - str.GetWordO; yscale - str.GetWordO; } else if ( flags & WE_HAVE_A_M)_BY_TWO ) { xscale - str.GetWordO; scaleOl - str.GetWordO; scalelO - str.GetWordO; yscale « str.GetWordO; if ( flags & ARGS_ARE_XY VALUES ) { XFORM xm; xm.eDx - (float) argumenti; xm.eDy - (float) argument2; xm.eMll - xscale / (float) 16384.0; xm.eM12 - scaleOl / (float) 16384.0; xm.eM21 - scalelO / (float) 16384.0; xm.eM22 - yscale / (float) 16384.0; len +« DecodeGlyph(glyphIndex, curve, & xm); } else assert(false); Продолжение £
780 Глава 14. Шрифты Листинг 14,3, Продолжение while ( flags & MORE_COMPONENTS ); if ( flags & WE_HAVE_INSTRUCTIONS ) // Пропустить инструкции { unsigned numlnstr = str.GetWordO: for (unsigned i=0: i<numlnstr; i++) str.GetByteO; } return Ten; } Метод DecodeCompositeGlyph получает флаги, индекс глифа и матрицу преобразования для каждого глифа, входящего в составной глиф, и расшифровывает глиф при помощи метода DecodeGlyph. Обратите внимание на передачу матрицы преобразования при вызове DecodeGlyph. Метод завершает работу, обнаружив сброшенный флаг M0REC0MP0NENTS. Полный код находится на прилагаемом компакт- диске. Расшифрованные глифы шрифтов TrueType можно было бы легко вывести средствами GDI, если бы не одна маленькая проблема. GDI рисует только кубические кривые Безье, поэтому контрольные точки кривых Безье второго порядка, полученные из таблицы глифов, необходимо преобразовать в контрольные точки кубических кривых Безье. Немного повозившись с исходным математическим определением кривых Безье, мы приходим к простой функции вывода кривых Безье второго порядка средствами GDI: // Вывод сегмента кривой Безье 2-го порядка BOOL Bezier2(HDC hDC. int & xO. int & yO. int xl. int yl, int x2. int y2) { // pO pi p2 -> pO (p0+2pl)/3 (2pl+p2)/3. p2 POINT P[3] - { { (x0+2*xl)/3. (y0+2*yl)/3 }, { (2*xl+x2)/3. (2*yl+y2)/3 }, { x2. У2 } }; xO « x2; yO = y2; return PolyBezierTo(hDC.P.3); } Для кривой Безье второго порядка, определяемой тремя точками (р0, ри р2), соответствующие точки кубической кривой Безье вычисляются по формулам (Ро> (Ро +2 х Pl)/3, (2 х Pl + р2)/3, р2). На рис. 14.14 показан результат применения кода, реализованного в классе KCurve. На заднем плане изображен em-квадрат, разделенный сеткой на 16 частей по обеим осям. Прямоугольник представляет ограничивающий блок глифа, в данном примере — символа @. Точки обозначены маленькими кружками. Как видно из рисунка, точки на линии чередуются с контрольными точками. Но самое важное — то, что построенные кривые соответствуют контуру, определяемому сложным описанием шрифта.
Шрифты TrueType 781 Рис. 14.14. Описание глифа TrueType Инструкции глифа При просмотре листингов 14.2 и 14.3 может возникнуть впечатление, что растеризатор шрифтов TrueType легко реализуется преобразованием контуров глифов — скажем, заполнением траектории, которая создается при выводе контуров, функцией GDI StrokeFinAndPath. Такой примитивный растеризатор шрифтов вряд ли принесет какую-нибудь практическую пользу, разве что на устройствах высокого разрешения (например, на принтерах). Рисунок 14.15 поможет вам убедиться в этом. Рис. 14.15. Растеризация глифов На рисунке сравниваются два варианта растеризации глифов TrueType: простейший растеризатор из листингов 14.2 и 14.3 и настоящий механизм растери-
782 Глава 14. Шрифты зации глифов для шрифтов TrueType операционных систем Microsoft. Сверху показан результат применения простейшего растеризатора, а снизу — то, что реализует ОС. Результаты приведены как в исходном размере, так и в увеличении. В правой части рисунка изображены контуры глифов TrueType, аппроксимируемые обеими реализациями. Как видно из рисунка, простейший растеризатор создает изображения с разной толщиной линий, выпадением пикселов, потерей элементов изображения, утратой симметрии и т. д. При уменьшении кегля результат становится еще хуже. Масштабирование контура глифа, определенного на большом em-квадрате (обычно 2048 единиц), в сетку меньшего размера (скажем, 32 х 32) неизбежно приводит к потере точности и появлению ошибок. Допустим, в единицах em- квадрата определяются две вертикальные черты с ограничивающими блоками [14,0,25,200] и [31,0,42,200]; обе черты обладают одинаковыми размерами Их 200. Все выглядит замечательно, но давайте попробуем уменьшить изображение 10 раз с округлением. Первая черта масштабируется в блок [1,0,3,20], а вторая — в блок [3,0,4,20]. Обратите внимание: размеры первой черты теперь равны 2 х 20, а размеры второй — 1 х 20. Именно так возникают черты разной толщины. Посмотрите на рисунок — нижняя черта буквы В толще средней и верхней. В технологии TrueType проблемы растеризации решаются путем управления масштабированием контура из сетки em-квадрата в итоговую сетку, чтобы результат лучше выглядел и сохранял сходство с исходным дизайном глифа. Эта методика, называемая подгонкой по сетке, имеет три основные цели. О Устранение случайных зависимостей от расположения контуров в сетке, чтобы при растеризации одинаковая толщина линий сохранялась независимо от их расположения в сетке. О Сохранение ключевых размеров внутри одного глифа и между разными глифами. О Сохранение симметрии и других важных аспектов глифа (например, засечек). Соответствующие требования для шрифта TrueType кодируются в двух местах: в таблице контрольных величин и в инструкциях подгонки по сетке, задаваемых на уровне отдельных глифов. Таблица контрольных величин («cvt») предназначена для хранения массива, элементы которого могут использоваться в инструкциях. Например, для шрифта с засечками в числе контролируемых параметров могут быть высота засечки, ширина засечки, толщина черт прописной буквы и т. д. Эти значения заносятся в таблицу контрольных величин в порядке, известном разработчику шрифта, и позднее инструкции ссылаются на них по индексам. В процессе растеризации шрифтов значения таблицы контрольных величин масштабируются в соответствии с текущим кеглем. Использование масштабированных величин в инструкциях гарантирует, что будут применяться одни и те же значения независимо от их относительной позиции в сетке. Например, если горизонтальную черту [14,0,25,200] задать в виде [14,0,14+CVT[stem_width],0+CVT[stem_height]] с использованием двух значений из таблицы CVT, то ширина и высота останутся постоянными при любом расположении линии в сетке. С каждым определением глифа связывается серия инструкций, называемых инструкциями глифа и управляющих подгонкой глифа по сетке. Ссылки на па-
Шрифты TrueType 783 раметры из контрольной таблицы в инструкциях глифов гарантируют, что эти параметры будут выдерживаться во всех глифах. Инструкции глифов предназначены для стековой псевдомашины. Стековая машина широко используется в интерпретируемых средах из-за простоты своей реализации. В частности, Forth (простой и мощный язык встраиваемых систем), RPL (язык научных калькуляторов HP) и виртуальная машина Java построены на базе стековых машин. Стековая машина обычно не имеет регистров, поскольку все вычисления производятся в стеке (у некоторых стековых машин контрольный стек отделен от стека данных). Например, инструкция PUSH заносит значение в стек, инструкция POP удаляет из стека верхний элемент, а инструкция бинарного сложения удаляет из стека два верхних элемента и заносит в стек их сумму. Виртуальная машина TrueType не относится к числу стековых машин общего назначения. Это специализированная псевдомашина, предназначенная для единственной цели — подгонки контуров глифов по сетке. Кроме значений из таблицы контрольных величин, она использует несколько переменных графического состояния (эталонная точка 0, эталонная точка 1, вектор проекции и т. д.). Мы не будем рассматривать весь набор инструкций глифов TrueType. Вместо этого базовые принципы будут продемонстрированы на простом примере буквы «Н» из шрифта Tahoma. Контур глифа изображен на рис. 14.16. Иомп Рис. 14.16. Контур буквы «Н» шрифта Tahoma Буква Н шрифта Tahoma состоит из одного контура с 12 контрольными точками, которые все расположены на линии; другими словами, в данном глифе кривые Безье отсутствуют. Помимо точек глиф содержит 50 байт инструкций, которые занимают больше места, чем координаты. Ниже приведен список координат и инструкций глифа. Координаты 0: 1232. О 1: 1034, 0
784 Глава 14. Шрифты 2 3 4 5 6 7 8 9 К 1 Длина 1034, 349. 349, 151, 151, 349, 349. 1034, ): 1034, L: 1232. 729 729 0 0 1489 1489 905 905 1489 1489 инструкций: 50 00: NPUSHB (28): 3 53 8 8 5 10 7 3 8 9 2 20 0 101 13 64 13 2 8 3 100 12 30: SRP0 31: MIRP[srpO,nmd.rd,2] 32: MIRP[srpO,md,rd,l] 33: SHP[rp2,zpl] 34: DELTAP1 35: SRPO 36: MIRP[srp0.nmd.rd,2] 37: MIRPEsrpO.md.rd.l] 38: SHP[rp2,zpl] 39: SVTCA[y-axis] 40: MIAP[rd+ci] 41: ALIGNRP 42: MIAP[rd+ci] 43: ALIGNRP 44: SRP2 45: IP 46: MDAP[rd] 47: MIRP[nrpO,md.rd.l] 48: IUP[y] 49: IUP[x] 50 байт инструкций глифа разделены на 21 инструкцию. Большинство инструкций (кроме первой) состоит из одного байта. У каждой инструкции имеется мнемоническое название, набор флагов в квадратных скобках и ряд дополнительных параметров. Давайте последовательно рассмотрим все инструкции. 1. Инструкция NPUSHB (занести N байт в стек) заносит в стек заданное количество байт. В данном примере в стек заносятся 28 байт из потока инструкций. Верхний элемент стека равен 12. 2. Инструкция SRP0 (установить эталонную точку 0) извлекает значение 12 из стека и назначает контрольную точку 12 эталонной точкой 0. Контрольная точка 12 соответствует базовой точке em-квадрата. 3. Инструкция MIRP[srp0,nmd,rd,2] (относительное перемещение эталонной точки) извлекает из стека значения 100 и 5 и перемещает точку 5 так, чтобы ее расстояние от эталонной точки 0 было равно CVT[100] (элемент таблицы
Шрифты TrueType 785 контрольных величин с индексом 100). Эта инструкция привязывает крайнюю левую точку глифа к заданному расстоянию от базовой точки по оси х. 4. Инструкция MIRP[srpO,md,rd,l] (относительное перемещение эталонной точки) извлекает из стека значения 20 и 3 и перемещает точку 3 относительно точки 5 в соответствии со значением CVT[20]. Тем самым обеспечивается фиксированная ширина горизонтальной черты. 5. Инструкция SHP[rp2,zpl] (сдвинуть точку по эталонной точке) извлекает из стека значение 8 и сдвигает точку 8 на то же расстояние, на которое была сдвинута эталонная точка (точка 3). 6. Инструкция DELTAP1 (дельта-исключение Р1) извлекает из стека значения 2, 13, 64, 13 и 15 и создает исключения со значением 13 в точках 64 и 15. В результате заданные точки перемещаются на величину, определяемую парными величинами (13). В данном случае номера точек, похоже, неверны. 7. Инструкция SRP0 (установить эталонную точку 0) извлекает значение 13 из стека и назначает контрольную точку 13 эталонной точкой 0. Точка 13 является автоматически добавляемой точкой, расстояние которой от базовой точки em-квадрата (точка 12) равно полной ширине глифа. 8. Инструкция MIRP[srpO,nmd,rd,2] (относительное перемещение эталонной точки) извлекает из стека значения 101 и 0 и перемещает точку 0 относительно точки 13 со смещением CVT[101]. Кроме того, эталонная точка 0 перемещается в точку 0. 9. Инструкция MIRP[srpO,nmd,rd,2] (относительное перемещение эталонной точки) извлекает из стека значения 20 и 2 и перемещает точку 2 относительно точки 0 со смещением CVT[20]. 10. Инструкция SHP[rp2,zpl] (сдвинуть точку по эталонной точке) извлекает из стека значение 9 и сдвигает точку 8 на то же расстояние, на которое была сдвинута эталонная точка (точка 2). 11. Инструкция SVTCA[y-axis] перемещает проекционный вектор на ось у. Подгонка по оси х закончена, мы переходим к оси у. 12. Инструкция MIAP[rd+ci] извлекает из стека значения 8 и 15 и перемещает точку 5 в абсолютную позицию CVT[8] = 0. 13. Инструкция ALIGNRP (выровнять по эталонной точке) извлекает из стека значение 1 и выравнивает точку 1 по эталонной точке 0 (точка 5). 14. Инструкция MAIP[rd+ci] извлекает из стека значения 3 и 7 и перемещает точку 7 в абсолютную позицию CVT[3] = 1489. Это гарантирует однозначное определение высоты буквы Н. 15. Инструкция ALIGNRP (выровнять по эталонной точке) извлекает из стека значение 10 и выравнивает точку 10 по эталонной точке 0 (точка 7). 16. Инструкция SRP2 (установить эталонную точку 2) извлекает значение 15 из стека и назначает контрольную точку 5 эталонной точкой 2. 17. Инструкция IP (интерполировать точку) извлекает из стека значение 8 и интерполирует позицию точки 8 с учетом исходного отношения между эталонными точками (5 и 10).
786 Глава 14. Шрифты 18. Инструкция MDAP[rd] (абсолютное перемещение эталонной точки) извлекает из стека значение 8, устанавливает эталонные точки 0 и 1 в точку 8 и округляет точку 8. 19. Инструкция MIRP[nropO,md,rl,l] (относительное перемещение эталонной точки) извлекает из стека значения 53 и 3 и перемещает точку 3 относительно точки 80 со смещением CVT[53]. 20. Инструкция IUP[y] интерполирует остальные точки контура в направлении оси у. 21. Инструкция ШР[х] интерполирует остальные точки контура в направлении осих Впрочем, это всего лишь упрощенное описание инструкций простейшего глифа. Полный набор инструкций глифов TrueType и их семантика — гораздо более сложная тема. Существует более 100 различных инструкций, 20 переменных графического состояния и несколько типов данных. За полной информацией обращайтесь к руководству «TrueType Reference Manual» на сайте fonts.apple.com. Горизонтальные метрики Информация, хранящаяся в таблице данных глифа, недостаточна для горизонтального выравнивания последовательности глифов, образующих строку текста, или вертикального выравнивания строк абзаца. Базовые метрики латинских шрифтов TrueType хранятся в двух таблицах: таблице горизонтальной структуры и таблице горизонтальных метрик (таблицы «hhea» и «htmx»). Прежде чем рассматривать эти таблицы, необходимо познакомиться с некоторыми шрифтовыми метриками, показанными на рис. 14.17. Подстрочный интервал ^ Левый / отступ Высота строки Базовая линия \ Правый отступ Рис. 14.17. Метрики глифа Надстрочным интервалом (ascent) называется расстояние от верхней границы прописных букв до базовой линии. Надстрочный интервал является атрибу-
Шрифты TrueType 787 том всего шрифта, а не отдельного глифа. Эта метрика определяет положение базовой линии текстовой строки от начальной позиции вывода. Подстрочный интервал (descent) также является атрибутом шрифта и определяет расстояние от базовой линии до нижней границы подстрочных элементов (в таких глифах, как Q, q или g). Сумма подстрочного интервала с надстрочным образует высоту строки шрифта, хотя при выводе абзацев могут использоваться дополнительные междустрочные интервалы. У каждого глифа имеется ограничивающий блок, в шрифтах TrueType являющийся частью заголовка глифа. Ограничивающий блок описывается четверкой [xmin,ymin,xmax,ymax], то есть минимальными и максимальными координатами контрольных точек глифа. По горизонтали между базовой точкой и позицией xmin глифа обычно существует небольшой зазор, который называется левым отступом. После размещения глифа в строке базовая точка следующего глифа смещается от позиции хтах на расстояние, называемое правым отступом. Левый и правый отступы, как и ограничивающий блок, относятся к числу атрибутов отдельных глифов. Сумма левого отступа, ширины глифа (хтах - xmin) и правого отступа называется полной шириной. Полная ширина определяет горизонтальное смещение базовой точки после размещения глифа в строке. Следующий глиф выводится от новой базовой точки. Значения левого и правого отступов обычно положительны, что соответствует разделению глифов дополнительными промежутками. Впрочем, иногда они бывают отрицательными для сближения глифов. Например, в шрифте Times New Roman строчная буква «j» имеет отрицательный левый отступ, а строчная буква «f» имеет отрицательный правый отступ. На рис. 14.18 изображена буква «F» курсивного шрифта с отрицательными отступами с обеих сторон. Рис. 14.18. Глиф с отрицательными значениями левого и правого отступов В шрифтах TrueType такие атрибуты, как надстрочный и подстрочный интервалы шрифта, хранятся в горизонтальной заголовочной таблице, а данные уровня глифа (такие, как левый отступ и полная ширина) — в таблице горизонтальных метрик.
788 Глава 14. Шрифты Определение структуры горизонтальной заголовочной таблицы («hhea») выглядит следующим образом: typedef struct Fixed FWord FWord FWord FWord FWord FWord FWord SHORT SHORT SHORT SHORT version; Ascender; Descender; LineGap; advanceWidthMax; minLeftSideBearing; minRightSideBearing xMaxExtent; caretSlopeRise; caretSlopeRun; reserved[5]; metricDataFormat; USHORT numberofHMetrics; } TableJHoriHeader; В горизонтальной заголовочной таблице («hhea») хранятся надстрочные и подстрочные интервалы шрифта, междустрочный промежуток, максимальная полная ширина, минимальный левый и правый отступы, максимальные габариты (левый отступ + хтах - xmin), хинты для вывода каретки и информация о таблице горизонтальных метрик. В таблице горизонтальных метрик (таблица «htmx») хранится информация горизонтальных метрик уровня глифа. Для каждого глифа должен существовать способ получения левого отступа и полной ширины, по которым правый отступ вычисляется по формуле «полная ширина - левый отступ - (хтах - xmin)». Впрочем, для моноширинных шрифтов с постоянным значением полной ширины хранение нескольких копий полной ширины считается расточительством, поэтому таблица горизонтальных метрик делится на две части: в первой части хранится полная ширина и левый отступ каждого глифа, а во второй — только левые отступы. В таблице должны содержаться сведения обо всех глифах шрифта; количество глифов, имеющих полные горизонтальные метрики, хранится в последнем поле горизонтальной заголовочной таблицы (поле numberOfHMetrics). Ниже приведено описание структуры таблицы горизонтальных метрик. Обратите внимание: обе части представляют собой массивы переменной длины. typedef struct { FWord advanceWidth; FWord lsb; } IngHorMetric; typedef struct { longHorMetric hMetrics[l]; // numberOfHMetrics; FWord leftSideBearing[l]; // С предыдущим advanceWidth } Table_HoriMetrics; Данные горизонтальных метрик шрифта можно получить средствами GDI при помощи функций GetCharABCWidths, GetCharABCWidthsFloat и GetCharABCWidthsI. В терминологии GDI левый отступ называется метрикой А, хтах - xmin назы- // 0x00010000 для версии 1.0 // Типографский надстрочный интервал // Типографский подстрочный интервал // Типографский междустрочный интервал // Максимальная полная ширина // Минимальный левый отступ ; // Минимальный правый отступ // Мах(левый отступ + (хМах - xMin)) // Наклон курсора // 0 для вертикального положения. // Присваивается 0. // 0 означает текущий формат // элементы hMetric в таблице 'htmx'
Шрифты TrueType 789 вается метрикой В, а правый отступ — метрикой С. Мы рассмотрим эти функции в следующей главе при знакомстве с форматированием текста, поскольку эти функции в большей степени связаны с логическими шрифтами GDI, нежели с физическими шрифтами TrueType. Кернинг При размещении глифов в строке используются параметры левого и правого отступов, улучшающие ее внешний вид, однако для каждого глифа эти атрибуты являются постоянными величинами. Когда два конкретных глифа находятся по соседству, из-за особенностей их формы эти глифы иногда должны располагаться ближе или дальше друг от друга. Регулировка интервалов между определенными парами глифов называется кернингом. Благодаря кернингу сочетания этих глифов выглядят более естественно. В режиме TrueType данные кернинга берутся из таблицы, созданной разработчиком шрифта. Ниже приведены структуры таблицы кернинга (таблица «kern») для шрифтов TrueType. typedef struct { FWord FWord FWord } KerningPair; typedef struct { FWord FWord FWord FWord FWord FWord FWord FWord FWord KerningPair leftglyph; rightglyph: move; Version; nSubTables; SubTableVersion; Bytesinsubtable; Coveragebits; Numberpairs; SearchRange; EntrySelector; RangeShift; KerningPair[lJ: // Переменный размер } TableJCerning; Таблица кернинга имеет довольно простую структуру — она состоит из заголовка и простого массива структур KerningPair; каждая структура содержит два индекса глифов и поправку. Пары кернинга сообщают подсистеме вывода текста о необходимости отрегулировать расстояние между двумя конкретными глифами, следующими в указанном порядке. Например, поля первой пары кернинга шрифта Tahoma равны 4, 180, -94. Это означает следующее: «Если глиф 180 следует непосредственно за глифом 4, его базовая точка смещается влево на 94 единицы em-квадрата, чтобы глифы располагались ближе друг к другу». Для шрифта, содержащего п глифов, максимальное количество пар кернинга равно п х п; если шрифт состоит из тысяч глифов, число получается очень большим. К счастью, разработчики шрифта определяют пары кернинга лишь для относи-
790 Глава 14. Шрифты тельно небольшого количества пар. Например, в шрифте Tahoma определены 674 пары. Приложение может получить данные кернинга шрифта при помощи функции GDI GetKerningPairs. typedef struct { WORD wFirstl WORD wSecond; int iKernAmount; } KERNINGPAIR; DWORD GetKerningPairs(HDC hDC. DWORD nNumPairs. LPKERNINGPAIR Ipkrnpair): Чтобы получить данные кернинга для текущего логического шрифта, выбранного в контексте устройства, сначала вызовите функцию GetKerningPair с параметрами 0 (nNumPairs) и NULL (Ipkrnpair); функция вернет количество определенных пар. Выделите память pi вызовите функцию повторно для получения фактических данных кернинга. Учтите, что значение iKernAmount в структуре KERNINGPAIR задается в логических координатах контекста устройства, а не в единицах em-квадрата TrueType. Конечно, таблицу кернинга можно также получить функцией GetFontData. Метрики OS/2 и Windows В таблице «OS/2» хранятся важные метрические данные, используемые в операционных системах семейств OS/2 (IBM) и Windows (Microsoft). По названию можно предположить, что первоначально эта таблица предназначалась для OS/2. Графическая система должна иметь возможность как-то охарактеризовать различные шрифты, установленные в системе, чтобы при поступлении запроса от приложения можно было подобрать установленный шрифт, наиболее близко отвечающий требованиям. Таблица «OS/2» содержит большое количество атрибутов, учитываемых графической системой при обработке запросов. Таблица «OS/2» имеет следующую структуру: typedef struct USHORT SHORT USHORT USHORT SHORT SHORT SHORT SHORT SHORT SHORT SHORT SHORT SHORT SHORT SHORT SHORT version; xAvgCharWidth; usWeightClass; usWidthClass; fsType; ySubscriptXSize; ySubscriptYSize; ySubscriptXOffset ySubscriptYOffset ySuperscriptXSize ySuperscriptYSize ySuperscriptXOffset; ySuperscriptYOffset; yStrikeoutSize; yStrikeoutPosition; sFamilyClass; // 0x0001 // Взвешенная средняя ширина a..z // FW THIN .. FW BLACK // ULTRA_C0NDENSED .. ULTRA_EXPANDED // Возможность внедрения шрифта // Толщина перечеркивающей линии // Шрифты IBM
Шрифты TrueType 791 PANOSE ULONG ULONG ULONG ULONG CHAR USHORT USHORT USHORT USHORT USHORT USHORT USHORT USHORT ULONG ULONG panose; ulUnicodeRangel ulUnicodeRange2 ulUnicodeRange3 u!UnicodeRange4 achVendID[4]; fsSelection; // Биты 0-31 Интервал символов Unicode // Биты 32-63 // Биты 64-95 // Биты 96-127 // Идентификатор поставщика // ITALIC.REGULAR usFirstCharlndex; // Первый символ UNICODE usLastCharlndex sTypoAscender; sTypoDescender; sTypoLineGap; usWinAscent; usWinDescent; ulCodePageRange] // Последний символ UNICODE // Типографский надстрочный интервал // Типографский подстрочный интервал // Типографский междустрочный интервал // Надстрочный интервал для Windows // Подстрочный интервал для Windows L: // Биты 0-31 ulCodePageRange2; // Биты 32-63 } Table 0S2; Таблица «OS/2» содержит подробную информацию в формате, достаточно близком к структурам шрифтовых метрик GDI — таких, как LOGFONT, TEXTMETRICS, ENUMTEXTMETRIC и OUTLINETEXTMETRIC. Вследствие мультиплатформенной природы шрифтов TrueType обилие непоследовательной информации иногда сбивает с толку. Например, в таблице «OS/2» хранятся два набора надстрочных и подстрочных интервалов, которые не всегда совпадают с одноименными атрибутами, хранящимися в горизонтальной заголовочной таблице. Другие таблицы Мы подробно рассмотрели важнейшие таблицы шрифта TrueType. Впрочем, шрифты ТгиеТуре/ОрепТуре могут содержать другие таблицы, относящиеся к нетривиальным возможностям, используемые на других платформах или просто при печати на принтере. Таблица имен («name») позволяет связывать со шрифтом TrueType строковые данные на нескольких языках. Строки могут содержать названия шрифтов, семейств, стилей, информацию об авторских правах и т. д. Таблица PostScript («post») содержит дополнительную информацию для принтеров PostScript, в том числе описание Fontlnfo и имена PostScript всех глифов шрифта. Программная таблица контрольных величин («prep») содержит инструкции TrueType, которые должны выполняться при каждом изменении шрифта, кегля или матрицы преобразования, перед интерпретацией контура глифа. Программная таблица шрифта («fpgm») содержит инструкции, выполняемые при первом использовании шрифта. Таблица базовой линии («BASE») содержит информацию, используемую при выравнивании глифов разных начертаний и размеров в одной строке. Таблица определения глифов («GDEF») содержит данные классификации глифов, точки входа для упрощения доступа к данным и кэширования растров и т. д. Таблица размещения глифов («GPOS») позволяет точно управлять положением глифов при нетривиальном форматировании текста в каждом начертании и языке, поддерживаемом шрифтом. Таблица подстановки глифов
792 Глава 14. Шрифты («GSUB») содержит информацию подстановки глифов для воспроизведения поддерживаемых начертаний и языков. Она применяется для поддержки лигатур, контекстной замены глифов и т. д. Таблица выключки («JSFT») обеспечивает возможность дополнительного управления заменой и позиционированием глифов в тексте, выровненном по ширине. Вертикальная заголовочная таблица («vhea») и таблица вертикальных метрик («vmtx») содержат метрические данные для вертикальных шрифтов, включая зеркальные копии горизонтальной заголовочной таблицы и таблицы горизонтальных метрик. Таблица электронной подписи («DSIG») содержит электронную подпись шрифта ОрепТуре, на основе которой реализуются некоторые меры безопасности. Например, по электронной подписи операционная система может проверить источник и целостность шрифтовых файлов перед их использованием, а разработчик шрифта может установить ограничения на внедрение шрифта в документы. Коллекции TrueType Технология Microsoft ОрепТуре позволяет упаковать несколько шрифтов ОрепТуре в один шрифтовой файл, называемый «коллекцией TrueType» (TrueType Collection, TTC). Коллекции TrueType удобны для работы с похожими шрифтами, имеющими большое количество одинаковых глифов. Например, японский набор символов делится на небольшое количество глифов каны (японской слоговой азбуки) и тысячи глифов кандзи (иероглифов). В группе японских шрифтов было бы вполне разумно определить уникальные глифы каны при использовании общих глифов кандзи. Как говорилось выше, нормальный шрифт TrueType/OpenType состоит из одного каталога и нескольких таблиц. Файл коллекции TrueType состоит из одной заголовочной таблицы ТТС, нескольких каталогов таблиц (по одному для каждого шрифта) и большого количества таблиц (используемых совместно или раздельно). Заголовочная таблица ТТС устроена достаточно просто. В ней хранится тег («ttcf»), версия, количество каталогов и массив смещений каталогов таблиц TrueType. typedef struct { ULONG TTCTag; //Тег ТТС 'ttcf ULONG Version: // Версия ТТС (изначально 0x0001000) ULONG DirectoryCount; // Количество каталогов таблиц DWORD Directory[l]; // Смещения каталогов (переменный размер) } TTCJeader; Хотя коллекции шрифтов экономят память и место на диске, они нарушают работу функции GetFontData. При вызове GetFontData приложение запрашивает данные TrueType для всего шрифта, сохраняет их и передает на другой компьютер, где позднее этот шрифт устанавливается. Однако при работе с коллекцией приложение не знает, являются ли полученные данные полными или же они входят в коллекцию шрифтов TrueType. Что еще хуже, некоторые смещения задаются
Установка и внедрение шрифтов 793 относительно невидимого заголовка коллекции TrueType вместо текущего каталога таблиц. Например, смещения в структуре Tab! eDi rectory задаются относительно начала физического файла, поэтому они зависят от того, откуда были получены данные — из отдельного шрифта или из коллекции. Обходное решение заключается в проверке размеров всех шрифтов в коллекции по тегу ТТС. Сравнивая их с размерами текущего шрифта, можно определить его смещение в коллекции и в дальнейшем использовать его для поиска нужных таблиц. Установка и внедрение шрифтов Шрифты распространяются в виде файлов. Чтобы шрифт мог использоваться приложениями, он должен быть предварительно установлен операционной системой. В GDI существует несколько функций, управляющих установкой и удалением шрифтов, а также используемых при внедрении шрифтов в приложения или документы. BOOL CreateScalableFontResourceCDWORD fdwHidden, LPCTSTR IpszFontRes. LPCTSTR IpszFontFile, LPCTSTR 1pszCurrentPath); int AddFontResource(LPCTSTR IpszFileName); BOOL RemoveFontResourceCLPCTSTR IpFileName); int AddFontResourceExCLPCTSTR IpszFileName. DWORD f 1, DESIGNVECTOR * pdv); int RemoveFontResourceExCLPCTSTR IpszFileName, DWORD f1, DESIGNVECTOR * pdv); HANDLE AddFontMemResourceEx(LPVOID pbFont, DWORD cbFont. DESIGNVECTOR * pdb, DWORD * pcFonts); int RemoveFontMemResourceEx(HANDLE fh); Ресурсные файлы шрифтов Основными типами шрифтов в операционных системах Windows считались векторные и растровые шрифты, а форматы TrueType, OpenType и PostScript поначалу воспринимались как что-то постороннее. Несколько ресурсов растровых или векторных шрифтов (обычно относящихся к одной гарнитуре, но с разным кеглем) объединялись в 16-разрядные библиотеки DLL, называемые файлами шрифтовых ресурсов. В этих файлах шрифты подключались к приложению в виде двоичных ресурсов типа FONT (RTF0NT). Непосредственная поддержка установки шрифтов предусмотрена в GDI только для шрифтов в старом 16-разрядном формате файлов шрифтовых ресурсов. Для установки шрифта TrueType необходимо создать ресурсный файл масштабируемого шрифта. Ресурсный файл масштабируемого шрифта имеет тот же формат 16-разрядной библиотеки DLL, однако он не содержит копии шрифта TrueType. Вместо этого в нем указывается имя файла шрифта TrueType, по которому GDI находит данные шрифта. Чтобы создать ресурсный файл масштабируемого шрифта, вызовите функцию CreateScalableFontResource и передайте ей
794 Глава 14. Шрифты целочисленный флаг, имя создаваемого ресурсного файла, имя существующего файла шрифта TrueType и путь к нему (если он не включен в имя). Флаг fdwHidden сообщает GDI, должен ли шрифт быть скрыт от остальных процессов в системе. Функция CreateScalableFontResource записывает на диск небольшой файл шрифтового ресурса. Ресурсным файлам шрифтов TrueType рекомендуется назначать расширение .FOT, чтобы они отличались он ресурсов растровых и векторных шрифтов с расширениями .FON. Установка открытых шрифтов Функция AddFontResource устанавливает шрифт в системе по имени ресурсного файла, который может соответствовать растровому, векторному или шрифту TrueType. В результате установки шрифта ресурсный файл заносится в системную таблицу шрифтов и начинает использоваться при перечислении шрифтов, подстановке шрифтов, создании логических шрифтов и выводе текста. Шрифт, установленный функцией AddFontResource, доступен для всех приложений, если только шрифтовой ресурс не был создан со специальным флагом, скрывающим его в процессе перечисления шрифтов. Впрочем, шрифт, установленный функцией AddFontResource, доступен только во время текущего сеанса. После перезагрузки шрифт не будет автоматически добавлен в таблицу шрифтов. Чтобы установленный шрифт присутствовал в системе постоянно, информация о нем должна быть включена в реестр. Функция RemoveFontResource решает противоположную задачу — она удаляет шрифтовой ресурс из системной таблицы. При этом работающие приложения необходимо оповестить об изменениях в системной таблице шрифтов. Приложение, изменяющее таблицу шрифтов, должно оповестить об этом все окна верхнего уровня рассылкой сообщения WM_FONTCHANGE. Приложение, использующее список установленных шрифтов, должно обрабатывать сообщение WM_FONTCHANGE и обновлять содержимое списка. Установка закрытых шрифтов и шрифтов Multiple Master OpenType В Windows 2000 появились новые функции AddFontresourceEx и RemoveFontRe- sourceEx. Второй параметр AddFontResourceEx управляет «закрытостью» шрифта. При установке бита FPPRIVATE шрифт не может использоваться другими процессами и становится недоступным после завершения текущего процесса; если установлен флаг FP_N0T_ENUM, шрифт не участвует в перечислении. При установке любого из этих флагов вам уже не придется рассылать сообщение WM_F0NTCHANGE и оповещать другие приложения о шрифте, с которым они не могут работать. Функция RemoveFontResourceEx использует тот же параметр, что и AddFontResourceEx, для удаления шрифта, установленного функцией AddFontResourceEx. В последнем параметре передается указатель на структуру DESIGNVECT0R, используемую только для шрифтов Multiple Master OpenType. Шрифты Multiple Master OpenType строятся на базе шрифтовой технологии PostScript Type 1. Несколько шрифтов Multiple Master OpenType могут обладать общими характеристиками, принимающими значения из определенного интервала (такие характе-
Установка и внедрение шрифтов 795 ристики называются осями), что позволяет осуществлять точную регулировку внешнего вида шрифта. Например, ось насыщенности шрифта Multiple Master ОрепТуре может изменяться в интервале от 300 (тонкий) до 900 (тяжелый). Структура DESIGNVECT0R имеет переменный размер и содержит информацию о количестве характеристик и их значениях. Установка шрифтов из образа в памяти Для установки шрифта TrueType функцией AddFontResource или AddFontResourceEx на диске должны находиться два физических файла — файл шрифта TrueType и ресурсный файл шрифта. Это затрудняет программирование приложений, работающих с закрытыми шрифтами, и полную маскировку закрытых шрифтов от других приложений. Функция AddFontMemResourceEx, появившаяся в Windows 2000, пытается решить эти проблемы, позволяя устанавливать шрифты из образа в памяти. Первые два параметра этой функции задают адрес и размер блока памяти, содержащего один или несколько шрифтовых ресурсов. Третий параметр содержит указатель на структуру DESIGNVECT0R для шрифтов Multiple Master ОрепТуре. Функция AddFontMemresource устанавливает шрифты из образа в памяти, возвращая манипулятор и количество установленных шрифтов. Шрифты, установленные функцией AddFontResourceEx, всегда остаются закрытыми для приложения, в котором была вызвана эта функция. Далее приложение может удалить шрифты функцией RemoveFontMemResourceEx, передавая ей полученный манипулятор. Если приложение этого не сделает, шрифты будут автоматически удалены при завершении процесса. Блок памяти, переданный функции AddFontMemResource, заполняется в формате непосредственных данных ресурса, а не в формате 16-разрядной библиотеки DLL шрифтового ресурса. По сравнению с функциями AddFontResource и AddFontResourceEx функция AddFontMemResourceEx гораздо удобнее, поскольку она позволяет приложению устанавливать и использовать шрифты независимо от других приложений. Внедрение шрифтов При передаче документов на другие компьютеры нередко возникают серьезные проблемы со шрифтами. Установив на своем компьютере нужные шрифты, вы можете отформатировать документ и придать ему желаемый вид. Но если открыть этот документ на другом компьютере с другим набором установленных шрифтов, он может выглядеть совершенно иначе. Подобные проблемы возникают в приложениях, использующих специализированные шрифты, при работе с документами текстовых редакторов, web-страницами и даже файлами спулера при печати на удаленном сервере. Технология внедрения шрифтов (font embedding) позволяет включить специальные шрифты прямо в документ. При открытии документа внедренные шрифты автоматически устанавливаются на другом компьютере, благодаря чему документ сохраняет прежний вид. Внедрение шрифтов должно соответствовать лицензионным правилам использования шрифтов. Для шрифтов TrueType/OpenType определены шесть уров-
796 Глава 14. Шрифты ней внедряемости, обозначаемые флагом fsType в таблице метрик OS/2 и Windows («OS/2»). О Внедрение с возможностью установки (0x0000): шрифт может внедряться в документы и устанавливаться в удаленной системе для постоянного использования. Большинство шрифтов из поставки ОС Windows допускает именно этот способ внедрения. О Внедрение для редактирования (0x0008): шрифт может внедряться в документы, но только для временной установки в удаленной системе. Например, при внедрении такого шрифта в документ Word вы сможете просматривать и редактировать документ на удаленном компьютере, однако при выходе из WinWord шрифт автоматически удаляется из системы. О Внедрение для просмотра (0x0004), также называемое внедрением только для чтения: шрифт может внедряться в документы, но только для временной установки в удаленной системе. Документы могут открываться только для чтения. Данные шрифта должны быть зашифрованы в документе. На удаленном компьютере шрифт расшифровывается в скрытый файл без расширения .TTF, устанавливается в качестве скрытого шрифта, используется только для просмотра и печати документа и удаляется при выходе из приложения. О Запрет частичного внедрения (0x0100): допускается только полное внедрение всего шрифта. О Внедрение растров (0x0200): внедрение разрешается только для растров, содержащихся в шрифте. Если шрифт состоит из одних контуров глифов, он не может внедряться. О Запрет внедрения (0x0002): шрифт не может внедряться в документы. Учтите, что уровень внедряемости шрифта относится только к внедрению шрифтов в документы, но не в приложения. Согласно MSDN, шрифты не могут внедряться в приложения, а в поставку приложений не могут входить документы, содержащие внедренные шрифты. Функция GetOutlineTextMetrics используется для проверки возможности внедрения шрифтов ТгиеТуре/ОрепТуре. Она возвращает структуру OUTLINETEXTMETRIC, содержимое которой близко к содержимому таблицы метрик OS/2 и Windows (таблица «OS/2») в файле шрифта TrueType. Поле otmfsType этой структуры имеет то же значение, что и описанное выше поле f sType. В листинге 14.4 приведены две функции установки и удаления шрифтов ТгиеТуре/ОрепТуре. Функция Install Font получает образ шрифта TrueType/ ОрепТуре в памяти, создает файлы .TTF и .FOT и устанавливает шрифт. Функция RemoveFont исключает шрифт из системного списка и удаляет файлы .TTF и .FOT. Обе функции получают параметр option, который сообщает, должен ли шрифт быть открытым, скрытым, закрытым, не перечисляемым или устанавливаемым прямо из образа в памяти. В зависимости от значения option выбирается функция GDI, вызываемая при установке и удалении шрифта. Листинг 14.4. Установка и удаление шрифтов #define FR_HIDDEN 0x01 #define FR_MEM 0x02
Установка и внедрение шрифтов 797 BOOL RemoveFont(const TCHAR * fontname. int option. HANDLE hFont) { if ( option & FR_MEM ) { return RemoveFontMemResourceEx(hFont); } TCHAR ttffile[MAX_PATH]; TCHAR fotfile[MAX_PATH]; GetCurrentDirectory(MAX_PATH-l, ttffile); _tcscpy(fotfile. ttffile); wsprintf(ttffile + _tcslen(ttffile). "\Us.ttf". fontname); wsprintf(fotfile + _tcslen(fotfile), "\Us.fot". fontname); BOOL rslt; switch ( option ) { case 0; case FR_HIDDEN: rslt = RemoveFontResource(fotfile); break; case FR_PRIVATE: case FR_NOT_ENUM; case FR_PRIVATE | FR_NOT_ENUM: rslt = RemoveFontResourceEx(fotfile, option. NULL): break; default: assert(false); rslt = FALSE; } if ( ! DeleteFile(fotfile) ) rslt = FALSE; if ( ! DeleteFile(ttffile) ) rslt = FALSE; return rslt; HANDLE InstallFont(void * fontdata. unsigned fontsize. const TCHAR * fontname. int option) { if ( option & FR_MEM ) { DWORD num; return AddFontMemResourceEx(fontdata. fontsize. NULL. & num); } TCHAR ttffile[MAX_PATH]; Продолжение &
798 Глава 14. Шрифты Листинг 14.4. Продолжение TCHAR fotfile[MAX_PATH]; GetCurrentDirectory(MAX_PATH-1. ttffi 1 e); _tcscpy(fotfile. ttffile); wsprintf (ttffile + Jcslen(ttffile). "\Us.ttf". fontname); wspnntf(fotfile + Jxslen(fotfile). "\Us.fot". fontname); HANDLE hFile = CreateFileCttffile. GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN. 0); if ( hFile==INVALIDJANDLE_VALUE ) return NULL; DWORD dwWritten; WriteFile(hFile. fontdata. fontsize. & dwWritten, NULL); FlushFileBuffers(hFile); CloseHandle(hFile); if ( ! CreateScalableFontResource(option & FR_HIDDEN. fotfile, ttffile, NULL) ) return NULL; switch ( option ) { case 0; case FRJIDDEN: return (HANDLE) AddFontResource(fotfile); case FR_PRIVATE; case FRJOTJNUM: case FR_PRIVATE | FRJOTJNUM: return (HANDLE) AddFontResourceEx(fotfile, option, NULL); default: assert(false); return NULL; Функции, приведенные в листинге 14.4, были использованы в простой демонстрационной программе FontEmbed. Эта программа представляет собой простое приложение на базе диалогового окна (рис. 14.19). В диалоговом окне программы FontEmbed расположены три кнопки. Кнопка Generate генерирует «документ» с внедренными шрифтами ТгиеТуре/ОрепТуре, выбранными пользователем, применяя несложный механизм шифрования. Кнопка Load загружает сгенерированный документ и устанавливает внедренные шрифты в системе. Режим использования шрифта определяется группой переключателей. Кнопка Unload удаляет все установленные шрифтовые ресурсы. Справа показаны результаты, полученные при выводе текста внедренными шрифтами.
Установка и внедрение шрифтов 799 1!^Н1ШНнняН1 бшшЫ© toad Urtoacf №W|l|l|WWl'|lWiMlM|TMl"|iWWlMlMil 4e Г Private Ozzie Black С No Enumerate ^ # л# г Memcv Ozzie Blueli Italic ж Рис. 14.19. Демонстрация внедрения шрифтов При построении рисунка были использованы три бесплатных шрифта TrueType с web-страницы HP FontSmart Homage (www.fonstmart.com): Euro Sign, Ozzie Black и Ozzie Black Italic. Если эти шрифты не установлены в системе, первая строка выводится стандартным шрифтом Symbol, а две других — шрифтом Arial. После установки шрифтов диалоговое окно принимает вид, показанный на рисунке, но после удаления шрифтов окно возвращается к прежнему виду. Если у вас нет этих шрифтов, загрузите их, а если есть — найдите в Интернете какие-нибудь новые бесплатные или условно-бесплатные шрифты. Запустите программу FontEmbed, поэкспериментируйте с разными вариантами установки и проверьте, доступен ли шрифт после установки в текущем приложении и в других приложениях. Системная таблица шрифтов В Windows NT/2000 список шрифтов, постоянно присутствующих в системе, хранится в следующем разделе реестра: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts Во время загрузки системы шрифты загружаются в системную таблицу шрифтов, что дает возможность их использовать. Шрифты в списке соответствуют физическим шрифтам, совместно используемым всеми процессами в системе. На самом деле графический механизм хранит в адресном пространстве режима ядра целых три таблицы — для открытых шрифтов, для закрытых шрифтов и для шрифтов устройств, которые обычно поддерживаются современными принтерами (например, принтерами PostScript). Постоянные шрифты, предоставленные операционной системой, обычно должны быть доступны для всех приложений, поэтому они хранятся в таблице открытых шрифтов. Если файл шрифтового ресурса OpenType/TrueType создается с флагом скрытия, при передаче флага FR_HIDDEN при вызове CreateFontResourceEx или при использовании функции CreateFontMemResourceEx шрифт хранится в таблице закрытых шрифтов. Если флаг FR_NOT_ENUM используется без флага FRHIDDEN, шрифт заносится в таб-
800 Глава 14. Шрифты лицу открытых шрифтов. В системном списке шрифтов хранятся полные пути к файлам каждого шрифта. Если шрифт устанавливался из ресурса, находящегося в памяти, для него используется имя псевдофайла типа «MEMORY-1». В расширении отладчика GDI поддерживаются три команды pubft, pvtft и devft для вывода содержимого таблиц шрифтов. Вы можете использовать эти команды в управляющей программе Fosterer (см. главу 3). Итоги Эта глава посвящена основным принципам вывода текста в графическом Windows-программировании. Она начинается с описания базовых концепций шрифтов: символов, наборов символов, глифов, кодировок и отображения символов в глифы. Далее описываются три основных типа шрифтов системы Windows: растровые, векторные и шрифты TrueType. Мы знакомимся с тем, как в каждом типе шрифта представляются глифы и как происходит их вывод в процессе растеризации. Глава завершается описанием установки и удаления шрифтов, а также внедрения шрифтов в документы. Руководствуясь хорошим пониманием шрифтов, заложенным в этой главе, в главе 15 мы переходим к их практическому применению — выводу текста. Примеры программ К главе 14 прилагаются два примера программ (табл. 14.4). Таблица 14.4. Программы главы 14 Каталог проекта Описание Samples\Chapt_14\Fonts Иллюстрация общих концепций — наборов символов, кодировок, глифов, семейств шрифтов, процесса перечисления и трех основных типов шрифтов (растровые, векторные и шрифты TrueType) Samples\Chapt_14\FontEmbed Иллюстрация установки, удаления и внедрения шрифтов в документы
Глава 15 Текст Как было показано в предыдущей главе, шрифты являются основным элементом выводимого текста. В этой главе рассматриваются логические шрифты, функции вывода текста, простейшее форматирование, качественное и точное форматирование и специальные эффекты, используемые при выводе текста. Логические шрифты В главе 14 были описаны важнейшие особенности трех основных шрифтовых технологий, применяемых в Windows-программировании, — растровых шрифтов, векторных шрифтов и шрифтов ТшеТуре/OpenType. Впрочем, даже если вы досконально разбираетесь в устройстве физических шрифтов, работать с ними напрямую — дело сложное и долгое, на которое явно не стоит тратить время программиста. К счастью, при выводе текста приложениям Windows (и даже графическому механизму) не приходится напрямую общаться с физическими шрифтами. Прикладная программа обычно работает только с логическими шрифтами при помощи специальных функций API. С физическими шрифтами работают шрифтовые драйверы, находящиеся в системе на одном уровне с драйверами графических устройств. В графическом механизме Windows NT/2000 реализованы три шрифтовых драйвера для трех типов шрифтов, непосредственно поддерживаемых Microsoft. Шрифты ATM поддерживаются отдельным шрифтовым драйвером (atmfd.dll). Поддержка логических шрифтов основана на взаимодействии графического механизма с шрифтовыми драйверами. По сравнению с физическими шрифтами логические шрифты обладают рядом существенных преимуществ. О Логические шрифты обеспечивают независимость от устройства. Логический шрифт создается по перечню требований пользователя к шрифту. Графический механизм отвечает за подбор шрифта с указанными параметрами среди физических шрифтов, установленных в системе. Система сможет подобрать
802 Глава 15. Текст похожий шрифт даже в том случае, если некоторые шрифты в ней отсутствуют. При этом для разных графических устройств могут выбираться разные шрифты, отвечающие заданным требованиям. О Логические шрифты поддерживают использование кодировок. Чтобы найти в шрифте TrueType глиф для заданной кодировки, вам придется провести поиск в таблице отображения символов на индексы глифов. Логические шрифты маскируют индексы глифов от приложений. О Логические шрифты позволяют создавать экземпляры шрифтов с заданными размерами. Описания глифов в шрифте представляют собой общие шаблоны для построения глифов с любым кеглем или углом поворота. Растровый шрифт обычно содержит разные шрифтовые ресурсы для разных кеглей. Векторные шрифты и шрифты TrueType/OpenType допускают произвольное масштабирование и любые преобразования. При выборе логического шрифта в контексте устройства создается конкретный экземпляр шрифта с заданным кеглем и углом поворота. Такая архитектура позволяет графическому механизму и шрифтовым драйверам кэшировать масштабированные и растеризованные версии глифов для повышения быстродействия системы. О Логические шрифты позволяют имитировать определенные возможности на программном уровне. Некоторые распространенные начертания шрифтов (например, подчеркивание и перечеркивание) не реализуются в физических шрифтах, а имитируются GDI. Кроме того, GDI может имитировать курсивное и полужирное начертание в тех случаях, когда соответствующий физический шрифт недоступен. Метрики шрифтов в Windows Прежде чем переходить к подробному рассмотрению логических шрифтов, давайте познакомимся с терминами, используемыми при работе со шрифтами в Windows. Учтите, что смысл некоторых терминов Windows GDI слегка отличается от смысла этих терминов в шрифтовой спецификации TrueType и традиционном печатном деле. На рис. 15.1 показаны основные метрики, применяющиеся при форматировании текста в GDI. Воображаемая линия, по которой осуществляется вертикальное выравнивание глифа, называется базовой линией. Нижняя точка большинства прописных букв находится практически на базовой линии. Символы располагаются в ячейках одинаковой высоты. Расстояние от верхнего края ячейки до базовой линии называется надстрочным интервалом. Обычно даже самые высокие глифы не достают до верхнего края ячейки, поэтому в GDI понятие «надстрочный интервал» несколько отличается от типографских надстрочных интервалов, используемых в шрифтах TrueType. Расстояние от базовой линии до нижней части ячейки символа называется подстрочным интервалом. Нижняя точка подстрочного элемента глифа также может отделяться некоторым расстоянием от нижней стороны ячейки. Сумма надстрочного и подстрочного интервалов называется высотой шрифта. В промежутке между надстрочной линией и верхней стороной ячейки обычно размещаются акценты и другие диакритические знаки. Высота этого проме-
Логические шрифты 803 жутка называется внутренним зазором (internal leading). Когда несколько строк текста образуют абзац, нижняя сторона ячеек предыдущей строки отделяется от верхней стороны ячеек текущей строки дополнительным интервалом, который называется внешним зазором (external leading). Внешний зазор I Надстрочный] интервал Высота Метрика А Внутренний зазор Метрика С Рис. 15.1. Метрики шрифтов в Windows Размер текста измеряется в пунктах. В традиционном печатном деле один пункт равен 0,01389 дюйма (1/71,99424 дюйма). В компьютерной верстке пункт округляется до 1/72 дюйма, поэтому один дюйм состоит ровно из 72 пунктов. Погрешность составляет всего 1/12 500 дюйма, поэтому для практических целей ей можно пренебречь. При упоминании текста или шрифта термин «кегль» относится к метрике «надстрочный интервал + подстрочный интервал - внутренний зазор», то есть «высота - внутренний зазор». Обратите внимание: кегль не включает ни внутренний, ни внешний зазор. Например, в абзаце 10-пунктового текста сумма «надстрочный интервал + подстрочный интервал - внутренний зазор» равна 10 пунктам, что соответствует 13,3 пиксела на экране с разрешением 96 dpi или 83,3 пиксела на принтере с разрешением 600 dpi. В абзацах с кеглем 10 пунктов расстояние между строками обычно равно 12 или 13 пунктам, то есть 6 или 5,54 строки на дюйм. Горизонтальные метрики, используемые в GDI, почти совпадают с метриками шрифтов TrueType. Расстояние между двумя соседними символами называется пол7юй шириной. Полная ширина делится на три части. Левая часть обычно соответствует пробелу перед крайней левой точкой глифа; она называется метрикой А (левый отступ в терминологии шрифтов TrueType). Средняя часть определяет фактическую ширину глифа в ячейке и называется метрикой В. Правая часть обычно соответствует пробелу после крайней правой точки глифа и называется метрикой С (правый отступ в TrueType). Полная ширина символа равна сумме метрик А, В и С. Метрики А и С могут иметь отрицательные значения для сближения глифов, особенно в курсивных шрифтах.
804 Глава 15. Текст Стандартные шрифты Логический шрифт представляет собой объект GDI, описывающий требования к конкретному воплощению физического шрифта. Объект логического шрифта, как и другие объекты GDI, находится под управлением GDI, а с точки зрения приложения он представляют собой «черный ящик». Пользовательские приложения работают с логическими шрифтами только через манипуляторы логических шрифтов, относящиеся к типу HF0NT. В системе определяются семь стандартных (встроенных) логических шрифтов GDI, используемых операционной системой при выводе пользовательского интерфейса, а также в приложениях. Манипуляторы стандартных логических шрифтов возвращаются вызовами GetStockOb ject (DEFAULTGU I_F0NT), GetStockObject (SYSTEMFONT) и т. д. Большинство стандартных логических шрифтов относится к категории растровых шрифтов, используемых для ускоренного вывода заголовков окон, меню, диалоговых окон и т. д. На рис. 15.2 показаны 7 стандартных шрифтов на мониторе с разрешением 96 dpi. Для каждого стандартного шрифта приведен способ получения манипулятора функцией GetStockObject и содержимое структуры L0GF0NT, о которой речь пойдет ниже. DialogBasellnits: baseunixX=8, baseunitY=16 GetDeviceCaps(LOGPIXELSX)=96, GetDeviceCaps(LOGPIXELSX)=96 GetStockObjecKDEFAULT.GULFONT) M 1,0,0,0,400,0,0,0,0,0,0,0,0, MS Shell Dig} GetStockObject<OEM_FIXED_FONT> <12, 8, 0, 0, 400, 0, 0, 0, 255, 1, 2, 2, 49, Terminal> GetStockObject(ANSI_FIXED_FONT) {12, 9, 0, 0, 400, 0, 0, 0, 0, 0, 2, 2, 1, Courier} GetStockObiect(ANSI_VAR^FONT) {12,9,0,0,400, 0,0,0, 0, 0, 2,2,2, MS Sans Serif} GetStockObject(SYSTEM_FONT) {16,7, 0, 0, 700, 0, 0, 0, 0, 1, 2, 2, 34, System} GetStockObject(DEVICE_DEFAULT_FONT| {16, 7, 0, 0, 700, 0, 0, 0, 0, 1, 2, 2, 34, System} GetStockObject(SVSTEM_FIXED_FONT) {15, 8, 0, 0, 400, О, О, О, О, 1, 2, 2, 49, Fixedsys} Рис. 15.2. Стандартные шрифты в разрешении 96 dpi Стандартные шрифты часто встречаются в заголовках окон, меню и текстах, выводимых в различных элементах управления. Размер стандартного шрифта в пикселах изменяется при изменении логического разрешения экрана. Например, логическое разрешение нормального экрана равно 96 dpi — так называемый режим «мелких шрифтов». При помощи панели управления можно переключиться в режим «крупных шрифтов» с разрешением 120 dpi. При переключении экрана из режима мелких шрифтов в режим крупных шрифтов все стандартные шрифты приходится заново отображать на физические шрифты большего раз-
Логические шрифты 805 мера, для чего обычно перезагружают систему. После этого все заголовки окон, меню, элементы и диалоговые окна увеличиваются в соответствии с изменениями в размере шрифта. Проектирование пользовательского интерфейса, который бы идеально выглядел в обоих режимах (крупных и мелких шрифтов) — задача не из простых. Вы можете воспользоваться функцией GetSystemMetrics для получения различных системных метрик, в том числе текущих размеров строк заголовка и меню. Например, вызов GetSystemMetrics(SM_CYMENU) возвращает высоту строки меню с одной строкой команд. Диалоговые окна проектируются в аппаратно-независи- мых шаблонных диалоговых единицах. При создании диалогового окна шаблонные единицы преобразуются в экранные пикселы с учетом текущих базовых диалоговых единиц по следующим формулам: pixelX = (templateunitX * baseunitX) / 4; pixelY = (templateunitY * baseunitY) / 8; Базовые диалоговые единицы определяются средней шириной и высотой символов стандартного шрифта, используемого для вывода элементов в диалоговых окнах. Их значения можно получить при помощи функции GetDialogBaseUnits. В разрешении 96 dpi baseunitX = 8, a baseunitY =16, поэтому каждая шаблонная диалоговая единица преобразуется в два экранных пиксела. В разрешении 120 dpi baseunitX = 10, baseunitY = 20, а каждая шаблонная диалоговая единица преобразуется в 2,5 экранных пиксела. В результате при переключении из режима мелких шрифтов в режим крупных шрифтов диалоговые окна увеличиваются на 12,5 %. На первый взгляд кажется, что все очень здорово, поскольку вы «бесплатно» получаете текст высокого разрешения, однако не все элементы пользовательского интерфейса справляются с подобным увеличением. Если в диалоговом окне присутствуют растры и значки или вы используете немодальное диалоговое окно, внедренное в недиалоговое окно, в увеличенном диалоговом окне может наблюдаться уменьшение растров и значков, усечение текста и нарушение выравнивания диалоговых окон относительно недиалоговых. Создание логических шрифтов Область применения стандартных шрифтов обычно ограничивается простым выводом элементов пользовательского интерфейса. Для любых других целей вам придется создавать собственные логические шрифты. В GDI предусмотрены три функции для создания логических шрифтов. typedef struct tagLOGFONT { LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG IfHeight; IfWidth; IfEscapement; 1 fomentation; IfWeight; Ifltalic; IfUnderline; IfStrikeOut; IfCharSet; IfOutPrecision; IfClipPrecision;
806 Глава 15. Текст LONG lfQuality; LONG IfPitchAndFamily; LONG 1fFaceName[LF_FACESIZE]; } LOGFONT. *PLOGFONT; typedef struct tagENUMLOGFONTEX { LOGFONT elfLogFont; TCHAR elfFul1Name[LF_FULLFACESIZE]; TCHAR elfStyle[LF_FACESIZE]; TCHAR elfScript[LF_FACESIZE]; } ENUMLOGFONTEX, *LPENUMLOGFONTEX; typedef struct tagENUMLOGFONTEXDV { ENUMLOGFONTEX elfEnumLogfontEx; DESIGNVECTOR elfDesignVector; } ENUMLOGFONTEXDV, *PENUMLOGFONTEXDV; HFONT CreateFont (int nHeight, int nWidth. int nEscapement. int nOrientation. int fnWeight. DWORD fdwltalic, DWORD fdwUnderline, DWORD fdwStrikeOut, DWORD fdwCharSet. DWORD fdwOutputPrecision, DWORD fdwClipPrecision, DWORD fdwQuality, DWORD fdwPitchAndFamily. LPCTSTR IpszFace); HFONT CreateFontIndirect(CONST LOGFONT * Iplf); HFONT CreateFontIndirectEx(const ENUMLOGFONTEXDV * penumlfex); Параметры этих трех функций описывают требования пользователя к создаваемому логическому шрифту. Функция CreateFont использует для описания логического шрифта 14 параметров — рекорд для функций GDI. Функция CreateFontlndirect получает указатель на структуру LOGFONT, в которой упакованы все 14 параметров. Новая функция CreateFontlndirectEx, появившаяся только в Windows 2000, получает указатель на структуру ENUMLOGFONTEXDV. В структуру ENUMLOGFONTEXDV добавляется поле DESIGNVECTOR, в котором содержится уникальное имя шрифта, имена начертания и модификации. Таким образом, базовые требования к логическому шрифту описываются структурой LOGFONT, передаваемой при вызове CreateFontlndirect; функция CreateFont просто получает развернутую версию структуры LOGFONT, тогда как функция CreateFontlndirectEx всего лишь использует расширенный вариант этой структуры. LOGFONT и другие шрифтовые структуры играют очень важную роль для понимания шрифтов GDI, поэтому мы должны рассмотреть их основные поля. О If Height. Желательная высота шрифта в логических единицах. Если значение равно 0, используется стандартная высота шрифта, равная примерно 12 пунктам. Положительные значения определяют требуемую высоту ячеек (то есть сумму надстрочного и подстрочного интервалов). Отрицательные значения определяют высоту символов шрифта (надстрочный интервал + подстрочный интервал - внутренний зазор). О lfWidth. Желательная ширина шрифта в логических единицах. Если значение равно 0, поиск осуществляется сравнением аспектного отношения графического устройства с аспектным отношением физического шрифта. Экраны и принтеры обычно обладают одинаковым разрешением по вертикали и горизонтали — например, 120 х 120 dpi или 600 х 600 dpi. В этом случае нулевое
Логические шрифты 807 значение параметра отдает предпочтение шрифтам с аспектным отношением 1:1. О 1 fEscapement. Угол (в десятых долях градуса против часовой стрелки) между базовой линией текста и осью х устройства. Например, 1 fEscapement = 900 означает, что весь текст выводится вдоль базовой линии, параллельной оси у. О IfOrientation. Угол (в десятых долях градуса против часовой стрелки) между базовой линией каждого символа и осью х устройства. Следует помнить, что ориентация определяет угол поворота отдельных символов, а наклон (lfEscapement) — угол поворота всей строки. Если устройство работает в расширенном графическом режиме (GM_ADVANCED), доступном лишь в Windows NT/ 2000, наклон и ориентация задаются независимо друг от друга. В совместимом графическом режиме (GM_C0MPATIBLE), поддерживаемом в Windows 95/98, поле lfEscapement определяет IfOrientation, и значения этих полей должны совпадать. О lfWeight. Насыщенность (жирность) шрифта в интервале от 0 до 1000. Значение FMJD0NTCARE (0) позволяет GDI выбрать шрифт с произвольной насыщенностью, FMJI0RMAL (400) соответствует средней насыщенности, а FW_HEAVY (900) обычно определяет самый жирный шрифт. О lfltalic. Если значение этого поля равно TRUE, предпочтение отдается курсивным шрифтам. О If Underline. Если значение этого поля равно TRUE, текст выводится с подчеркиванием. О IfStrikeOut. Если значение этого поля равно TRUE, текст выводится перечеркнутым (посреди строки проводится линия). О IfCharSet. Набор символов шрифта; также определяет кодировку, в которой строки передаются функциям вывода текста GDI. Наборы символов и кодировки описаны в разделе «Что такое шрифт?» главы 14. У поля IfCharSet имеется специальное значение DEFAULT_CHARSET. В Windows 95/98 шрифт определяется значениями других полей, а в Windows NT/2000 будет задействован набор символов, используемый по умолчанию для текущего системного локального контекста. Например, если системный локальный контекст соответствует английскому языку, используется значение ANSI CHARSET. При работе с экзотическими языками поле 1 fCharSet играет очень важную роль для выбора правильного шрифта, поскольку глифы нужного набора могут поддерживаться лишь небольшим количеством физических шрифтов. О IfOutPrecision. Желательные параметры подбора физических шрифтов. Значение 0UT_DEFAULT_PRECIS указывает на стандартный способ подбора шрифтов. При значении 0UTDEVICEPRECIS предпочтение отдается шрифтам устройств, а при значении OUTRASTERPRECIS — растровым шрифтам. Значение 0UT_0UTLINE_ PRECIS (только в Windows NT/2000) отдает предпочтение контурным шрифтам, в том числе и шрифтам TrueType. При значении 0UT_JT_PRECIS предпочтение отдается шрифтам TrueType, а при значении 0UTTT0NLYPRECIS подбор осуществляется только среди шрифтов TrueType. О IfClipPrecision. Способ отсечения шрифтов. Для этого поля определено несколько флагов, но похоже, что к отсечению имеет отношение только флаг
808 Глава 15. Текст CLIPDEFAULTPRECIS (стандартная процедура отсечения). Если поле lfClipPre- cision равно CLIPEMBEDDED, допускается использование внедренных шрифтов, доступных только для чтения. При указании флага CLIPLHANGLES направление поворота глифов шрифтов устройств зависит от того, является ли логическая система координат левосторонней или правосторонней; в противном случае шрифты устройств всегда поворачиваются против часовой стрелки. О lfQuality. Качество вывода глифов. Значение DEFAULTQUALIYY сообщает GDI, что внешний вид символов несущественен. Значение DRAFT_QUALITY говорит о том, что размер шрифта важнее качества глифа, что позволяет GDI масштабировать растровые шрифты по нужным размерам с возможными искажениями. Значение PR00FQUALITY указывает на то, что качество глифа важнее размера шрифта, поэтому масштабирование растровых шрифтов запрещается. Для шрифтов TrueType константы DRAFTQUALIT Y и PROOFQUALITY несущественны, поскольку контуры глифов свободно масштабируются до нужной величины. Значение ANTIALIASEDQUALITY заставляет GDI выполнять сглаживание текста, если оно поддерживается шрифтом, а сам шрифт не слишком велик и не слишком мал. Значение NONANTIALIASEDQUALITY запрещает сглаживание. О lfPitchAndFamily. Шаг и семейство шрифта определяются в одном поле. Младшие 2 бита могут быть равны DEFAULTPITCH (шаг по умолчанию,) FIXEDPITCH (моноширинный шрифт) или VARIABLEPITCH (пропорциональный шрифт). Биты 4-7 определяют семейство шрифта в виде константы FFDECORATIVE, FF_DONTCARE, FF_M0DERN, FF_R0MAN, FFJCRIPT и FFJWISS. Семейства шрифтов описаны в разделе «Что такое шрифт?» главы 14. О IfFaceName. Имя гарнитуры шрифта. Имена гарнитур шрифтов, установленных в настоящий момент, перечисляются функцией EnumFontFamilies. О elf Full Name. Уникальное имя шрифта, включающее название компании, имя гарнитуры, начертания и т. д. О elf Sty! е. Начертание шрифта — например, Bold Italic. О elf Script. Название языковой модификации шрифта — например, Cyrillic. О elfDesignVector. Оси шрифтов Multiple Master OpenType. Функции CreateFont, CreateFontIndirect и CreateFontlndirectEx создают объект логического шрифта и возвращают его манипулятор вызывающей стороне. Вызывая по манипулятору объекта GDI функцию GetObject, можно получить структуру LOGFONT или ENUMLOGFONTEX с описанием логического шрифта. Когда объекты логических шрифтов становятся ненужными, их, как и остальные объекты GDI, следует удалить функцией Del eteObject. При создании логического шрифта следует прежде всего рассчитать высоту шрифта в логических координатах. Если вы знаете размер шрифта в пунктах, следует преобразовать его в логические координаты по эталонному контексту устройства. Ниже приведена функция, которая преобразует размер шрифта в пунктах в размер, заданный в логических координатах. // Преобразование пунктов в логические координаты int PointSizetoLogicaKHDC hDC. int points, int divisor) {
Логические шрифты 809 POINT P[2] = // Две точки (POINT) в координатах устройства, // расстояние между которыми равно высоте шрифта { { 0. О }, { 0. ::GetDeviceCaps(hDC. L0GPIXELSY) * points / 72 / divisor DPtoLPChDC. P, 2); // Преобразовать координаты устройства // в логические координаты return abs(P[l].y - P[0].y); } Функция Poi ntSizeToLogi cal получает манипулятор эталонного контекста устройства, размер в пунктах и необязательный делитель, повышающий точность вычислений. Сначала пункты преобразуются в пикселы на основании вертикального разрешения устройства, после чего значение преобразуется в высоту, заданную в логической системе координат. Например, высота шрифта с кеглем 12 пунктов вычисляется вызовом Poi ntSi zetoLogi cal (hDC,12), а для шрифта с кеглем 12,25 используется вызов PointSizetoLogicaKhDC, 1225, 100). На устройствах высокого разрешения (скажем, на принтере с разрешением 1200 dpi) каждый пиксел равен 0,06 пункта, поэтому дробная часть кегля может влиять на форматирование текста. Заполнение 14 полей структуры L0GF0NT или передача 14 параметров функции CreateFont — утомительная процедура, которая нередко чревата ошибками. В листинге 15.1 приведен простой класс KLogFont, инкапсулирующий структуру логического шрифта L0GF0NT. Листинг 15.1. Класс KLogFont: инкапсуляция структуры LOGFONT class KLogFont public: LOGFONT mjf; KLogFont(int height, const TCHAR * typeface=NULL) i m If.lfHeight mJf.lfWidth mjf. If Escapement mjf Л fomentation m lf.lfWeight mjf.lfltalic m If.IfUnderline mJf.lfStrikeOut mJf.lfCharSet mJf.lfOutPrecision mJf.lfClipPrecision m If.lfQuality mJf.lfPitchAndFamily if ( typeface ) = height; - 0; = 0; = 0; - FW NORMAL; - FALSE; - FALSE; = FALSE; - ANSI CHARSET; = OUT TT PRECIS; = CLIP DEFAULT PRECIS: - DEFAULT QUALITY; - DEFAULT PITCH | FF D0NTCARE; Jxsncpy(mJf.lfFaceName. typeface. LF_FACESIZE-1) else Продолжение &
810 Глава 15. Текст Листинг 15.1. Продолжение mJf.lfFaceName[0] = 0; HFONT CreateFont(void) return ::CreateFontlndirect(& mjf); int Get0bject(HF0NT hFont) return ::GetObject( hFont. sizeof(mjf), &mjf); }: Класс KLogFont сокращает количество параметров с 14 до 2. Остальным параметрам присваиваются разумные значения по умолчанию, которые можно изменить через поля открытой переменной ml f. В следующем фрагменте создается логический курсивный шрифт с кеглем 36 пунктов для шрифта Times New Roman: KLogFont lf(- PointSizetoLogical (hDC. 36), "Times New Roman"; lf.mjf.lfltalic = TRUE; HFONT hFont - lf.CreateFontO; Подстановка шрифта Новый логический шрифт, созданный функцией CreateFont, CreateFontlndirect или CreateFontlndirectEx, не ассоциируется ни с каким физическим шрифтом, поскольку он еще не ассоциирован с контекстом устройства. При выборе логического шрифта в контексте устройства перед GDI встает задача — подобрать физический шрифт, соответствующий заданному описанию. Этот процесс называется подстановкой шрифта (font matching). В процессе подстановки GDI сверяет требования логического шрифта с данными всех шрифтов, доступных для графического устройства. Помимо шрифтов, постоянно установленных в системе, в подстановке также могут участвовать внедренные шрифты и шрифты устройств. В главе 14 было показано, как внедрить шрифт в документ, установить его при открытии документа и получить список установленных шрифтов. Шрифты устройств поддерживаются драйвером графического устройства и реализуются графическим устройством на аппаратном уровне. Например, принтер PostScript обычно поддерживает несколько десятков шрифтов PostScript и передает информацию о них GDI, чтобы эти шрифты использовались при выводе текста. Обычно пользовательское приложение форматирует текст на основании метрических данных, запрашиваемых у контекста устройства. При непосредственном получении команд вывода драйвер принтера может генерировать команды, использующие шрифты устройства, вместо загрузки шрифтов TrueType. В классических растровых и векторных шрифтах Windows структура заголовка шрифта очень похожа на структуру TEXTMETRIC, используемую в GDI. Структура TEXTMETRIC содержит практически те же данные, что и L0GF0NT, — высоту, среднюю ширину, насыщенность, семейство и тип, курсив, подчеркивание, перечеркивание и т. д. GDI без особых усилий подбирает нужный шрифт, сопостав-
Логические шрифты 811 ляя содержимое L0GF0NT с заголовком шрифта. Иначе говоря, создание логических шрифтов по структуре L0GF0NT ориентировано на работу с растровыми и векторными шрифтами. Шрифты TrueType и ОрепТуре содержат гораздо более подробную информацию о характеристиках физического шрифта. Метрические данные шрифтов TrueType/OpenType хранятся в таблице метрик OS/2 и Windows, похожей на структуру GDI OUTLINETEXTMETRIC. Самым важным фактором при подборе физического шрифта является набор символов. Хотя большинство шрифтов поддерживает набор ANSI, символы других языков иногда поддерживаются лишь незначительной долей шрифтов, установленных в системе. Например, шрифты очень редко поддерживают декоративные знаки. Когда приложение запрашивает конкретный набор символов, GDI прикладывает все усилия к тому, чтобы найти шрифт с поддержкой именно этого набора; в противном случае символы могут выводиться совершенно неверными глифами. Растровые и векторные шрифты поддерживают лишь один набор символов; шрифт TrueType может поддерживать десятки наборов. В каждом шрифте TrueType/OpenType хранится 64-разрядное поле флагов с определением кодировок, поддерживаемых шрифтом. Очень большое внимание также уделяется точности вывода. Этот показатель ограничивает кандидатов определенными типами шрифтов. Например, 0UT_ OUTLINEPRECIS отдает предпочтение контурным шрифтам. Моноширинные шрифты по внешнему виду сильно отличаются от пропорциональных, поэтому тип шрифта также является важным фактором при подстановке. Первостепенное внимание уделяется и имени гарнитуры. Обнаружив физический шрифт с точным совпадением имени гарнитуры, у которого совпадают другие важные факторы (набор символов, высота, курсивное начертание и насыщенность), подсистема подстановки шрифтов GDI прекращает дальнейшие поиски. В системном реестре хранится список синонимов для имен гарнитур, заданных пользователем. Например, этот список может сообщить системе подстановке шрифтов, что «Helv» является синонимом «MS Sans Serif», «MS Shell Dig» — синонимом «Microsoft Sans Serif», a «Times» — синонимом «Times New Roman». Другими важными факторами, учитываемыми в процессе подстановки для растровых шрифтов, является семейство шрифта, высота, ширина и аспектное отношение. Для контурных шрифтов насыщенность шрифта, подчеркивание, перечеркивание, высота, ширина и аспектное отношение уже не столь существенны. Система подстановки шрифтов PANOSE Как видите, процесс подбора шрифтов по данным L0GF0NT выглядит вполне логично. Однако в нем учитываются лишь те данные, которые передаются при вызовах CreateFont/CreateFontlndirect и хранятся в заголовках растровых и векторных шрифтов. Если документ пересылается на компьютер с другим набором шрифтов, ситуация значительно усложняется. Допустим, документ хранит в структуре L0GF0NT информацию о шрифте с гарнитурой Antique Olive Compact. Как следует действовать GDI, чтобы подобрать правильный физический шрифт?
812 Глава 15. Текст Хотя структура OUTLINETEXTMETRIC шрифтов ТшеТуре/OpenType содержит копию простой структуры TEXTMETRIC, основным средством классификации шрифтов является структура PANOSE. Система подстановки шрифтов PANOSE предназначена для классификации и подбора шрифтов в соответствии с их внешним видом. В настоящее время шрифты TrueType используют технологию PANOSE 1.0, которая описывает шрифт 10 однобайтовыми характеристиками: typedef struct tagPANOSE { BYTE bFamilyType; BYTE bSerifStyle; BYTE bWeight; BYTE bProportion; BYTE bContrast; BYTE bStrokeVariation; BYTE bArmStyle; BYTE bLetterForm; BYTE bMidline; BYTE bxHeight; } PANOSE, * LPPANOSE; В отличие от структуры L0GF0NT, в которой к внешнему виду шрифта относятся всего два поля (IfWeight и IfPitchAndFamily), структура PANOSE закладывает основу для более точной подстановки шрифтов. В частности, в PANOSE определяются 14 разных стилей засечек — квадратные, треугольные, закругленные и т. д. Структура PANOSE обеспечивает компактный и эффективный способ классификации и подстановки шрифтов в системе. Для каждого шрифта TrueType/ ОрепТуре заполняется структура PANOSE, и степень сходства двух шрифтов оценивается по «расстоянию» между соответствующими точками 10-мерного пространства характеристик шрифтов. Технология PANOSE 2.0 идет еще дальше — для описания шрифтов в ней используется 36 значений. За дополнительной информацией о системе подстановки шрифтов PANOSE обращайтесь по адресу www.fonts.com/hp.panose/index.htm. Хотя технология PANOSE обеспечивает значительно лучший результат, чем подстановка шрифтов на основании структур L0GF0NT и TEXTMETRIC, в GDI не существует функций для ее непосредственной поддержки. Функции CreateFont, CreateFontIndirect и CreateFontlndirectEx не используют структуру PANOSE при определении логического шрифта. На самом деле работа алгоритма подстановки шрифтов PANOSE основана на СОМ-интерфейсе IPANOSEMapper, реализованном в одной из малоизвестных системных библиотек panmap.dll. В частности, этот интерфейс используется приложением Fonts (Шрифты) панели управления, когда пользователь запрашивает группировку схожих шрифтов. На рис. 15.3 показана система подстановки шрифтов PANOSE в действии. На рисунке показано, как выглядит окно приложения при выборе команды View ► List Fonts by Similarity (Вид ► Группировать схожие шрифты). Если выбрать в качестве эталона шрифт Tahoma, то шрифт Verdana будет обозначен как «очень похожий», шрифт Arial — «весьма похожий», а шрифт Courier — «не похожий». Для некоторых шрифтов в списке имеет место запись о недоступности сведений (то есть данные PANOSE отсутствуют).
Логические шрифты 813 9# £с& $ew ¥$т%т 1<А "* « Si I Фв««Ь <&?<**** &НМоф ] Ш Ш\Ш jj &®$ j Sinia#loT#iQma ■ иищвдЧВДГ Mam© |.CJJ] Tahoma \d\ Verdana Ш Aria| У Arial Black i*j Arial N arrow fifl Rnnkro*nn.HShd* Very similar Very similar Fairly similar Fairly similar Fairly similar F^irln <?imil*r ?11опЩр1и$$2КШепЗ Рис 15.3. Механизм PANOSE в приложении панели управления В листинге 15.2 приведен простой класс для работы с интерфейсом IPANOSEMapper. Листинг 15.2. Класс KFontMapper: использование интерфейса IPANOSEMapper class KFontMapper { IPANOSEMapper * m_pMapper; const PANOSE * m_pFontList: int mjiFontNo; public: KFontMapper(void) { m_pMapper = NULL; m_pFontList = NULL; mjiFontNo = 0; CoInitialize(NULL); CoCreateInstance(CLSID_PANOSEMapper, NULL. CLSCTX_INPROC_SERVER, IID_IPANOSEMapper, (void **) & m_pMapper); void SetFontList(const PANOSE * pFontList. int nFontNo) { m_pFontList = pFontList; m nFontNo = nFontNo: int PickFonts(const PANOSE * pTarget, unsigned short * pOrder, unsigned short * pScore, int nResult) / Продолжение &
814 Глава 15. Текст Листинг 15.2. Продолжение m_pMapper->vPANRelaxThreshold(); int rslt = m_pMapper->unPANPickFonts( pOrder, // Порядок (от лучшего к худшему) pScore, // Результат поиска (BYTE *) pTarget, // Метрика PANOSE для сравнения nResult. // Количество возвращаемых шрифтов (BYTE *) m_pFontList, // Метрика PANOSE первого шрифта mjiFontNo. // Количество сравниваемых шрифтов sizeof(PANOSE), рТа rget->bFami1уТуре); m_pMapper->bPANRestoreThreshold(); return rslt; } -KFontMapper0 { if ( m_pMapper ) m_pMapper->Release(); CoUninitializeO; Помимо конструктора и деструктора, класс KFontMapper содержит две функции. Функция SetFontList заполняет массив структур PANOSE для доступных шрифтов. Функция PickFonts получает метрику PANOSE и пытается найти для нее хорошие совпадения. Результаты возвращаются в двух массивах — шрифтов и расстояний между исходной структурой PANOSE и подобранными вариантами. Чтобы использовать класс KFontMapper, необходимо решить две проблемы. Первая — определение метрики PANOSE для шрифта, которому вы подбираете замену. Вторая — построение базы данных с метриками PANOSE для всех доступных шрифтов в системе. В одном из возможных решений метрика PANOSE сохраняется вместе со структурой L0GF0NT в документе. При создании логического шрифта и его выборе в контексте устройства GDI подбирает для логического шрифта физический шрифт, установленный в системе. Функция GetOutlineTextMetric GDI возвращает структуру OUTLINETEXTMETRIC для физического шрифта. В поле otmPanoseNumber этой структуры хранится метрика PANOSE. Метрика PANOSE сохраняется в форматах RTF (Rich Text Format) и EMF (Enhanced Metafile). Формат RTF используется расширенными текстовыми полями, исходными справочными файлами системы Windows, такими приложениями, как WordPad и даже Microsoft Word. В MSDN Knowledge Base имеется статья с упоминанием о дефекте Word 97. Хотя формат RTF, используемый в Word 97, сохраняет метрики PANOSE со шрифтами, при подстановке отсутствующих шрифтов Word 97 эти метрики игнорирует. Если провести поиск слова «PANOSE» в заголовочном файле wingdi.h GDI, выясняется, что оно используется в структуре EXTLOGFONT. Структура EXTLOGFONT является расширением L0GF0NT с полными именами гарнитуры и стиля, идентификатором разработчика, метрикой PANOSE и т. д. Таким образом, структура
Логические шрифты 815 содержит информацию как о логическом, так и о физическом шрифтах. Как ни странно, ни одна документированная функция GDI не получает и не возвращает структуру EXTL0GF0NT. Существует лишь одно документированное применение EXTL0GF0NT - в структуре EMREXTCREATEFONTINDIRECTW, используемой для записи команды создания логического шрифта в формате EMF. Задача построения базы данных чисел PANOSE для всех доступных шрифтов может показаться простой. В главе 14 мы выяснили, как при помощи функции EnumerateFontFamiliesEx получить список всех семейств шрифтов в системе. Для каждого семейства EnumerateFontFamiliesEx вызывает функцию, переданную приложением, и передает ей структуру NEWTEXTMETRICEX, в которой среди прочих интересных данных хранится поле для метрики PANOSE. Но проблема заключается в том, что в этих функциях перечисляются не физические шрифты, а семейства шрифтов, причем каждое семейство обычно включается в список несколько раз для каждой поддерживаемой кодировки. Например, в семейство Arial входят четыре разных шрифта: Arial, Arial Bold, Arial Bold Italic и Arial Italic, однако функция EnumerateFontFamiliesEx считает их за одно семейство Arial, которое включается в список 9 раз для каждой поддерживаемой модификации (латиница, иврит, арабский, греческий, турецкий, прибалтийский, центрально- европейский, кириллица и вьетнамский). Конечно, шрифт Arial заметно отличается от Arial Bold Italic, но функция EnumerateFontFamiliesEx выводит только один шрифт семейства и скрывает все остальные. Если вы воспользуетесь ей для заполнения базы данных PANOSE, база данных получится неполной. Собственно, такая база данных уже хранится в реестре Windows по ключу HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Panose Список установленных физических шрифтов хранится в реестре по ключу SOFTWAREWMicrosoftWWindows NTWCurrentVersionWFonts В результате перебора списка физических шрифтов можно получить имена гарнитур шрифтов и отфильтровать их, оставив только шрифты TrueType/ ОрепТуре. По имени гарнитуры вы создаете логический шрифт, выбираете его в контексте устройства и отображаете на физический шрифт. Если найденный физический шрифт является шрифтом TrueType/OpenType, его метрику PANOSE можно получить функцией GetOutlineTextMetrics. Функция, приведенная в листинге 15.3, по имени гарнитуры возвращает метрику PANOSE и имя подставленной гарнитуры. Методика перечисления шрифтов рассматривается в главе 14. Листинг 15.3. Получение метрики PANOSE по имени гарнитуры // 'Arial Bold Italic' -> PANOSE bool GetPANOSECHDC hDC. const TCHAR * full name, PANOSE * panose. TCHAR facenameCJ) { TCHAR name[MAX_PATH]; // Удалить начальные пробелы while (fullname[0]==' ') full name ++; Продолжение^
816 Глава 15. Текст Листинг 15.3. Продолжение _tcscpy(name. full name); // Удалить завершающие пробелы for (int i=_tcslen(name)-l; (i>=0) && (name[i]==' '); i--) name[i] = 0; LOGFONT If; memset(&lf. 0, sizeof(lf)); If.lfHeight « 100; If.lfCharSet - DEFAULT_CHARSET; If.IfWeight = FW_REGULAR; if ( strstr(name, "Italic") ) If.lfltalic = TRUE; if ( strstr(name, "Bold") ) If.IfWeight - FW_B0LD; _tcscpy(lf.IfFaceName, name); HFONT hFont = CreateFontIndirect(& If); if ( hFont==NULL ) return false; HGDIOBJ hOld = SelectObjectChDC. hFont); { KOutlineTextMetric otm(hDC); if ( otm.GetName(otm.m_pOtm->otmpFaceName) ) { _tcscpy(facename. otm.GetName(otm.m_pOtm->otmpFaceName) ); * panose = otm.m_pOtm->otmPanoseNumber; } else facename[0] = 0; } SelectObjectChDC. hOld); DeleteObject(hFont); return facename[0] != 0; } На рис. 15.4 показано окно PANOSE Font Matching демонстрационной программы Font. Основное место в нем занимает список всех доступных шрифтов и их атрибутов. Если щелкнуть правой кнопкой мыши на одной из строк списка, метрика PANOSE этого шрифта сравнивается с метриками PANOSE всех остальных шрифтов. В окне, расположенном в правой части рисунка, приведены результаты сопоставления для шрифта Courier New. Обратите внимание: у шрифта MingLiU нет метрики PANOSE, что приводит к некоторому искажению результатов. Этот шрифт следовало бы исключить из массива чисел PANOSE. Из ри-
Получение информации о логическом шрифте 817 сунка видно, что шрифт Courier New близок к шрифтам Andale Mono, Lucida Console, Georgia и Palatino Linotype. ШШШШ^^У^'У ' ' \im&*№. \ Courier New j Courier New Bold J Courier New Bold Italic 1 Courier New Italic 1 Lucida Console J Lucida Sans Unicode 1 Times New Roman 1 Times New Roman В... 1 Times New Roman В... I Times New Roman It... 1 Wingdings I Symbol 1 Verdana м fmb Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Pictorial Pictorial Text and Display 3 $*fl Thin Thin Thin Thin Normal Sans Normal Sans Cove Cove Cove Cove Any Obtuse Sq Normal Sans J } Vtafcrt* Light Medium Medium Thin Medium Medium Medium Bold Demi Book Any No Fit Medium j Рщийж Monospaced Mono:f£»fo> МОПО:^ МОПО: МОПО: Old St Mode* Mode* Mode* Mode* Any Old St Even' .УЛ-. n 1 a 3 4 5 S 7 8 a ,~iafxi I CorWil None | 0 24 35 38 5B 12 71 m ш 190 зй\ Cowrie? New з €ounef^«wBdcl ! PW»*UU ШдШ ; €out&*te#M*$c Andab Mono £шш Hew Bd4 ЫЬ iucida Consols Ошд» Pa&inoUttu^pfc OK j Рис. 15.4. Иллюстрация системы подстановки шрифтов PANOSE Качество замены отсутствующих шрифтов можно повысить несколькими способами. Например, в процессе построения документа наряду со структурой L0GF0NT можно сохранить метрику PANOSE вместе с другими данными физического шрифта. За образец можно взять структуру EXTL0GF0NT, используемую в EMF. При открытии документа на другом компьютере приложение проверяет сходство физического шрифта, предложенного GDI, с физическим шрифтом, использовавшимся на исходном компьютере. Степень сходства оценивается сравнением полных имен шрифтов или их чисел PANOSE. Если приложение решает, что исходный шрифт в системе отсутствует, а предложенная замена неприемлема, оно выполняет подстановку самостоятельно. Для этого оно может построить собственную базу данных чисел PANOSE для всех доступных шрифтов, найти оптимальную замену при помощи класса KFontMapper или другого средства и затем заново создать логические шрифты для лучших кандидатов, найденных на локальном компьютере. Получение информации о логическом шрифте После того как созданный объект логического шрифта выбран в контексте устройства, GDI подбирает для него физический шрифт из числа установленных в системе. Когда это происходит, логический объект ассоциируется с реализованным шрифтом. Реализованный шрифт не стоит путать с физическим; это всего лишь конкретный экземпляр физического шрифта с конкретным размером, ас- пектным отношением, углом поворота, имитацией особых возможностей и т. д. Например, физический шрифт Times New Roman в конкретный момент времени может существовать в нескольких воплощениях; одно будет соответствовать ло-
818 Глава 15. Текст гическому шрифту с кеглем 12 для экрана с разрешением 96 dpi, другое — логическому шрифту с кеглем 24 для принтера с разрешением 300 dpi, повернутому на 30 градусов. Реализация физического шрифта — сложная и длительная операция, требующая больших затрат памяти. Как было показано в главе 14, шрифты TrueType имеют весьма сложную структуру, и анализ и поиск в их исходной форме весьма затруднены. Чтобы найти глиф для символа конкретной кодировки, символ приходится преобразовывать в Unicode и затем находить индекс глифа по специальной таблице. В процессе построения глифа его контуры масштабируются по заданным размерам с учетом инструкций, хранящихся в шрифте, а затем преобразуются в растровую форму. Примитивная реализация, при которой вывод каждого символа начинается с отображения кода символа на индекс глифа, окажется непомерной нагрузкой для быстродействия системы. На практике графический механизм поддерживает сложные структуры данных, описывающие соответствие между объектами логических шрифтов и физическими шрифтовыми файлами. В Windows NT/2000 для каждого используемого физического шрифта в адресном пространстве ядра создается структура данных (PFF), содержащая связный список реализаций шрифта (FONTOBJ). В каждой структуре FONTOBJ имеется кэш построенных глифов для их повторного использования. В каждом объекте логического шрифта имеется указатель на недокументированный объект GDI — объект PFE, содержащий ссылки на соответствующие объекты PFF. Эта сложная организация данных обеспечивает возможность эффективного кэширования реализованных шрифтов и их многократного использования на системном уровне, одновременно позволяя GDI свободно создавать, использовать и удалять логические шрифты. Внутренние структуры данных графического механизма, относящиеся к работе со шрифтами, подробно описаны в главе 3. После того как логический шрифт выбран в контексте устройства, вы можете получить дополнительную информацию о подобранном физическом шрифте и о метриках текущей реализации физического шрифта. Для этого используются следующие функции: int GetTextFace(HDC hDC. int nCount, LPSTR lpFaceName); DWORD GetFontLanguagelnfoCHDC hDC); int GetTextCharSeKHDC hDC); int GetTextCharSetInfo(HDC hDC. LPFONTSIGNATURE IpSig. DWORD dwFlags): BOOL GetTextMetrics(HDC hDC, LPTEXTMETRIC Iptm); UINT Get0ut1ineTextMetrics(HDC hDC. UINT cData. LPOUTLINETEXTMETRIC IpOtm); Функция GetTextFace возвращает имя гарнитуры физического шрифта, соответствующего логическому шрифту в контексте устройства (может отличаться от имени гарнитуры, использованного при создании логического шрифта). Функция GetFontLanguagelnfo возвращает информацию о текущем шрифте, выбранном в контексте устройства, в том числе о поддержке двухбайтовых глифов, о присутствии диакритических знаков, о наличии таблицы кернинга и т. д. Информация, полученная при помощи этой функции, часто применяется при нетривиальном форматировании текста (например, при непосредственной работе с глифами с использованием функций GetCharacterPl acement или ExtTextOut).
Получение информации о логическом шрифте 819 Функция GetFontLanguagelnfo возвращает комбинацию нескольких флагов, самыми распространенными из которых являются FLIGLYPHS (0x40000) и GCP_ USERKERNING (0x0008). Флаг FLIGLYPHS означает, что шрифт содержит дополнительные глифы, к которым невозможно обратиться через текущую кодировку. Флаг GCPJJSERKERNING означает, что в шрифте присутствует таблица кернинга. Если на компьютере реализована поддержка арабского языка или иврита, для некоторых шрифтов TrueType функция GetFontLanguagelnfo возвращает флаги GCP_RE0RDER, GCP_GLYPHSHARE, GCP_LIGATE, GCPJHACRITIC и GCP_KASHIDA. Например, шрифты Arial, Lucida Sans Unicode, Tahoma и Andalus поддерживают GCP_ REORDER, a «Times New Roman» не поддерживает. Функция GetTextCharset возвращает идентификатор набора символов для текущего шрифта, выбранного в контексте устройства. По полученному значению можно проверить, поддерживает ли физический шрифт набор символов, необходимый для логического шрифта. Например, если при создании логического шрифта был запрошен набор HANGULCHARSET, но в вашей системе нет ни одного корейского шрифта, GDI может выбрать набор символов по умолчанию; в этом случае функция GetTextCharset вернет ANSICHARSET. Функция GetTextCharset Info возвращает структуру F0NTSIGNATURE с информацией о поддиапазонах Unicode и кодировках, поддерживаемых шрифтом True- Туре/OpenType. Первые четыре двойных слова F0NTSIGNATURE образуют 128-разрядное поле USB (Unicode subset bitfield), а два оставшихся двойных слова образуют 64-разрядное поле СРВ (code page bitfield). Например, бит 9 USB показывает, поддерживаются ли глифы кириллицы, а бит 16 СРВ является признаком поддержки кодировки 874 (тайская). Для шрифта Tahoma в СРВ устанавливаются 12 бит, поскольку этот шрифт содержит глифы 12 разных кодировок. Функции GetTextMetrics и GetOutlineTextMetrics возвращают важные метрические данные о текущей реализации физического шрифта. Функция GetTextMetrics возвращает структуру TEXTMETRIC. Функция GetOutlineTextMetrics возвращает для шрифтов TrueType/OpenType структуру 0UTLINETEXTMETRIC — расширенную версию TEXTMETRIC, содержащую дополнительную информацию. Метрики растровых и векторных шрифтов Структура TEXTMETRIC изначально разрабатывалась для заголовков растровых и векторных шрифтов Windows, но она подходит и для шрифтов TrueType/Open- Type. Чтобы получить заполненную структуру TEXTMETRIC для текущего шрифта, выбранного в контексте устройства, вызовите функцию GetTextMetrics с манипулятором контекста устройства и указателем на TEXTMETRIC. Структура TEXTMETRIC определяется следующим образом: typedef struct tagTEXTMETRIC { LONG tmHeight; LONG tmAscent; LONG tmDescent: LONG tmlnternalLeading; LONG tmExternalLeading; LONG tmAveCharWidth;
820 Глава 15. Текст LONG LONG LONG LONG LONG BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE } TEXTMETRIC; tmMaxCharWidth; tmWeight; tmOverhang; tmDigitizedAspectX; tmDigitizedAspectY; tmFirstChar; tmLastChar; tmDefaultChar; tmBreakChar; tmltalic: tmUnderlined; tmStruckOut; tmPitchAndFamily: tmCharSet; Поле tmAscent определяет надстрочный интервал (высоту символа над базовой линией). Поле tmDescent определяет подстрочный интервал (высоту части символа, находящейся ниже базовой линии), а поле tmHeight определяет общую высоту символа (tmAscent+tmDescent) — см. рис. 15.1. Поле tmlnternalLeading определяет величину внутреннего зазора — промежутка, в котором обычно размещаются акценты и диакритические знаки, а поле tmExternalLeading определяет внешний зазор — рекомендуемый дополнительный интервал между строками. Разность между tmHeight и tmlnternal Leading соответствует кеглю шрифта — стандартной метрике размера символов. Например, для шрифта с кеглем 36 пунктов в экранном контексте с разрешением 96 dpi и в режиме ММ_ТЕХТ поле If Height структуры LOGFONT будет равно -48 (36 х 96,72). Если создать по этой структуре LOGFONT логический шрифт и выбрать его в экранном контексте устройства, поле tmHeight структуры TEXTMETRIC, возвращаемой функцией GetTextMetrics, будет равно 55, а поле tmlnternal Leading — 7. Разность между ними равна 48 — абсолютной величине поля If Height структуры LOGFONT. Для растровых шрифтов при запрещенном масштабировании (флаг PROOF_QUALITY в поле If Quality структуры LOGFONT) GDI иногда не удается точно подобрать шрифт для заданного размера. Например, для 10-пунктового шрифта гарнитуры Terminal поле If Height в структуре lfHeight равно -13, но поле tmHeight может быть равно 12 при внутреннем зазоре, равном 0. Если не удается подобрать растровый шрифт заданного размера, обычно используется меньший шрифт. Сумма tmHeight и tmExternal Leading составляет рекомендуемый межстрочный интервал. Высота п строк текста вычисляется по формуле tmHeight x n + tmExternal Leading x (n - 1), поскольку перед первой и за последней строкой дополнительный промежуток не нужен. Допустим, у шрифта с кеглем 36 пунктов в экранном контексте с разрешением 96 dpi и в режиме ММТЕХТ поле tmHeight равно 55, а поле tmExternal - Leading равно 2. Общая высота строки равна 57 единицам или 42,75 пункта. Следует помнить, что разные шрифты одного кегля могут иметь разные значения полей tmHeight и tmExternal Leading или их суммы. Поле tmAveCharWidth определяет среднюю ширину символов шрифта. В документации Microsoft сказано, что средняя ширина обычно равна ширине строч-
Получение информации о логическом шрифте 821 ной буквы «х». В шрифтах ТшеТуре/OpenType средняя ширина символов более точно вычисляется как взвешенная сумма ширин строчных букв a-z латинского алфавита и пробела. В моноширинных шрифтах все символы имеют одинаковую ширину, и поле tmAveCharWidth может использоваться для вычисления длины символьной строки. Средняя ширина символов также используется при подборе шрифтов. В поле tmMaxCharWidth хранится максимальная ширина символов шрифта. Поле tmWeight определяет насыщенность шрифта и совпадает с полем lfWeight структуры LOGFONT. Следует учитывать, что все ресурсы растровых и векторных шрифтов, а также физические шрифты TrueType обладают фиксированной насыщенностью. Гарнитура TrueType обычно содержит четыре физических начертания — обычное, курсивное, полужирное и полужирное курсивное. Насыщенность первых двух начертаний обычно равна 400 (FW_N0RMAL), а двух последних — 700 (FWBOLD). GDI пытается найти оптимальное соответствие для заданной насыщенности среди доступных шрифтов. Если точное совпадение найти не удается, GDI имитирует нужную насыщенность при помощи простого алгоритма. Поле tmOverhang определяет величину дополнительных промежутков, используемых GDI при синтезе шрифтов. Если требуемая насыщенность превышает доступную, GDI утолщает символы; если запрашивается курсивное начертание, а физический курсивный шрифт недоступен, GDI слегка наклоняет глифы. В любом случае размер строки немного увеличивается. Поле tmOverhang позволяет приложению точно рассчитать горизонтальные размеры строки символов, чтобы она не вышла за пределы отведенного ей места. На практике у растровых и векторных шрифтов это поле отлично от нуля, а у шрифтов TrueType/Open- Type оно всегда равно нулю. Например, для курсивного шрифта Wingdings с кеглем 72 пункта, имеющего только обычное физическое начертание, в поле tmOverhang возвращается 0. Поле tmFirstChar определяет первый символ, для которого в шрифте имеется глиф; поле tmLastChar определяет последний символ. Оба поля объявляются с типом BCHAR (WCHAR в Unicode, BYTE в остальных кодировках). Растровые и векторные шрифты содержат глифы для всех символов в интервале от tmFirstChar до tmLastChar. В шрифтах TrueType/OpenType однобайтовых версий полей tmFirstChar и tmLastChar недостаточно для представления истинного интервала символов, для которых в шрифте имеются глифы, а Unicode-версии полей не означают, что шрифт содержит глифы для всех символов между tmFirstChar и tmLastChar. Поле tmDefaultChar определяет символ замены для символов, не имеющих глифа в шрифте (обычно это прямоугольная рамка). В шрифтах TrueType по умолчанию обычно используется первый глиф с индексом 0. Поле tmBreakChar задает символ, по которому определяются границы слов при выравнивании текста. Следующие три поля повторяют поля LOGFONT с аналогичными именами. Поле tmltalic отлично от нуля, если требуется курсивное начертание; поле tmUnderline отлично от нуля, если требуется подчеркивание символов, а поле tmStruckOut отлично от нуля, если требуется перечеркивание символов. Помните, что эти поля отражают лишь требования, указанные в объекте логического шрифта, а не характеристики физического шрифта. Начертания, отсутствующие в физическом шрифте, синтезируются средствами GDI.
822 Глава 15. Текст Поле tmPitchAndFamily задает тип, технологию и семейство физического шрифта. Старшая половина поля совпадает со старшей половиной поля IfPitchAndFamily структуры L0GF0NT. Младшая половина состоит из четырех независимых флагов и отличается от младшей половины IfPitchAndFamily. О TMPFFIXEDPITCH (0x01) — устанавливается для пропорциональных шрифтов. Все поставлено с ног на голову — почему было не назвать это поле TMPFPROPPITCH? О TMPF_VECT0R (0x02) — устанавливается для контурных шрифтов (проще говоря, для всех, кроме растровых). О TMPF_TRUETYPE (0x04) — устанавливается для шрифтов TrueType/OpenType. О TMPF_DEVICE (0x08) — устанавливается для шрифтов устройств. Последнее поле tmCharSet определяет набор символов логического шрифта. Его значение совпадает с полем IfCharSet структуры L0GF0NT (если это не DEFAULT_ CHARSET) и с возвращаемым значением функции GetTextCharSet. Шрифт TrueType/ ОрепТуре обычно поддерживает несколько наборов символов, информацию о которых можно получить при помощи функции GetTextCharlnfo. Метрики шрифтов TrueType/ОрепТуре Шрифты TrueType/OpenType обладают значительно большим количеством метрик, для которых была разработана структура 0UTLINETEXTMETRIC. Приставка «outline» может вызвать недоразумения, поскольку эта структура не относится к векторным шрифтам — только к шрифтам TrueType/OpenType и шрифтам устройств, для которых шрифтовой драйвер может предоставить структуру OUTLINE- TEXTMETRIC. Ниже приведено определение структуры 0UTLINETEXTMETRIC. typedef struct JXJTLINETEXTMETRIC { UINT otmSize; TEXTMETRIC otmTextMetrics; BYTE otmFiller; PANOSE otmPanoseNumber; UINT otmfsSelection; UINT otmfsType; int otmsCharSlopeRise; int otmsCharSlopeRun; int otmltalicAngle; UINT otmEMSquare; i nt otmAscent; int otmDescent; UINT otmLineGap; UINT otmsCapEmHeight; UINT otmsXHeight; RECT otmrcFontBox; int otmMacAscent; int otmMacDescent; UINT otmMacLineGap: UINT otmusMinimumPPEM; P0INT otmptSubscri ptSi ze; POINT otmptSubscri ptOffset; POINT otmptSuperscriptSize;
Получение информации о логическом шрифте 823 POINT otmptSuperscriptOffset; UINT otmsStrikeoutSize; int otmsStrikeoutPosition; int otmsUnderscoreSize; int otmsUnderscorePosition; PSTR otmpFamilyName; PSTR otmpFaceName: PSTR otmpStyleName; PSTR otmpFullName; } OUTLINETEXTMETRIC; После того как манипулятор логического шрифта выбран в контексте устройства (при условии, что ему соответствует физический шрифт TrueType/ ОрепТуре), можно вызвать функцию GetOutlineTextMetrics и получить от GDI заполненную структуру OUTLINETEXTMETRIC. Хотя структура OUTLINETEXTMETRIC выглядит не такой уж сложной (если не считать большого количества полей), эта простота обманчива. Описание OUTLINETEXTMETRIC в документации Microsoft оставляет желать лучшего. Во-первых, эта структура имеет переменный размер, поскольку ее последние четыре поля содержат смещения строк, которые обычно присоединяются к блоку данных за последним полем. Здесь же кроется и вторая хитрость: в последних четырех полях хранятся не указатели, как указано в объявлении, а смещения относительно начала блока данных. Последнее поле называется otmSize, и по названию можно предположить, что перед вызовом GetOutlineTextMetrics в это поле следует занести размер структуры. В действительности этого делать не нужно, поскольку размер блока данных передается во втором параметре GetOutlineTextMetrics. Поскольку структура OUTLINETEXTMETRIC имеет переменный размер, функцию GetOutlineTextMetrics приходится вызывать дважды. При первом вызове вы получаете реальный размер структуры, выделяете блок нужного размера и передаете его при втором вызове для получения данных. На прилагаемом компакт- диске имеется класс KOutlineTextMetric, предназначенный для получения структуры OUTLINETEXTMETRIC. Ниже приведено объявление этого класса. class KOutlineTextMetric { public: OUTLINETEXTMETRIC * m_pOtm; KOutlineTextMetricCHDC hDC); -KOutlineTextMetricO; }: Конструктор класса KOutlineTextMetric получает структуру OUTLINETEXTMETRIC в блок памяти, выделенный из кучи. Деструктор освобождает память при выходе экземпляра класса из области видимости. Если вы смотрели программный код этого класса, возможно, вы обратили внимание на странную проверку смещения поля otmFiller. Самое коварное свойство структуры OUTLINETEXTMETRIC заключается в том, что ее поля должны выравниваться по границе двойных слов. Это ограничение не документировано и не форсируется заголовочными файлами Windows. Автор в течение нескольких дней пытался понять, почему структура OUTLINETEXTMETRIC, возвращаемая функцией GetOutlineTextMetrics, не соответствует объявлению. Ответ удалось найти лишь при просмотре двоичного дампа данных.
824 Глава 15. Текст Второе поле OUTLINETEXTMETRIC содержит структуру TEXTMETRIC длиной 4п + 1 байт. Разработчик этой структуры добавил однобайтовый заполнитель (otmFiller), чтобы следующая структура PANOSE выравнивалась по границе слова. Следует учитывать, что эта структура разрабатывалась для Windows 3.1 Win 16 API, когда в Windows впервые появилась поддержка шрифтов TrueType. Вероятно, при компиляции исходных текстов GDI было задано выравнивание полей структур по границе 4 байт. В результате поле otmFiller оказалось выровненным по границе двойного слова, а перед ним добавились три байта. Структура PANOSE имеет длину 10 байт; следующее поле otmfsSelection должно начинаться с границы двойного слова, поэтому перед ним добавляются еще два байта. Обычно при специальном выравнивании полей структуры в заголовочные файлы Windows включается директива #pragma pack, которая обеспечивает нужный тип выравнивания и отменяет тип выравнивания, заданный в пользовательской программе. Это гарантирует, что приложение будет работать с одними и теми же структурами Win32 API независимо от конфигурации проекта. В файле wingdi.h для структуры OUTLINETEXTMETRIC такая директива отсутствует. На момент написания книги выравнивание по границе DWORD в заголовочном файле имело место лишь в случае определения макроса _МАС. Если в вашем проекте требуется выравнивание полей структур по границе 1 или 2 байт и вы хотите использовать структуру OUTLINETEXTMETRIC, обязательно перейдите на выравнивание по границе двойного слова: #pragma pack(push, 4) #include<windows.h> #pragma pack(pop) Первое поле otmSize определяет размер всей структуры вместе с внедренными строками. За ним следует структура TEXTMETRIC (см. выше), в точности совпадающая с той, которая возвращается при вызове GetTextMetric. Третье поле otmFiller предназначалось для выравнивания следующей за ним структуры PANOSE по границе слова. Похоже, исходные тексты GDI компилировались с выравниванием полей структур по границе 4 или 8 байт, в результате чего после otmTextMetrics и перед otmFiller появились три скрытых байта. В поле otmPanoseNumber хранится метрика PANOSE для физического шрифта. За ним следуют два скрытых байта, обеспечивающих выравнивание следующего поля по границе двойного слова. Поле otmfsSelection описывает начертание шрифта, используя для этого комбинацию 6 флагов. Бит 0 (0x01) устанавливается для курсивного шрифта, бит 1 (0x02) — для подчеркивания, бит 2 (0x04) — для негатива, бит 3 (0x08) — для контурного шрифта, бит 4 (0x10) — для перечеркивания, бит 5 (0x20) — для полужирного и бит 6 (0x40) — для обычного начертания. У шрифта TrueType имеется атрибут с похожим именем, по которому можно определить исходный дизайн физического шрифта. Например, установка битов 5 и 6 означает, что физический шрифт является полужирным (то есть полужирное начертание не синтезировано средствами GDI). Похоже, GDI использует это поле несколько иначе; otmfsSelection представляет характеристики логического, а не физического шрифта, поэтому по значению поля otmfsSelection вы не сможете узнать, является ли физический шрифт полужирным. Надежным источником информации о характеристиках физического шрифта является структура PANOSE; например, по содержимому поля bLetterForm можно определить, является ли шрифт курсивным.
Получение информации о логическом шрифте 825 Поле otmf sType определяет лицензионные права на внедрение шрифта в документы (см. раздел «Установка и внедрение шрифтов» в главе 14). Следующие три поля связаны с выводом текстовой каретки. Для вертикальных шрифтов каретка представляет собой тонкую вертикальную линию, обозначающую позицию ввода следующего символа. В профессиональных текстовых редакторах каретка в курсивном шрифте выводится под углом. Поле otmltalicAngle задает угол наклона шрифта в десятых долях градуса против часовой стрелки от оси у. Для вертикальных шрифтов поле otmltalicAngle равно 0, а для курсивных шрифтов оно обычно содержит отрицательное число. Наклон каретки определяется соотношением полей otmsCharSlopeRise и otmsCharSlopeRun. Для вертикальных шрифтов поле OtmsCharSlopeRise равно 1, а поле OtmsCharSlopeRun равно 0, поэтому каретка выглядит как вертикальная линия. Например, у курсивного шрифта «Times New Roman» поле otmltalicAngle равно -164, поле OtmsCharSlopeRise — 24, а поле OtmsCharSlopeRun — 7. Тангенс 16,4° равен 0,2943, что очень близко к 7/24 (0,2917). Все три характеристики относятся к физическому шрифту; синтез курсивных шрифтов средствами GDI никак не влияет на них. Например, попробуйте поработать с курсивным начертанием шрифта Tahoma, у которого нет физического курсивного шрифта. WordPad выводит вертикальную каретку, а более сообразительный редактор Word — наклонную. Поле otmEMSquare содержит размер em-квадрата физического шрифта. Em-квадратом называется эталонная сетка, по которой конструируются глифы. Все точки в описании глифа представлены целочисленными координатами em-квадрата, поэтому увеличение размера em-квадрата обычно повышает качество глифов. Это поле обычно используется приложениями для определения параметров логической системы координат. За дополнительной информацией об определении глифов TrueType обращайтесь к главе 14. Поле otmAscent определяет типографский надстрочный интервал шрифта, поле otmDescent — типографский подстрочный интервал, а поле otmLineGap — типографский межстрочный интервал. GDI использует собственную интерпретацию надстрочных и подстрочных интервалов, а также внутреннего и внешнего зазора, которая в одних случаях совпадает с типографской интерпретацией, а в других отличается от нее. Еще одно различие состоит в том, что поле otmDescent обычно содержит отрицательную величину, поскольку подстрочный элемент расположен ниже базовой линии, а в Windows всегда используется модуль (абсолютное значение) этой метрики. Группа полей otmMacAscent, otmMacDescent и otmMacLineGap содержит вертикальные метрики шрифта для Macintosh. В документации Microsoft сказано, что поля otmCapEmHeight и otmXHeight не поддерживаются. Вероятно, поле OtmCapEmHeight должно содержать высоту прописной буквы без подстрочного элемента, а поле otmXHeight — высоту строчной буквы «х». Поле otmrcFontBox определяет ограничивающий прямоугольник всех глифов шрифта относительно базовой точки символа. Поле otmrcFontBox.left обычно имеет отрицательное значение, соответствующее минимальной А-метрике, а поле otmrcFontBox.bottom имеет отрицательное значение, соответствующее наибольшему подстрочному элементу. Поле otmusMinimumPPEM определяет минимальный допустимый размер в пикселах, до которого можно уменьшить em-квадрат по рекомендации разработчика
826 Глава 15. Текст шрифта. По значению этого поля можно судить о том, насколько хорошо инструкции привязки подходят для построения при малом размере символов. Обычное значение равно 9 или 12 пикселам. Поля otmptSubscriptSize и otmptSubscriptOffset определяют размер и позицию нижних индексов шрифта от базовой точки символа. Поля otmptSuperscriptSize и otmptSuperscriptOffset содержат аналогичные данные для верхних индексов. Поля otmsStrikeoutSize и otmsStrikeoutPosition определяют толщину горизонтальной черты при перечеркивании символов и ее положение относительно базовой линии. Поля otmsUnderscoreSize и otmsUnderscorePosition определяют толщину и положение относительно базовой линии черты, используемой для подчеркивания. На рис. 15.5 показаны некоторые новые метрики, содержащиеся в структуре OUTLINETEXTMETRIC, — а именно ограничивающий прямоугольник, метрики верхнего/нижнего индексов, подчеркивания, перечеркивания и наклона символов. Пять пунктирных линий обозначают уровни внешнего зазора, надстрочного элемента, внутреннего зазора, базовой линии и подстрочного элемента. Большой контур соответствует ограничивающему прямоугольнику шрифта и кажется слишком большим. Две серые полоски имитируют подчеркивание и перечеркивание. Два прямоугольника справа обозначают базовую точку и размеры верхних/нижних индексов. Наклонная линия представляет угол наклона символов, используемый при выводе каретки. Рис. 15.5. Метрики шрифта в структуре OUTLINETEXTMETRIC Последние четыре поля структуры OUTLINETEXTMETRIC содержат смещения имен семейства, гарнитуры и стиля, а также полного имени физического шрифта относительно начала структуры. Сказанное проще пояснить конкретным примером. Для полужирного курсивного шрифта Times New Roman существует физический шрифт, поэтому четыре последних поля OUTLINETEXTMETRIC содержат смещения строк Times New Roman, Times New Roman Bold Italic, Bold Italic и Monotype Times New Roman Bold Italic Version 2-76 (Microsoft).
Получение информации о логическом шрифте 827 Структура LOGFONT и метрики шрифта Программа Font, прилагаемая к этой главе, поможет вам лучше понять связь между логическими шрифтами, определяемыми структурой LOGFONT, и физическим шрифтом. Эта программа модифицирует стандартное диалоговое окно для выбора шрифта и выводит в нем всю информацию о шрифте. Win32 API содержит функцию ChooseFont для вывода стандартного диалогового окна, в котором пользователь выбирает логический шрифт. Эта функция позволяет приложению переопределить стандартный механизм обработки сообщений в диалоговом окне. Передавая функцию ChooseFont косвенного вызова, мы выводим дополнительную информацию о текущей структуре LOGFONT и метриках текущей реализации физического шрифта. В программе Font на прилагаемом компакт-диске диалоговое окно шрифта расширяется вправо, и в нем выводится иерархическое дерево со всеми атрибутами шрифта. Измененное диалоговое окно выбора шрифта показано на рис. 15.6. В левой части (исходное диалоговое окно выбора шрифта) выбран полужирный шрифт Times New Roman с кеглем 72 пункта. В дочернем иерархическом дереве (Tree- View) выводятся результаты вызовов GetTextFace, GetFontLanguagelnfo, GetTextCharset, GetTextCharset Info, GetTextMetrics и GetOutlineTextMetrics для структуры LOGFONT. На рис. 15.6 структура OUTLINETEXTMETRIC развернута, в ней видны вложенные структуры TEXTMETRIC и PANOSE и несколько начальных полей. Если выделить в левой части окна другую строку и щелкнуть на кнопке Apply, содержимое иерархического дерева синхронизируется с выбранной строкой. Рис. 15.6. Измененное диалоговое окно выбора шрифта с выводом шрифтовых метрик Точность шрифтовых метрик При создании логического шрифта его размеры определяются полями (или параметрами) ширины/высоты. При подборе физического шрифта его метрики em-квадрата масштабируются по размерам логического шрифта и возвращаются
828 Глава 15. Текст в структуре TEXTMETRIC или OUTLINETEXTMETRIC. Когда логический шрифт выбирается в контексте устройства, его ширина и высота интерпретируются в логических координатах данного контекста, поэтому все метрики TEXTMETRIC и OUTLINETEXTMETRIC задаются в логической системе координат данного контекста устройства. Метрические данные используются при разбиении длинного текста на строки в абзацах и при размещении текста на страницах. Если приложение поддерживает печать документа, очень важно, чтобы разрывы строк и страниц на экране точно соответствовали разрывам строк и страниц при печати документа на разных принтерах. Кроме того, разрывы строк и страниц должны сохраняться при выводе экрана в другом масштабе. Это одно из основных требований концепции WYSIWYG (What You See Is What You Get — «что видите, то и получаете»). Допустим, вы пишете простейший текстовый редактор, в котором весь текст выводится одним шрифтом. Возникает вопрос: как по заданному кеглю и высоте страницы вычислить количество строк, помещающихся на странице? Ниже приведен один из возможных вариантов. int LinesPerPageCHDC hDC, int nPointSize, int nPageHeight) { KLogFont lf(-PointSizetoLogical(hDC, nPointSize), "Times New Roman"); HFONT hFont = lf.CreateFontO; HGDIOBJ hOld = SelectObjectChDC. hFont); TEXTMETRIC tm; GetTextMetncsChDC. & tm); int linespace = tm.tmHeight + tm.tmExternalLeading; SelectObjectChDC. hOld); DeleteObject(hFont); POINT P[2] = { 0. 0, 0, nPageHeight }; // Координаты устройства DPtoLP(hDC, P, 2); // Логические координаты nPageHeight = abs(P[l].y-P[0].y); ejinespace = linespace; e_pageheight = nPageHeight; e_externalleading = tm.tmExternalLeading; return (nPageHeight + tm.tmExternalLeading) / linespace; } Функция LinesPerPage получает манипулятор контекста устройства, кегль шрифта и высоту страницы в системе координат устройства (в пикселах). Она преобразует кегль в логические координаты, создает логический шрифт, выбирает его в контексте устройства и запрашивает вертикальные метрики шрифта. Расстояние между двумя строками вычисляется как сумма высоты и внешнего зазора. После преобразования высоты страницы (за вычетом полей) в логические координаты количество строк на странице вычисляется по формуле (высота страницы + внешний зазор)/расстояние между строками. Внешний зазор прибавляется к высоте страницы, поскольку N строк разделяются всего (N - 1) внешними зазорами.
Получение информации о логическом шрифте 829 Следующий вопрос: насколько точны подобные вычисления? В табл. 15.1 приведены примеры данных для высоты страницы, равной 10 дюймам (11-дюймовый лист формата Letter с полями по 0,5 дюйма сверху и снизу). Таблица 15.1. Разрывы строк Устройство Экран (96 dpi) Экран (120 dpi) Принтер (360 dpi) Принтер (600 dpi) Режим отображения ММ_ТЕХТ MML0ENGLISH MMTWIPS MMJEXT MM_L0ENGLISH MMTWIPS MM TEXT MM TEXT Логическая высота страницы 960 1050 15118 1200 1312 18897 3600 6000 Внешний зазор 1 1 9 1 1 И 2 4 Логический межстрочный интервал 16 17 245 20 22 310 59 98 Строк на страницу 60,0625 (60) 61,8235 (61) 61,74286 (61) 60,05 (60) 59,6818 (59) 60,9935 (60) 61,0508 (61) 61,2653 (61) Как видно из таблицы, в зависимости от логического разрешения экрана, режима отображения и устройства (экран или принтер) по шрифтовым метрикам, возвращаемым в структуре TEXTMETRIC, на простой вопрос о количестве строк в области высотой 10 дюймов можно получить три разных ответа: 59, 60 и 61. Обратите внимание: функция LinesPerPage усекает дробный результат до целой части, поскольку неполные строки не выводятся на экране. Даже если бы мы воспользовались округлением до ближайшего целого, все равно получилось бы три разных ответа: 60, 61 и 62. Также следует учитывать, что даже в одном стандартном режиме отображения (MML0ENGLISH или MMTWIPS) на одном и том же компьютере можно получить разные результаты при переходе от режима мелких шрифтов (96 dpi) к режиму крупных шрифтов (120 dpi). Вся суть проблемы заключается в том, что ошибки появляются при масштабировании метрических данных физического шрифта в логические координаты, используемые в контексте устройства (особенно если учесть, что логические координаты представляются целыми числами, как в Windows GDI). Как говорилось выше, кегль шрифта соответствует метрике «надстрочный интервал + подстрочный интервал - внутренний зазор». Для физических шрифтов TrueType эта характеристика совпадает с размером em-квадрата. Таким образом, координаты в описании глифа масштабируются по заданному кеглю по очень простой формуле. Для объекта логического шрифта, созданного функцией CreateFontIndirect, с высотой (надстрочный интервал + подстрочный интервал - внутренний зазор) height и шириной width точка (х,у) в исходном описании глифа масштабируется в точку с координатами (х * width/emsquaresize, у * height/emsquaresize)
830 Глава 15. Текст Подобным образом масштабируются не только точки в описании глифа, но и другие шрифтовые метрики — надстрочные и подстрочные интервалы, толщина перечеркивания и т. д. Дробные результаты преобразуются в целые посредством округления. Например, шрифт Times New Roman определяется в em-квадрате размера 2048, надстрочный интервал равен 1825, подстрочный интервал — 443, а внешний зазор — 87. Для шрифта с кеглем 10 пунктов на экране с разрешением 96 dpi поле высоты в структуре L0GF0NT будет равно -13; функция GetTextMetrics присваивает полю tmAscent значение 12, полю tmDescent — значение 3, а полю tmExternal - Leading — значение 1. Точные значения равны 11 + 1197/2048 (1825 х 13/2048), 2 + 1663/2048 (443 х 13/2048) и 1131/2048 (87 х 13/2048). При округлении до ближайших целых погрешности составляют 0,4155, 0,188 и 0,4478. При большом количестве строк ошибки накапливаются, и иногда это может привести к тому, что на странице вместо 59 строк разместится 60 или даже 61 строка. Хотя погрешность всегда меньше 1, при сравнении с метрическими размерами шрифта относительная погрешность оказывается довольно большой. В приведенном примере целочисленная высота полной строки (tmAscent + tmDescent + tmExternal - Leading) на 6,5 % отличается от ее точного значения. Логическая система координат и точность Существует два возможных пути к повышению точности метрик шрифтов для заданного контекста устройства. Первый способ — определение логической системы координат с высоким логическим разрешением — основан на том, что метрики в структурах TEXTMETRIC и 0UTLINETEXTMETRIC задаются в логических координатах. Например, если переключить экранный контекст с разрешением 96 dpi в режим MM_ANISOTROPIC, задать габариты окна (100,100) и габариты области просмотра (1,1), логическая система координат будет иметь разрешение 96 dpi. Иначе говоря, перемещение на 9600 единиц в логических координатах будет соответствовать 1 логическому дюйму на экране. Казалось бы, такая система координат должна обеспечивать значительно большую точность, чем контексты принтеров с разрешением 600 dpi, получившие столь широкое распространение. Как ни странно, на практике все получается совсем не так. Увеличение разрешения в логической системе координат не приводит к повышению точности шрифтовых метрик. Похоже, графический механизм допускает громадную ошибку, сначала масштабируя метрики в координатах устройства, а затем — в логических координатах. Хотя не исключено, что результаты масштабирования на первом этапе сохраняются в формате с фиксированной точкой, при масштабировании в логическую систему координат с высоким разрешением особых улучшений не наблюдается. Ниже приведена небольшая функция, которая проверяет, как разрешение логической системы координат влияет на точность текстовых метрик. void Test_LC(void) { HDC hDC - GetDC(NULL); SetMapMode(hDC. MM_ANISOTROPIC); SetViewportExtEx(hDC, 1, 1. NULL); TCHAR mess[MAX_PATH];
Получение информации о логическом шрифте 831 mess[0] = 0; for (int i-1: i<=64; i*=2) { SetWindowExtEx(hDC. i. i. NULL); KLogFont lf(-PointSizetoLogical(hDC. 24). "Times New Roman"); HFONT hFont = lf.CreateFontO; SelectObjectChDC. hFont); TEXTMETRIC tm; GetTextMetricsChDC, & tm); wsprintf(mess + _tcslen(mess). "%6:l lfHeight=£d. tmHeight=$d\n". i, If.mjf.IfHeight. tm.tmHeight); SelectObjectChDC. GetStockObject(ANSI_VAR_FONT)); DeleteObjectChFont); } ReleaseDCCNULL. hDC): MessageBoxCNULL. mess. "LCS vs. TEXTMETRIC". MB_0K); } При каждой итерации функция последовательно увеличивает логическое разрешение, создает шрифт с кеглем 24 пункта, выбирает его в контексте устройства и запрашивает высоту шрифта. На экране с логическим разрешением 96 dpi поле IfHeight принимает значения -32, -64 и т. д. до -2048, а поле tmHeight изменяется от 36, 72 до 2304. Значение tmHeight тоже каждый раз удваивается. Из чего же следует, что этот результат неверен? Попробуйте создать шрифт с кеглем 24 х 64 = 1536 пунктов в измененном диалоговом окне выбора шрифта, показанном на рис. 15.6. Поле IfHeight сохраняет то же значение 2048, но поле tmHeight равно 2268, а не 2304. Итак, увеличение разрешения логической системы координат не повышает точности шрифтовых метрик — по крайней мере, в текущей реализации GDI. Кегль и точность Второй способ увеличения логического размера шрифта для повышения точности шрифтовых метрик предельно прост — нужно увеличить кегль шрифта. Ниже приведена аналогичная функция для проверки связи между кеглем и точностью шрифтовых метрик. void Test_Point(void) { HDC hDC = GetDC(NULL); TCHAR mess[MAX_PATH*2]; mess[0] = 0; for (int i-1: i<=64; i*-2) { KLogFont lf(-PointSizetoLogical(hDC. 24*i). "Times New Roman"); HFONT hFont - lf.CreateFontO: SelectObjectChDC. hFont);
832 Глава 15. Текст TEXTMETRIC tm; GetTextMetrics(hDC, & tm); wsprintf(mess + _tcslen(mess), "%6 point lfHeight=%d. tmHeight=Ud\n", 24*i . If.mJf.lfHeight. tm.tmHeight); SelectObject(hDC, GetStockObject(ANSI_VAR_FONT)); DeleteObject(hFont); } ReleaseDC(NULL, hDC); MessageBox(NULL, mess, "Point Size vs. TEXTMETRIC", MB_0K); } При увеличении кегля до 1536 пунктов поле If Height равно -2048, а в поле tmHeight возвращается 2268. Кстати говоря, 2048 — это размер em-квадрата шрифта Times New Roman, a 2268 — фактическая высота шрифта, хранящаяся в таблице метрик физического шрифта TrueType. Мы приходим к неприятному заключению: для получения наиболее точных шрифтовых метрик необходимо создать логический шрифт, высота которого равна размеру em-квадрата (с обратным знаком). В базовых шрифтах Windows размер em-квадрата равен 2048; также часто встречается значение 4096. В соответствии со спецификацией шрифтов TrueType максимальный размер em-квадрата равен 16 384. Следующая функция создает эталонный шрифт для существующего логического шрифта. При создании эталонного шрифта указывается высота, равная размеру em-квадрата физического шрифта, что позволяет получить наиболее точные значения шрифтовых метрик. HFONT CreateReferenceFont(HFONT hFont, int & emsquare) { L0GF0NT If; OUTLINETEXTMETRIC otm[3]; // С учетом строк HDC hDC = GetDC(NULL); HGDIOBJ hOld = SelectObject(hDC, hFont): int size = GetOutlineTextMetrics(hDC, sizeof(otm), otm); SelectObject(hDC, hOld); ReleaseDC(NULL, hDC); if ( size ) // TrueType { GetObject(hFont, sizeof(lf), & If); emsquare = otm[0].otmEMSquare; // Получить размер ЕМ-квадрата If.IfHeight = - emsquare; // Соответствие 1:1 If.lfWidth =0; // Исходные пропорции return CreateFontIndirect(&lf); } else return NULL; }
Простой вывод текста 833 Помимо функций, упоминавшихся в этом разделе, существуют и другие функции получения метрических данных шрифтов. Они будут рассмотрены ниже, при обсуждении вывода и форматирования текста средствами GDI. Простой вывод текста Контекст устройства обладает рядом атрибутов, используемых всеми функциями вывода текста. К числу этих атрибутов относится цвет текста и цвет фона, режим заполнения фона, тип выравнивания текста и т. д. COLORREF SetTextColor(HDC hDC, COLORREF crColor); COLORREF GetTextColor(HDC hDC); COLORREF SetBkColor(HDC hDC, COLORREF crColor); COLORREF GetBkColorCHDC hDC); int SetBkModeCHDC hDC. int iBkMode); int GetBkMode(HDC hDC); Функция SetTextCol or задает цветовую ссылку (COLORREF), которая используется для вывода основных пикселов текстовой строки. Основными считаются пикселы, которые образуют внутренние области глифов в строке. Функция SetTextCol or возвращает предыдущий цвет текста для заданного контекста устройства. На всякий случай напоминаем, что цветовые ссылки задаются тремя способами: RGBUpacHbM, зеленый, синий), PALETTEINDEX(iiHfleKC) и PALETTERGBCкрасный, зеленый, синий). Функция GetTextColor возвращает текущий цвет текста. В прямоугольнике, определяемом начальной точкой вывода и шириной/высотой строки, пикселы, не относящиеся к числу основных, называются фоновыми пикселами. Функция SetBkColor задает цветовую ссылку, используемую при выводе фоновых пикселов, и возвращает предыдущий цвет фона. Функция GetBkCol or возвращает текущий цвет фона. Иногда применение фонового цвета при выводе текста оказывается нежелательным. Например, если вы накладываете текстовую строку на фотографию и цвет текста достаточно сильно контрастирует с фоновым изображением, возможно, вы предпочтете не выводить фоновые пикселы. Функция SetBkMode задает для контекста устройства специальный атрибут — режим заполнения фона; этот атрибут управляет прорисовкой фоновых пикселов. В GDI определены два режима заполнения фона: в режиме OPAQUE фон заполняется фоновым цветом, а в режиме TRANSPARENT он остается без изменений. Функция GetBkMode возвращает текущий режим заполнения фона для контекста устройства. Выравнивание текста Наконец-то мы добрались до простейшей функции вывода текстовой строки в контексте устройства. Заодно будут рассмотрены две функции, управляющие выравниванием выводимого текста: BOOL TextOutCHDC hDC, int nXStart. int nYStart, LPCTSTR IpString, int cbString); UINT SetTextAlign(HDC hDC. UINT fMode); UINT GetTextAlignCHDC hDC);
834 Глава 15. Текст Функция TextOut выводит текстовую строку в заданной позиции, используя текущие значения управляющих атрибутов — шрифт, выбранный в контексте устройства, цвета текста и фона, режим заполнения фона и т. д. Выводимая строка задается указателем на первый символ и количеством символов. Таким образом, выводимый текст не обязательно завершать нуль-символом. Символы строки должны входить в набор символов текущего шрифта. Например, при использовании набора ANSI все управляющие символы выводятся глифом по умолчанию (глифом отсутствующего символа). Точная интерпретация параметров nXStart и nYStart определяется специальным атрибутом контекста устройства — типом выравнивания текста. Тип выравнивания задается в виде комбинации нескольких флагов (табл. 15.2). Таблица 15.2. Выравнивание текста Группа Флаг Описание Верхний край (надстрочная линия) текста совмещается с nYStart Базовая линия текста совмещается с nYStart Нижний край (подстрочная линия) текста совмещается с nYStart Левый край текста совмещается с nXStart Горизонтальный центр текста совмещается с nXStart Правый край текста совмещается с nXStart Использовать nXStart, nYStart; текущая позиция не обновляется при выводе текста Игнорировать nXStart, nYStart; использовать текущую позицию. Текущая позиция обновляется при каждом выводе текста Текст выводится справа налево. В Windows 2000 этот порядок возможен лишь при реализованной поддержке арабского языка или иврита. Ранее этот флаг поддерживался лишь в версиях ОС для арабского языка и иврита Флаги выравнивания текста делятся на четыре группы и управляют выравниванием по вертикали и горизонтали, обновлением текущей позиции и возможностью вывода текста справа налево в иврите/арабском языке. Флаги разных групп объединяются логической операцией OR. По умолчанию используется значение TA_T0P | TA_LEFT | TAJ0UPDATECP. При установке флага TAJJPDATECP GDI игнорирует начальную позицию, заданную координатами (nXStart, nYStart). Текст выводится с текущей позиции контекста устройства, причем каждая операция вывода обновляет горизонтальную координату текущей позиции. По вертикали ТА_Т0Р TABASELINE ТА_В0ТТ0М По горизонтали TA_LEFT TA_CENTER TA_RIGHT Обновление TA_N0UPDATECP ТА UPDATECP Справа налево TA_RTLREADING
Простой вывод текста 835 В следующем фрагменте демонстрируются некоторые интересные комбинации флагов выравнивания текста. SetTextColor(hDC, RGB(0, 0. 0)); // Черный SetBkColor(hDC. RGB(0xD0. OxDO, OxDO)); // Серый SetBkMode(hDC, OPAQUE); // Непрозрачный int x = 50; int у = 110; const TCHAR * mess = "Align"; for (int i=0: i<3; i++. x+=250) { const UINT Align[] = { TAJOP | TA_LEFT, TAJASELINE | TA_CENTER. TA_B0TT0M | TA_RIGHT }; SetTextAlign(hDC, Align[i] | TAJJPDATECP); MoveToEx(hDC, x, y, NULL); // Установка текущей позиции TextOut(hDC, x. у, mess. _tcslen(mess)); POINT cp; MoveToEx(hDC, 0, 0. & cp); // Получение текущей позиции Line(hDC, cp.x-5. cp.y+5, cp.x+5, cp.y-5); // Пометка текущей позиции Line(hDC, cp.x-5. cp.y-5. cp.x+5. cp.y+5); Line(hDC. x. y-75. x. y+75); // Вертикальный ориентир SIZE size; GetTextExtentPoint32(hDC. mess. _tcslen(mess). & size); Box(hDC. x. y, x + size.ex. у + size.су); } x - 250 * 3; Line(hDC. x-20. у. х+520. у); // Горизонтальный ориентир В этом фрагменте назначается черный цвет текста и светло-серый цвет фона при непрозрачном режиме заполнения фона; текст выводится в светло-сером прямоугольнике, обозначающем границы области вывода. Затем программа трижды выполняет тело цикла, демонстрируя разные комбинации горизонтального и вертикального выравнивания. Начальная точка каждого вызова отмечена перекрестием, а конечная точка обозначается наклонным крестиком. Пример приведен на рис. 15.7. При установке флагов ТАТОР | TALEFT с начальной точкой совмещается левый верхний угол текстовой области. Если установлен флаг TAJJPDATECP, начальная точка находится в текущей позиции контекста и обновляется координатами правого верхнего угла текстовой области. При установке флагов TABASELINE | TA_CENTER с начальной точкой совмещается точка пересечения базовой линии и горизонтального центра текстовой области. Если установлен флаг TAJJPDATECP, начальная точка находится в текущей позиции контекста, которая при выводе не обновляется.
836 Глава 15. Текст ТА_ТОР | TAJ.EFT TA_BASELINE | TA_CENTER TA_BOTTOM | TA_RIGHT № Allffn Alien Рис. 15.7. Выравнивание текста При установке флагов ТАВОТТОМ | TARIGHT с начальной точкой совмещается правый нижний угол текстовой области. Если установлен флаг TAJJPDATECP, начальная точка находится в текущей позиции контекста и обновляется координатами левого нижнего угла текстовой области. Вывод текста справа налево Мы привыкли, что текст выводится слева направо, а страница заполняется горизонтальными строками сверху вниз. Однако это лишь одна из многих систем письма, существующих в мире. В арабском языке и иврите большая часть текста пишется справа налево, хотя страница также заполняется горизонтальными строками сверху вниз. В традиционной японской и китайской письменности иероглифы пишутся сверху вниз, а вертикальные столбцы текста заполняют страницу справа налево. И в наши дни традиционная письменность используется во многих печатных изданиях — например, в газетах, журналах, художественной литературе. А в монгольском языке символы также записываются в вертикальные столбцы, но страница заполняется слева направо. Направление письма, стандартное для латиницы и кириллицы, хорошо поддерживается в GDI на уровне базовых средств вывода текста. Как будет показано ниже в этой главе, вертикальную письменность можно имитировать путем поворота и при помощи вертикальных шрифтов. С поддержкой вывода справа налево дело обстоит сложнее. Не существует специализированных версий Windows 2000 для других стран; для всего мира используется один и тот же двоичный код, а это означает, что в системе должна присутствовать встроенная поддержка других языков, включая языки с письменностью справа налево. При разработке Windows 2000 обеспечивалась поддержка чтения и создания документов на разных языках. Впрочем, поддержка других языков относится к числу дополнительных возможностей и реализуется отдельно с помощью панели управления (рис. 15.8). После установки системы поддержки других языков, включающей шрифты, файлы преобразования кодировок, методы ввода и т. д., Windows 2000 позволит читать и создавать документы на арабском и армянском языке, на языках стран Центральной Европы и кириллице, на грузинском, греческом, иврите, санскрите, китайском (в упрощенной и традиционной письменности), на тайском, ту-
Простой вывод текста 837 рецком, вьетнамском, на языках Западной Европы и американском диалекте английского. Шш& j Number | tuirem^ | Many редев* mw&l **&№ $«№&, and date& Set fa&hse j English (United States) t'^y** 1Ш Г $^рЪ№№&М*№Ш№ и|ШШШ«1 El 10001 (MAC - Japanese) El 10002 (MAC - Traditional Chinese Big5) El Ю003(MAC-Korean) El Ю004 (MAC-Arabic) El Ю005 (MAC-Hebrew) □ 10006 (MAC -Greek I) OK С&Ы Your дадо**здгё&**й & m& Ш wl* ttommto fo mtifa languages. EI Hebrew El Indic El Japanese El Korean El Simplified Chinese j $0tdefatdL. OK twee) Рис. 15.8. Установка системы поддержки других языков На уровне приложения при создании окна функцией CreateWindowEx флаг WS_EX_RTLREADING указывает, что текст должен выводиться справа налево. Флаг WSEXRIGHT присваивает окну общий набор атрибутов выравнивания по правому краю. В Windows 2000 появился новый флаг WS_EX_LAY0UTRTL, при котором базовая точка окна перемещается к правому краю, а горизонтальные координаты увеличиваются справа налево. На уровне GDI в Windows 98 и Windows 2000 появился новый атрибут контекста устройства — раскладка (layout), для работы с которым используются две функции: DWORD SetLayoutCHDC hDC, DWORD dwLayout); DWORD GetLayout(HDC hDC); Для атрибута раскладки определены всего два флага. Флаг LAY0UT_BITMAP- 0RIENTATI0NPRESERVED запрещает зеркальное отражение растров, выводимых функциями BitBlt, StretchBlt и т. д.; флаг LAY0UTRTL задает общее направление горизонтальной раскладки справа налево. В тернарные растровые операции был добавлен новый бит NOMIRRORBITMAP (0x80000000), запрещающий горизонтальное отражение растров. При выводе текста флаг TARTLREADING сообщает, что GDI следует расположить текст в соответствии с правилами чтения справа налево. Практическая реализа-
838 Глава 15. Текст ция вывода справа налево в Windows 2000 сосредоточена в новом компоненте GDI — UniScribe, API которого позволяет точно задать основные характеристики текста при выводе. Рассмотрим простой пример использования флагов LAY0UT_RTL и TA_RTLREADING. void Demo_RTL(HDC hDC, const RECT * rcPaint) { KLogFont lf(-PointSizetoLogical(hDC. 36). "Lucida Sans Unicode"); lf.mjf.IfCharSet - ARABIC_CHARSET; lf.mjf .IfQuality - ANTIALIASED_QUALITY; KGDIObject font (hDC. If .CreateFontO); TEXTMETRIC tm: GetTextMetrics(hDC. & tm); int linespace = tm.tmHeight + tm.tmExternalLeading; const TCHAR * mess - "1-360-212-0000 \xD0\xDl\xD2": for (int i=0; i<4; i++) { if ( i & 1 ) SetTextAlign(hDC. TAJOP | TA_LEFT | TA_RTLREADING); else SetTextAlign(hDC. TAJOP | TA_LEFT); if ( i & 2 ) SetLayout(hDC. LAYOUT_RTL); else SetLayout(hDC. 0); Text0ut(hDC. 10. 10 + linespace * i. mess. Jxslen(mess)); } } Функция Demo_RTL создает логический шрифт для арабского набора символов, используя гарнитуру Lucida Sans Unicode. На экран выводятся четыре строки с одним и тем же текстом, но с разными комбинациями флагов. Результаты показаны на рис. 15.9. 1-360-212-0000 j> j> 0000-212-360-1 jjb 0000-212-360-1 1-360-212-0000 j> Рис. 15.9. RTLJ.AYOUT и TAJOLREADING Первая строка выводится слева направо и выравнивается по левому краю — ничего необычного. Вторая строка выводится справа налево со стандартной ле-
Простой вывод текста 839 восторонней раскладкой (флаг TARTLREADING). Обратите внимание: GDI при помощи UniScribe разбивает текстовую строку на слова и располагает их в порядке, соответствующем чтению справа налево. Строка выводится в той же позиции, но с обратным порядком следования символов. Третья и четвертая строки выглядят похоже, но они выводятся после установки флага RTL_LAYOUT в раскладке контекста устройства. Без флага TA_RTLREADING текст выравнивается по правому краю клиентской области и выводится справа налево. При выравнивании с флагом TARTLRADING текст возвращается к стандартному порядку вывода. Таким образом, флаг TARTLREADING меняет направление чтения на противоположное. Дополнительные интервалы Функция TextOut обеспечивает выравнивание текста по левому/правому краю и по центру, но не поддерживает выравнивание по ширине (выключку). Выравнивание по ширине обычно означает вывод текста в горизонтальной области таким образом, что левый край текста выравнивается по левой стороне области, а правый край — по правой стороне. В процессе выравнивания по ширине пробелы в строке могут увеличиваться, обеспечивая совмещение правого края с правой стороной области. В GDI выравнивание по ширине состоит из нескольких шагов. Сначала GDI вычисляет точную ширину текстовой строки и сравнивает ее с шириной области вывода, чтобы узнать, сколько места необходимо компенсировать. Затем под- считывается количество пробелов в строке, передаваемое при вызове функции SetTextJustification GDI. На последнем шаге функция TextOut выводит строку, выровненную по ширине. У контекста устройства имеется специальный атрибут, управляющий расстоянием между символами, — дополнительный межсимвольный интервал (character extra). Дополнительный межсимвольный интервал определяет целочисленную величину в логической системе координат — размер дополнительного промежутка, добавляемого после каждого символа при выводе текста. Ниже перечислены функции GDI, предназначенные для вычисления размеров текста, настройки межсимвольных интервалов и выравнивания. int SetTextCharacterExtra(HDC hDC, int nCharExtra); int GetTextCharacterExtra(HDC hDC); BOOL GetTextExtentPoint32(HDC hDC. LPCTSTR IpString. int cbString, LPSIZE IpSize); BOOL SetTextJustificationCHDC hDC, int nBreakExtra, int nBreakCount); По умолчанию дополнительный межсимвольный интервал в контексте устройства равен 0. Функция SetTextCharacterExtra присваивает ему новое целочисленное значение в логических координатах, возвращая предыдущее значение. Функция GetTextCharacterExtra просто возвращает текущее значение межсимвольного интервала. Межсимвольные интервалы могут использоваться как для разрядки, так и для уплотнения текста. Функция GetTextExtentPoint32 возвращает размеры символьной строки в логической системе координат. Высота равна высоте текущего шрифта, а ширина
840 Глава 15. Текст определяется шириной отдельных символов, межсимвольными интервалами и параметрами выравнивания по ширине. Функция SetTextJustification задает величину дополнительного интервала, распределяемого между N ограничителями слов. Обычно ограничителем слова является пробел, но любой шрифт может переопределить этот символ. Ограничитель слова хранится в поле tmBreakChar структуры TEXTMETRIC. Ниже приведен небольшой пример использования функций SetTextCharac- terExtra и GetTextExtentPoint32. const TCHAR * mess = "Extra"; SetTextAlign(hDC, TA_LEFT | TAJOP); for (int i=0: i<3; i++) { SetTextCharacterExtraChDC, 1*10-10); TextOut(hDC, x. y. mess. _tcslen(mess)); SIZE size; GetTextExtentPoint32(hDC. mess. _tcslen(mess). & size); Box(hDC. x. y. x + size.ex. у + size.су): у += size.су + 10; } SetTextCharacterExtra(hDC. 0); // Сбросить межсимвольный интервал В этом фрагменте одна и та же строка выводится три раза с межсимвольными интервалами -10, 0 и 10. Функция GetTextExtentPoint32 возвращает размеры текстовой области, которая слева на рис. 15.10 обведена рамкой. Из рисунка видно, что GDI поддерживает как положительные, так и отрицательные межсимвольные интервалы. Межсимвольный интервал применяется после вывода символа. Если межсимвольный интервал отрицателен, между значением ширины текста, возвращаемым функцией GetTextExtentPoint32, и фактическим размером ограничивающего текст прямоугольника возникает расхождение в размере одного отрицательного интервала. The The quick The quick brown The quick brown fox The quick brown fox jumps The quick brown fox jumps over The quick brown fox jumps over the Thequickbrownfoxjumpsoverthelazy Tbequickbrownfoxjumpsoverthelazydog, The quick brown fox jumps over the lazy dog. Рис. 15.10. Дополнительные межсимвольные интервалы и расширение промежутков между словами Ниже все перечисленные этапы выравнивания текста по ширине объединены в одну удобную функцию TextOutJust. Функция вычисляет размеры текста, под-
Простой вывод текста 841 считывает количество символов-ограничителей, настраивает выравнивание текста по ширине в контексте устройства, после чего выводит выровненную строку. BOOL TextOutJust(HDC hDC, int left, int right, int y. LPCTSTR IpStr. int nCount. bool bAllowNegative, TCHAR cBreakChar) { SIZE size; SetTextJustification(hDC. 0, 0); GetTextExtentPoint32(hDC. IpStr. nCount. & size): int nBreak = 0; for (int i=0; i<nCount; i++) if ( lpStr[i]==cBreakChar ) nBreak ++; int breakextra = right - left - size.ex; if ( (breakextraO) && ! bAllowNegative ) breakextra =0; SetTextJustification(hDC, breakextra. nBreak); return TextOut(hDC. left. y. IpStr. nCount); } Правая часть рис. 15.10 иллюстрирует пример использования функции Text- OutJust. По ширине выравниваются части предложения — сначала одно слово, потом два, три слова и т. д. до полного текста. Как видно из рисунка, при выводе одного слова символы-ограничители в строке отсутствуют. При выводе двух слов расширяется только промежуток между словами, чтобы второе слово выровнялось по правой границе блока. С увеличением количества слов промежутки увеличиваются в равной степени. При некотором количестве слов промежутки между словами начинают сокращаться. Когда величина этих промежутков уменьшается до 0, текст начинает «перетекать» через правую границу, и тогда строку приходится разбивать на абзацы. Для сравнения последняя строка выведена со стандартными промежутками между словами и символами. Ширина символа При использовании крупного шрифта можно заметить, что «левый край» текстовой области, совмещаемый с параметром nXStart функции TextOut при установке флага TALEFT, в действительности несколько отходит от левого края глифа. Точное расположение символов по горизонтали определяется их шириной и метриками ABC шрифтов TrueType/Open Type. Ниже перечислены функции GDI, возвращающие информацию о ширине символов и метриках ABC шрифта, выбранного в контексте устройства. typedef struct _ABC { int abcA; UINT abcB; int abcC; } ABC; typedef struct _ABCFL0AT {
842 Глава 15. Текст FLOAT abcfA; FLOAT abcfB; FLOAT abcfC; } ABC; BOOL GetCharABCWidths(HDC hDC. UINT uFirstChat. UINT uLastChar. LPABC); BOOL GetCharABCWidthsFloatCHDC hDC. UINT uFirstChat. UINT uLastChar, LPABCFLOAT lpABCF); BOOL GetCharWidth32(HDC hDC. UINT iFirstChar. UINT ILastChar. LPINT lpBuffer): BOOL GetCharWidthFloatCHDC hDC. UINT iFirstChar. UINT iLastChar. PFLOAT pxBuffer); Функция GetCharABCWidths заполняет массив структурами ABC для всех символов в заданном интервале. Структура ABC определяет ширину символа в виде трех составляющих. Метрика А (левый отступ) определяет смещение начала глифа от текущей позиции курсора. Метрика В (ширина глифа) определяет ширину самого глифа. Метрика С (правый отступ) определяет смещение от конца глифа до следующей позиции курсора. Метрика В является целым без знака, поэтому ее значение должно быть положительным. Метрики А и С относятся к знаковому целому типу и могут принимать отрицательные значения. Функция GetChar- ABCWidthsFloat аналогична GetCharABCWidths за одним исключением: в возвращаемой ею структуре ABCFL0AT вместо целых чисел используются вещественные числа одинарной точности. Значения, возвращаемые GetCharABCWidths и GetCharWidthsFloat, задаются в логической системе координат. Как ни печально, структура ABCFL0AT не обеспечивает повышения точности по сравнению со структурой ABC, кроме случаев, когда отображение из системы координат устройства в логическую систему координат осуществляется с дробными коэффициентами. Иначе говоря, обе функции берут значения ширины из внутренней таблицы, содержимое которой масштабируется по размерам текущего шрифта в системе координат устройства; для структуры ABC — по целым, а для структуры ABCFL0AT — по вещественным числам в логической системе координат. Например, в режиме отображения MMJTEXT обе структуры будут содержать одинаковые значения в разных форматах. Функция GetCharABCWidths работает только со шрифтами ТшеТуре/ОрепТуре, а функция GetCharABCWidthsFloat также поддерживает растровые и векторные шрифты. Для растровых и векторных шрифтов, глифы которых не имеют метрик А и С, функция GetCharABCWidthsFloat просто заполняет соответствующие поля нулями. Для любого шрифта, поддерживаемого системой Windows, всегда можно вызвать функцию GetCharWidth32 и заполнить массив значениями полной ширины символов из заданного интервала. Для шрифтов ТшеТуре/ОрепТуре полная ширина равна сумме метрик А, В и С, а для растровых и векторных шрифтов она равна ширине символа. Функция GetCharWi dthFl oat представляет собой вещественную версию GetCharWi dth32. Похоже, реализация GetCharABCWidthsFloat в Windows 2000 содержит ошибку — возвращаемые значения составляют 1/16 от фактических. Впрочем, приложения все равно не используют эту функцию.
Простой вывод текста 843 Роль метрик ABC при выводе текста, а также их связь с размерами текстовой области, возвращаемыми функцией GetTextExtentPoint32, иллюстрирует рис. 15.11. Т(0х66):-34 129-32 V(0x6f): б 91 б V(0x6e):7 101 5 'Г(0x74): 9 63 -5 ABC widths sum: 346 GetTextExtent346 218 GetTextABCExtent-34 385 -5 Рис 15.11. Метрики ABC при выводе текста На рисунке слово «font» выведено курсивным шрифтом с кеглем 144 пункта. Курсивное начертание выбрано из-за того, что в нем глифы обладают более заметными отрицательными метриками А и С, особенно для букв «f», «j», «t» и т. д. Длинная полоса в верхней части рисунка обозначает начальную точку вывода текста и его горизонтальный размер, возвращаемый функцией GetTextExtent- Point32. Справа приведены метрики ABC для каждого символа, их сумма и размеры текста. Длинные вертикальные линии обозначают начальную и конечную точку каждого символа. Расстояние между двумя соседними линиями равно полной ширине символа (сумме метрик ABC). Как видите, ширина символьной строки равна сумме полных значений ширины всех символов, возможно — с добавлением дополнительных межсимвольных интервалов и промежутков между словами. Отрезки, расположенные над символами, обозначают метрики А каждого символа; затушеванный прямоугольник соответствует отрицательному значению. Отрезки, расположенные под символами, обозначают метрики С. Для первой буквы «f», обладающей значительной отрицательной метрикой А, левый край глифа смещается на 34 пиксела влево. Хотя метрика В символа «f» достаточно велика, отрицательная метрика С заметно приближает начальную точку буквы «о». Буквы «о» и «п» обладают положительными метриками А и С. У буквы «t» метрика А положительна, а метрика С отрицательна. К счастью, при выводе строки средствами GDI значения метрик ABC учитываются автоматически и обеспечивают правильное позиционирование символов в строке, не требуя дополнительных усилий со стороны приложения. Впрочем, не обошлось и без проблем: если используемый шрифт имеет отрицательные метрики А или С, текстовая строка не начинается с точки, заданной приложением, и не завершается в позиции, возвращаемой функцией GetTextExtentPoint32.
844 Глава 15. Текст- Метрика А первого символа и метрика С последнего символа строки могут выходить за пределы области, в которой выводиться текстовая строка. Погрешности при обработке отрицательных метрик А и С вызывают ряд проблем. Прежде всего, это может привести к случайному отсечению частей глифов. Запустите WordPad, выберите курсивный шрифт Times New Roman с кеглем 72 пункта и введите букву «f» — ее часть, соответствующая отрицательной метрике А, отсекается. Ограничивающий прямоугольник строки вычисляется неверно. Подобное отсечение часто встречается и в профессиональных приложениях. Ширину строки, как и ширину одного символа шрифта TrueType, можно разделить на три метрики А, В и С. Метрика А строки соответствует метрике А первого символа, метрика С соответствует метрике С последнего символа, а метрика В равна сумме всех остальных метрик строки. Следующая функция вычисляет метрики ABC для целой строки. // ( АО, ВО, СО ) + ( А1. В1, С1 ) « ( АО. В0+С0+А1+В1. С1 } BOOL GetTextABCExtent(HDC hDC. LPCTSTR IpString, int cbString. long * pHeight, ABC * pABC) { SIZE size; if ( ! GetTextExtentPoint32(hDC, IpString, cbString. & size) ) return FALSE; * pHeight = size.cy; pABC->abcB = size.ex; ABC abe; GetCharABCWidths(hDC, lpString[0]. lpString[0]. & abc); // Первый символ pABC->abcB -= abc.abcA; pABC->abcA = abc.abcA; GetCharABCWidths(hDC. lpString[cbString-l]. lpString[cbString-l], & abc); // Последний символ pABC->abcB -= abc.abcC; pABC->abcC - abc.abcC; return TRUE; } Для строки, показанной на рис. 15.11, GetTextABCExtent возвращает структуру ABC {-34,385,-5}; это означает, что фактическая длина строки равна 385 единицам, причем строка смещена на -34 единицы от начальной точки. Когда функция TextOut выравнивает строку в соответствии с атрибутами выравнивания текста, метрики А и С не учитываются. Если метрика А первого символа отрицательна, часть глифа может исчезнуть. Если метрика А положительна, текст неточно выравнивается по правому краю (особенно при выравнивании текста, оформленного разными шрифтами или одним шрифтом с разным кеглем). В следующем фрагменте показано, как точно выровнять текст на уровне пикселов при помощи функции GetTextABCExtent.
Простой вывод текста 845 BOOL PreciseTextOut(HDC hDC. ( long height ABC abe; int x, int y. LPCTSTR IpString, int cbString) if ( GetTextABCExtent(hDC, IpString, cbString. & height, & abc) ) switch ( GetTextAlign(hDC) & (TA_LEFT | TA_RIGHT | TA_CENTER) ) 1 case case case ТА LEFT : ТА RIGHT : ТА CENTER: X X X -= abc.abcA; break; +» abc.abcC; break; -= (abc.abcA-abc.abcC)/2; break; return TextOut(hDC. x. y. IpString, cbString); } Функция PreciseTextOut вычисляет метрики ABC для всей строки и изменяет координату х вызова TextOut в зависимости от текущего флага горизонтального выравнивания текста. Рис. 15.12 наглядно показывает различия между стандартной реализацией выравнивания в GDI и корректировкой, вносимой функцией PreciseTextOut. В первом столбце приведены результаты обычного вызова TextOut для выравнивания строки по левому краю, по центру и по правому краю. Части глифов, выходящие за пределы двух пограничных линий, обычно отсекаются — как, например, в ячейках таблиц Word или Excel. Во втором столбце те же строки выводятся с небольшой поправкой, вычисленной функцией PreciseTextOut; выравнивание обеспечивает точность на уровне пикселов. fluff fluff fluffi [fluff fluff JLUJJ\ Рис. 15.12. Выравнивание текста: TextOut и PreciseTextOut Во внутренних операциях графического механизма используется система координат устройства, из которой и берутся все масштабированные текстовые
846 Глава 15. Текст метрики. Если отображение из системы координат устройства в логическую систему координат не сводится к масштабированию с целым коэффициентом, получение метрик функцией GetTextABCWidths может привести к возникновению погрешности и последующему накоплению ошибок при вычислениях. Функция GetTextABCWidthsFloat помогает избавиться от погрешности, возникающей при преобразовании координат. Если ваше приложение выводит текст как на экран, так и на принтер, необходимо специально позаботиться о том, чтобы экранные данные совпадали с печатными. Например, для получения точных текстовых метрик можно воспользоваться эталонным логическим шрифтом (см. раздел «Получение информации о логическом шрифте»). Нетривиальный вывод текста Функция TextOut обладает простейшими возможностями и предоставляет удобный интерфейс к средствам вывода текста GDI. При использовании этой функции приложение задает значения нескольких атрибутов контекста устройства и передает строку. Функция ограничивается простейшим выводом и полностью маскирует от приложения все сложные операции, выполняемые GDI при выводе текста. За удобство и простоту приходится расплачиваться тем, что приложение не может управлять преобразованием символов в глифы, упорядочением глифов, лигатурами и кернингом, позициями отдельных глифов и т. д. Для решения нетривиальных задач вывода текста в GDI предусмотрены специальные функции, используемые в современных текстовых редакторах и других графических пакетах. Преобразование символов в глифы В шрифте TrueType центральное место занимает набор описаний глифов, масштабируемых для любого разрешения и преобразуемых в глифы. Символы разных кодировок обычно сначала преобразуются в Unicode, а затем — в индексы глифов по таблице «стар», хранящейся в шрифте TrueType. Некоторые возможности обработки текстов, не поддерживаемые GDI напрямую, легко реализуются на уровне глифов. В GDI Windows 2000 появилась новая функция GetGlyphIndices, позволяющая приложению получить массив индексов глифов для символов строки. В предыдущих версиях Windows это преобразование выполнялось при помощи функции GetCharacterPlacement, описанной ниже. В GDI Windows 2000 также поддерживаются функции для получения информации о метриках глифов: DWORD GetGlyphlndicesCHDC hDC, LPCTSTR lpstr. int c. LPW0RD pgi. DWORD f1); BOOL GetCharWidth(HDC hDC. UINT giFirst, UINT cgi. LPW0RD pgi, LPINT lpBuffer); BOOL GetCharABCWidthsKHDC hDC. UINT giFirst. UINT cgi. LPW0RD pgi. LPABC Ipabc); BOOL GetTextExtentPointKHDC hDC. LPW0RD pgiln. int cpi. LPSIZE IpSize);
Нетривиальный вывод текста 847 Индексы глифов хранятся в виде 16-разрядных целых чисел (WORD). Символы Unicode тоже хранятся в виде 16-разрядных целых чисел, поэтому индексы глифов позволяют представить весь интервал Unicode. Для получения информации об интервалах Unicode, поддерживаемых шрифтом, используется функция Get- FontUnicodeRanges. Функция GetGlyphlndices отображает текстовую строку на массив индексов глифов. Параметр pgi указывает на массив WORD, размеры которого достаточны для хранения всех индексов. Последний параметр fl сообщает GDI, как следует поступать с отсутствующими символами. Если он равен CGI_MASK_NONEXISTING_ GLYPHS, отсутствующие символы заменяются маркером OxFFFF. По умолчанию отсутствующие глифы в шрифтах TrueType представляются первым глифом шрифта. Функции получения текстовых метрик GetCharWidthI, GetCharABCWidthsI и Get- TextExtentPointI очень похожи на аналогичные функции без суффикса I с одним исключением — при вызове им передаются интервалы или массивы индексов глифов. Функции API уровня глифов позволяют выполнять специфические операции, не поддерживаемые непосредственно в GDI. Если приложение хочет реализовать лигатуры или контекстную замену глифов, оно может получить соответствующую таблицу шрифта TrueType функцией GetFontData и произвести поиск в таблице. Внутреннее строение шрифтов TrueType подробно рассматривается в главе 14. Кернинг При простом выводе текста функцией TextOut символы позиционируются в строке только по межсимвольным расстояниям, хранящимся в шрифте TrueType, а именно по метрикам ABC глифов. Дополнительные межсимвольные интервалы просто применяются к каждому выводимому символу без учета их специфики. Шрифты TrueType обычно содержат данные кернинга, обеспечивающие контекстную регулировку расстояний. Данные кернинга кодируются в таблице кернинга, каждый элемент которой (пара кернинга) определяет параметры регулировки расстояния для двух конкретных символов, расположенных по соседству. Для получения пар кернинга приложение использует функцию GetKerning- Pairs: typedef struct tagKERNINGPAIR { WORD wFirst; WORD wSecond; int iKernAmount; } KERNINGPAIR; DWORD GetKerningPairs(HDC hDC, DWORD nNumPairs. LPKERNINGPAIR Ipkrnpair); Каждая пара содержит коды двух символов, wFirst и wSecond, и величину кернинга. Фактически она означает следующее: если за символом wFirst следует символ wSecond, относящийся к тому же шрифту, расстояние между этими двумя символами корректируется на величину i KernAmount. Обычно в парах кернинга задается отрицательное расстояние, чтобы символы приближались друг к другу,
848 Глава 15. Текст однако расстояние может быть и положительным. Величина кернинга, возвращаемая функцией GetKerningPairs, задается в логической системе координат. Шрифт TrueType может содержать сотни пар кернинга. Чтобы получить информацию обо всех парах, следует сначала вызвать функцию GetKerningPairs для получения общего числа пар. После выделения блока памяти достаточного размера функция вызывается повторно для получения данных кернинга. Ниже приведено объявление простого класса для работы с парами кернинга (полная реализация находится на прилагаемом компакт-диске). class KKerningPair { public: KERNINGPAIR * mj)KerningPairs; int mjiPairs; KKerningPair(HDC hDC); -KKerningPair(void); int GetKerning(TCHAR first. TCHAR second) }: Класс KKerningPair содержит две переменные. Переменная m_pKerningPairs указывает на динамически выделенный массив структур KERNINGPAIR, а в переменной mjiPairs хранится количество пар кернинга для текущего шрифта. Конструктор класса запрашивает количество пар кернинга, а деструктор освобождает выделенные ресурсы. Дополнительный метод GetKerning возвращает величину кернинга по кодам двух символов. Примеры кернинга показаны на рис. 15.13. В верхнем ряду текст выводится без кернинга. На левом верхнем рисунке изображены четыре пары символов, чрезмерно удаленных друг от друга, а на правом верхнем — три пары символов, расположенных слишком близко. В результате кернинга символы на левом рисунке сближаются (отрицательная корректировка), а на правом рисунке они удаляются (положительная корректировка). ,)Та г. MTf} F,) Та г. n f ] f} Рис. 15.13. Примеры пар кернинга Расположение символов Непосредственно работать с индексами глифов и данными кернинга в приложении достаточно трудно. В GDI существует удобная функция GetCharacterPl acement,
Нетривиальный вывод текста 849 возвращающая информацию о расположении символов в строке. Приложение может проанализировать полученные данные, изменить их или воспользоваться ими для других целей. Соответствующие определения выглядят так: typedef struct tabGCP_RESULTS { DWORD IStructSize; LPTSTR IpOutString; UINT * lpOrder; INT * IpDx; INT * IpCaretPos; LPTSTR IpClass; LPWSTR * lpGlyphs; UINT nGlyphs; UINT mMaxFit; } GCPJESULTS; DWORD GetCharacterPlacement(HDC hDC, LPCTSTR IpString, int nCount, int nMaxExtent, LPGCP_RESULTS lpResults, DWORD dwFlags); Функция GetCharacterPlacement получает манипулятор контекста устройства, указатель на текстовую строку и ее длину, необязательную ширину текстовой области и набор флагов. На выходе она заполняет структуру GCP_RESULT сведениями о расположении символов в строке: порядке следования символов, расстояниях между ними, позиции каретки, классификации символов, индексах глифов и количестве символов, помещающихся в заданной области. Сама структура GCP_RESULT не содержит всей информации, поскольку эти данные представляются массивами переменного размера; в структуре хранятся указатели на массивы. Приложение должно соответствующим образом подготовить GCP_RESULT перед вызовом GetCharacterPlacement. Если приложение запрашивает некоторую информацию, оно устанавливает требуемый флаг в последнем параметре, а соответствующее поле GCP_RESULTS должно содержать действительный указатель; в противном случае значение поля может быть равно NULL. Функция GetCharacterPlacement обладает очень широкими возможностями, проектировавшимися в расчете на поддержку разных аспектов обработки текста — выравнивания по ширине, кернинга, диакритических знаков, упорядочения глифов, лигатур и т. д. Конкретные возможности зависят от шрифта и текущей конфигурации системы. В частности, не все шрифты TrueType содержат таблицы кернинга и поддерживают лигатуры. Возможности конкретного шрифта можно проверить при помощи функции GetFontLanguagelnfo. Полное описание возможностей GetCharacterPlacement займет слишком много места. За подробностями обращайтесь к MSDN. Здесь же будут рассмотрены некоторые простые случаи — использование флагов GCPJJSERKING, GCPJ1AXENTENT и GCP_JUSTIFY. В структуре GCP RESULTS поле lpOutputString содержит указатель на выходную строку, которая будет выведена для заданной входной строки. Как правило, выходная строка совпадает с входной, однако строки могут отличаться — например, при установке флагов GCPREORDER (изменение порядка следования символов) и GCPMAXEXTENT (превышение предельной длины входной строки). Поле 1 pOrder содержит указатель на массив, заполняемый данными отображения входной строки на выходную. В иврите и в арабском языке текст выводится справа
850 Глава 15. Текст налево, что может привести к изменению порядка следования символов входной строки. Поле IpDx содержит указатель на массив, заполняемый сведениями о ширине каждого символа в строке (то есть разности расстояний между позициями текущего и следующего символа в строке). Это расстояние определяется полной шириной символа со всеми поправками — дополнительными межсимвольными интервалами, увеличенными промежутками между словами и кернингом. Порядок следования элементов в массиве IpDx соответствует порядку следования символов в выходной строке. Поле lpCaretPos указывает на массив, заполняемый позициями каретки для всех символов в порядке их следования во входной строке. Данные могут использоваться текстовым редактором для перемещения каретки в процессе работы. Информация об угле наклона символов хранится в структуре OUTLINETEXTMETRIC. Поле IpClass содержит массив классификационных признаков для всех символов строки. Например, флаг GCPCLASS_ARABIC обозначает арабский символ. Поле IpGlyphs содержит указатель на массив, заполняемый индексами глифов всех символов строки. При помощи этого массива можно получить индексы глифов без помощи функции GetGlyphlndices, поддерживаемой только в Windows 2000. Если одна текстовая строка используется при нескольких вызовах функций, следует получить индексы один раз и задействовать их повторно — это сэкономит время на многократные преобразования. Поле nGlyphs изначально содержит максимальное количество элементов в разных массивах структуры GCP_RESULTS. При выходе из GetCharacterPlacement в него записывается количество фактически используемых элементов в массивах. Поле nMaxFit содержит количество символов, укладывающихся в области, размеры которой задаются параметром GetCharacterPlacement. Обратите внимание: в поле nMaxFit хранится количество символов, в отличие от поля nGlyph, содержащего количество глифов. На компакт-диске находится простой класс КР1 acement, предназначенный для работы с функцией GetCharacterPlacement. Функция ExtTextOut Получение индексов глифов, данных кернинга и сведений о расположении символов — не более чем подготовка к выводу текста. Чтобы применить полученную информацию при выводе текста, следует воспользоваться функцией ExtTextOut. BOOL ExtTextOut(HDC hDC. int x, int ym UINT fuOptions. CONST RECT * lprc, LPCTSTR IpString, UINT cbCount. CONST INT * IpDx); Функция ExtTextOut представляет собой расширенную версию TextOut. Вызов TextOut можно заменить эквивалентным вызовом ExtTextOut: ExtTextOut(hDC. x, у. О, NULL, IpString. cbCount. NULL); Остается лишь разобраться с тремя новыми параметрами. Параметр fuOptions содержит набор флагов, управляющий интерпретацией других параметров. Необязательный параметр 1 pre указывает на прямоугольник, который может определять границы непрозрачной области, использоваться для отсечения или совмещать эти функции. Необязательный параметр IpDx содержит указатель на массив расстояний. В табл. 15.3 перечислены допустимые значения параметра fuOptions.
Нетривиальный вывод текста 851 Таблица 15.3. Флаги функции ExtTextOut Значение Описание ET0_0PAQUE (0x0012) Перед выводом текста прямоугольник, заданный параметром 1ргс, закрашивается цветом фона ET0_CLIPPED (0x0004) Текст отсекается по прямоугольнику, заданному параметром 1ргс ET0_GLYPH_INDEX (0x0080) Параметр lpStMng указывает на массив индексов глифов (вместо кодов символов). Индексы глифов всегда являются 16-разрядными величинами ET0_RTLREADING (0x0080) То же, что и флаг TA__RTLREADING при выравнивании текста. В шрифтах арабского языка и иврита текст выводится справа налево ET0__NUMERICSLOCAL (0x0400) Числа выводятся символами национальных алфавитов ET0JIUMERICSLATIN (0x0800) Числа выводятся стандартными европейскими цифрами ET0_IGN0RELANGUAGE (0x1000) He выполнять дополнительную языковую обработку текста ET0__PDY (0x2000) Параметр lpDx указывает на массив пар, в которых первое число определяет горизонтальное расстояние, а второе — вертикальное Параметр 1 pre функции TextOut не влияет на обычную прорисовку фона; скорее он упрощает вывод текста в ограниченной области — например, в ячейке таблицы. Если параметр 1ргс отличен от NULL, он интерпретируется как указатель на прямоугольник в логической системе координат. При установленном флаге ET0_0PAQUE прямоугольник закрашивается текущим цветом фона вместе со стандартным фоном текста. При установленном флаге ET0_CLIPPED прямоугольник задает дополнительный уровень отсечения в логической системе координат (не в координатах устройства!). Чтобы вывести текстовую строку в ячейке таблицы, приложение может передать прямоугольник ячейки ExtTextOut и установить оба флага, ET0_0PAQUE и ET0_CLIPPED. При этом текущим цветом фона закрашивается весь прямоугольник ячейки, а не только текстовая область, и текст заведомо не нарушит границ ячейки. Параметр lpDx позволяет приложению точно определять позицию каждого глифа, не полагаясь на помощь GDI. При такой архитектуре приложение может дополнительно обработать структуру расположения символов, сгенерированную функцией GetCharacterPlacement. Вспомните: текстовые метрики, возвращаемые GDI приложению, задаются в логической системе координат, а вывод текста происходит в системе координат устройства. При печати документа, созданного на экране монитора, на принтере с высоким разрешением, обладающим существенно более точными данными метрик, раскладка текста не будет точно соответствовать экранной. Для решения этих двух проблем несоответствия (между логическими координатами/координатами устройства и экраном/принтером) приложение может получить точные данные метрик шрифта по эталонному логическому шрифту, размер которого совпадает с размером em-квадрата физиче-
852 Глава 15. Текст ского шрифта. Все позиционные вычисления производятся по точным данным em-квадрата и масштабируются в логической системе координат, сохраняются в массиве и передаются ExtTextOut. Функция ExtTextOut также может использоваться для вывода текстовой строки по индексам глифов, полученным при помощи функции GetGlyphlndices или GetCharacterPlacement. Следующий метод выводит текстовую строку по содержимому структуры GCP_RESULTS, возвращаемой функцией GetCharacterPlacement. BOOL KPlacement::GlyphTextOut(HDC hDC. int x, int y) { return ExtTextOut(hDC, x. y. ETO_GLYPH_INDEX, NULL. (LPCTSTR) m_glyphs, m_gcp.nGlyphs. m_dx): } Следующий фрагмент иллюстрирует пример использования индексов глифов и кернинга при работе с функциями GetCharacterPlacement и ExtTextOut. void Test_Kerning(HDC hDC. int x. int y, const TCHAR * mess) { TextOut(hDC. x. y, mess. Jxslen(mess)); KPlacement<MAX_PATH> placement; TEXTMETRIC tm: GetTextMetrics(hDC, & tm); int linespace - tm.tmHeight + tm.tmExternalLeading; KKerningPair kerning(hDC); SIZE size; GetTextExtentPoint32(hDC. mess. _tcslen(mess). & size); for (int test-0: test<3; test++) { у += linespace: int opt; switch ( test ) { case 0: opt s 0; break; case 1: opt - GCPJJSEKERNING; break; case 2: opt - GCPJJSEKERNING | GCP_JUSTIFY; break; } placement.GetPlacement(hDC. mess, opt | GCP_MAXEXTENT, size.cx*ll/10); placement.GlyphTextOut(hDC. x. y); } } В программе, находящейся на компакт-диске, расстояние между глифами отмечается линиями; также выводятся индексы глифов и интервалы кернинга. Пример приведен на рис. 15.14.
Нетривиальный вывод текста 853 AVOWAL ШошШ gi('A')* 36, gi('V')- 57, gi('0')= 50, gi('¥')= 58, gi('A')= 36, gi('L')= 47, extent 346, gi('A')= 36, gi('V')= 57, gi('0')= 50, gi('W')= 58, gi('A')= 36, gi('L')= 47, extent 346, gi('A')= 36, gi('V')= 57, gi('0')= 50, gi('W')= 58, gi('A')= 36, gi('L')= 47, extent 346, dx[ 0]= 49 dx[ 1]= 49 dx[ 2]= 58 dx[ 3]= 66 dx[ 4]= 49 dx[ 5]= 44 sum dx=315 dx[ 0]= 45, dx[ 1]= 47, dx[ 2]« 58, dx[ 3]= 60, dx[ 4]= 49, dx[ 5]= 44 sum dx=303 dx[ 0]= 54 dx[ 1]= 56 dx[ 2]= 67 dx[ 3]= 68 dx[ 4]= 57 dx[ 5]= 44 sum dxe346 kern( kern( kern( kern( kern( A' V 0' W A' 'V '0 'W 'A 'L ) = ) = ) = ) = ) = -4 -2 0 -6 0 Рис. 15.14. Пример ExtTextOut: индексы глифов и кернинг Первая строка выведена обычной функцией TextOut. Вторая строка выведена функцией ExtTextOut по индексам глифов и расстояниям, сгенерированным функцией GetCharacterPlacement, но без установки флага GCPUSEKERNING. Результат выглядит точно так же, как и в первом случае. Третья строка выведена функцией ExtTextOut для данных, возвращенных функцией GetCharacterPlacement при установленном флаге GCPUSEKERN I NG. Наконец, нижняя строка демонстрирует результат выравнивания по ширине с флагом GCPJUSTIFY. Справа выводятся индексы глифов, расстояния между символами и пары кернинга. Видно, что различия в положении символов второй и третьей строк обусловлены применением кернинга, в результате чего ширина текстовой области сократилась на 12 единиц. Различия между третьей и четвертой строками обусловлены выравниванием по ширине: 43 единицы дополнительного пространства почти равномерно распределяются между 5 парами символов (9,9,9,8,8). Если при выравнивании текста установлен флаг TARTLREADING и при вызове функции GetCharacterPlacement был передан флаг GCP_RE0RDER, GDI записывает глифы в соответствии с направлением чтения и правилами замены глифов, если они поддерживаются текущим шрифтом. Пример: KLogFont If(-PointSizetoLogical(hDC. 48). "Andalus"); lf.mJf.lfCharSet - ARABIC_CHARSET; lf.mJf.lfQuality - ANTIALIASED_QUALITY; KGDIObject font (hDC. lf.CreateFontO): assert ( GetFontLanguagelnfo(hDC) & GCP_RE0RDER ); KPlacement<MAX_PATH> placement; const TCHAR * mess = "abc \xC7\xC8\xC9\xCA": SetTextAlign(hDC. TA_LEFT);
854 Глава 15. Текст placement.GetPlacement(hDC. mess, 0); placement.GlyphTextOut(hDC, x, yO); SetTextAlign(hDC. TA_LEFT | TA_RTLREADING); placement.GetPlacement(hDC. mess, GCP_REORDER): placement.GlyphTextOut(hDC, x, yl); На рис. 15.15 показан очень интересный результат. order[0]=0, order[l]-l, order[2]=2, order[3]=3, order[ 4 ] = 4, order[5]=5, order[6]=6, order[7]s 7, order[0]- 5, order[1]■6, order[2] = 7, order[3]■4, order[4]■3, order[5]■2, order[6]■1, order[7]=0, gi(0x61)= gi(0x62)= gi(0x63)= gi(0x20)= gi(0xc7)= gi(0xc8)= gi(0xc9)= gi(0xca)= gi(0xca)a gi(0xc9)= gi(0xc8)= gi(0xc7)= gi(0x20)= gi(0x61)= gi(0x62)= gi(0x63)= = 68, - 69, ■• 70, • 3, = 346, '349, = 352, = 356, = 353, = 352, = 349, = 345, • 3, - 68, - 69, •■ 70, dx[0]= dx[l]= dx[2]= dx[3]= dx[4]= dx[5]- dx[6]= dx[7]= dx[0]= dx[l]= dx[2]= dx[3]= dx[4]= dx[5]- dx[6]= dx[7]= 30 33 29 16 13 18 23 18 30 33 29 16 19 18 23 48 Рис. 15.15. Изменение порядка следования и замена глифов Сравните две строки, верхнюю и нижнюю: слова поменялись местами, порядок следования символов изменился, а сами символы отображаются на разные глифы. В первой строке символы выводятся в стандартном порядке — слева направо, от первого до восьмого. Во второй строке изменился порядок следования как слов, так и арабских символов. Арабские символы отображаются на разные глифы в зависимости от их относительной позиции в слове. Скажем, буква «алеф» (0хС7) отображается на глиф 346 в первой строке и на глиф 345 во второй строке. Uniscribe В Windows 2000 появился компонент Uniscribe — новый интерфейс API, обеспечивающий высокую степень контроля над обработкой сложных текстов. Под «сложным текстом» понимается любой текст, использующий двунаправленный вывод, контекстную замену глифов, лигатуры, особые правила разбивки на слова и выравнивания по ширине или фильтрацию недопустимых комбинаций символов. К сложным текстам относятся тексты на иврите, арабском, тайском и санскрите. Интерфейс Uniscribe API отличается повышенной сложностью, в нем задей- ствуется свыше 30 новых функций и 10 новых структур. Uniscribe использует заголовочный файл usplO.h, библиотечный файл usplO.lib и библиотеку uspl0.dll. А если принять во внимание, что по своим размерам uspl0.dll превосходит даже gdi32.dll, вы поймете, почему в этой книге не найдется места для рассмотрения Uniscribe. Начиная с Windows 2000, стандартные функции GDI для вывода текста (такие, как TextOut и ExtTextOut) были расширены для поддержки сложных текстов. abc UU iil abc
Нетривиальный вывод текста 855 Впрочем, фактическое использование этих возможностей зависит от того, поддерживаются ли они на уровне шрифтов и текущих настроек системы. При пошаговом выполнении кода текстовых функций GDI вы увидите, что GDI загружает внешнюю библиотеку lpk.dll, где LPK означает «Language Pack», то есть «пакет языковой поддержки», lpk.dll экспортирует десятки функций с именами LpkDrawTextEx, LpkExtTextOut и LpkTabbedTextOut и импортирует ряд функций из uspl0.dll. Правосторонняя раскладка и порядок следования символов, замена глифов — в Windows 2000 все эти возможности базируются на использовании Uniscribe. Обработка сложных текстов всегда была слабым местом Windows GDI API, особенно по сравнению с QuickDraw GX компании Apple. До появления Uniscribe приложению, обеспечивающему нетривиальные возможности вывода текста, приходилось работать на уровне внутренних таблиц шрифтов TrueType. Применение Uniscribe значительно упрощает обработку сложных текстов в приложениях. Доступ к данным глифов Как говорилось выше, глифы в шрифтах TrueType представляются квадратичными кривыми Безье. При выводе текста контур глифа преобразуется в растровое изображение с заданными размерами и углом наклона. Полученные растры выводятся на поверхности устройства в соответствии с запросами приложений. Впрочем, возможности вывода текста в GDI остаются ограниченными. Например, GDI не разрешает приложению использовать для прорисовки текста кисть или растр; допускаются только однородные цвета. Для нетривиальных приложений, предусматривающих специальную обработку глифов перед выводом, в GDI существует низкоуровневая функция GetGlyphOutline. При помощи этой функции приложение запрашивает данные глифов в виде набора метрик, растра или описания контура. Ниже приведены соответствующие определения. typedef struct _GLYPHMETRICS { UINT gmBlackBoxX; UINT gmBlackBoxY; POINT gmptGlyphOrigin; short gmCelllncX: short gmCelllncY; } GLYPHMETRICS; typedef struct JIXED { WORD fract; short value; } FIXED; typedef struct _MAT2 { FIXED eMll; FIXED eM12; FIXED eM21: FIXED eM22; } MAT2; DWORD GetGlyphOutlineCHDC hDC, UINT uChar, UINT uFormat.
856 Глава 15. Текст LPGLYPHMETRICS lpgm, DWORD cbBuffer. LPVOID IpvBuffer. CONST MAT2 *lpmat2); Функция GetGlyphOutline за один вызов может вернуть для символа один из трех типов данных: метрики глифа, растр глифа или контур глифа. Первый параметр определяет контекст устройства с выбранным шрифтом TrueType/Open- Type. Параметр uChar определяет код символа в однобайтовом формате или в кодировке Unicode (в зависимости от того, используется ли Unicode-версия этой функции). Параметр uFormat в основном управляет форматом данных, запрашиваемых приложением; его основные значения перечислены в табл. 15.4. Следующий параметр, lpgm, указывает на структуру GLYPHMETRICS, заполняемую метриками глифа. Чтобы получить растр или контур глифа, приложение должно передать функции GetGlyphOutline указатель на буфер IpvBuffer; размер буфера задается параметром cbBuffer. Последний параметр, lpmat2, указывает на матрицу аффинного преобразования в формате с фиксированной точкой. Таблица 15.4. Формат результата GetGlyphOutline Значение Описание GGOMETRICS Заполнить только структуру GLYPHMETRICS. Вернуть 0 в случае успеха, GDIERR0R при неудаче GGOBITMAP Заполнить GLYPHMETRICS и растр в формате 1 бит/пиксел GGONATIVE Заполнить GLYPHMETRICS и исходное описание глифа из шрифта TrueType GGOBEZIER Заполнить GLYPHMETRICS и описание глифа в виде кубических кривых Безье GG0_GRAY2_BITMAP Заполнить GLYPHMETRICS и растр в формате 8 бит/пиксел с 5 уровнями серого GG0_GRAY4_BITMAP Заполнить GLYPHMETRICS и растр в формате 8 бит/пиксел с 17 уровнями серого GG0_GRAY8_BITMAP Заполнить GLYPHMETRICS и растр в формате 8 бит/пиксел с 65 уровнями серого GGOGLYPHINDEX Параметр uChar вместо кода символа содержит индекс глифа GGOUNHINTED Запретить обработку инструкций (хинтов) для контуров глифов (GGONATIVE или GGO_BEZIER). Поддерживается только в Windows 2000 Метрики глифа возвращаются в структуре GLYPHMETRICS, которая описывает отдельный глиф в логической системе координат текущего контекста устройства. Поля gmBlackBoxX и gmBlackBoxY определяют ширину и высоту ограничивающего прямоугольника глифа (а также ширину и высоту возвращаемого растра). Следует учитывать, что этот прямоугольник обычно меньше прямоугольника ячейки символа. Поле gmptGlyphOrigin содержит структуру POINT, которая задает координаты левого верхнего угла ограничивающего прямоугольника глифа относительно эта-
Нетривиальный вывод текста 857 лонной точки. Помните, что при описании глифа TrueType в координатах em- квадрата вертикальная ось направлена снизу вверх, противоположно направлению вертикальной оси в системе координат устройства или логической системе координат в режиме ММ_ТЕХТ. Поля gmCelllncX и gmCelllncY определяют смещение эталонной точки. Растр глифа Функция GetGlyphOutline может вернуть растр глифа в одном из четырех форматов. Формат GGO_BITMAP предназначен для простейших монохромных растров, в которых 0 соответствует фоновым пикселам, а 1 — основным пикселам. В трех других форматах растр возвращается в формате 8 бит/пиксел с разным количеством оттенков серого. Эти растры используются GDI для вывода сглаженных символов (вместо обычных резких переходов на границах). Чтобы вывести текст со сглаженными символами, достаточно задать качество ANTIALIASEDQUALITY при создании логического шрифта. Три растровых формата в оттенках серого, GG0_ GRAY2_BITMAP, GG0_GRAY4_BITMAP и GG0_GRAY8_BITMAP, используют 4, 17 и 65 уровней серого соответственно. Вероятно, формат GGOGRA Y8BITM АР логичнее было бы назвать GG0_GRAY6_BITMAP. Растры глифов либо имеют монохромный формат, либо формат 8 бит/пиксел. Каждая строка развертки всегда выравнивается по границе двойного слова, причем в буфере, возвращаемом GDI, строки развертки следуют в стандартном порядке (от верхней к нижней). Таким образом, формат растра глифа точно совпадает с форматом массива пикселов 1- или 8-разрядного DIB-растра с прямым порядком следования строк. Растр глифа имеет переменный размер, поэтому приложение обычно сначала вызывает функцию GetGlyphOutline для получения размера растра, выделяет блок памяти достаточного объема и вызывает функцию вторично для получения данных растра. Структура МАТ2 определяет ограниченное аффинное преобразование на плоскости. Матрица преобразования содержит числа с фиксированной точкой в формате 16.16. Полное аффинное преобразование описывается структурой XF0RM, содержащей шесть вещественных полей: еМП, еМ21, eDx, eM21, еМ22 и eDy, и позволяет выполнять смещение, масштабирование, зеркальное отражение, поворот, сдвиг и их произвольные комбинации. Структура МАТ2 удаляет eDx и eDy из структуры XF0RM и преобразует оставшиеся поля в формат с фиксированной точкой. Таким образом, структура МАТ2 позволяет описывать масштабирование, повороты, сдвиги и отражения, но не смещения. Учитывая, что смещение легко реализуется при выводе растра глифа, функция GetGlyphOutline фактически поддерживает полные аффинные преобразования. Обратите внимание: параметр МАТ2 является обязательным. Даже если преобразование является тождественным, вы должны передать правильно заполненную структуру МАТ2. Получив растр глифа, вы можете преобразовать его в аппаратно-зависимый или аппаратно-независимый растр и воспользоваться растровыми функциями GDI для его вывода. Вариант с DIB предпочтителен, поскольку он не требует создания новых объектов GDI и упрощает управление цветовой таблицей для сглаженных глифов. Функция GetGlyphOutline позволяет приложению самостоятельно имитировать вывод текста, поэтому перед приложением открывается
858 Глава 15. Текст множество интересных возможностей. Впрочем, получение растров глифов и их вывод — задача не из простых. В листинге 15.4 приведено объявление класса KGIyph для работы с растрами глифов, инкапсулирующего GetGlyphOutline и имитирующего вывод отдельного символа средствами GDI. Полная реализация класса KGIyph находится на компакт-диске. Листинг 15.4. Класс KGIyph: работа с растрами глифов class KGIyph { public: GLYPHMETRICS mjnetrics: BYTE * m_pPixels; DWORD m_nAl1ocSize, mjiDataSize; int mjjFormat; KGlyphO; -KGlyph(void); DWORD GetGlyphCHDC hDC. UINT uChar. UINT uFormat, const MAT2 * pMat2=NULL); BOOL DrawGlyphROP(HDC HDC. int x. int у. DWORD гор. COLORREF crBack. COLORREF crFore); BOOL DrawGlyph(HDC HDC. int x. int y. int & dx. int & dy); }: Класс KGIyph содержит четыре основные переменные: структуру GLYPHMETRICS, буфер растра глифа, размер буфера и флаг формата. Конструктор устроен достаточно просто; деструктор освобождает всю выделенную память. Метод GetGlyph является оболочкой для вызова функции GetGlyphOutline. По сравнению с GetGlyphOutline ему не передается структура с метриками глифа и буфер, поскольку эти данные находятся под управлением класса, а параметр МАТ2 не обязателен, поскольку тождественная матрица легко строится в приложении. KGIyph::GetGlyph создает матрицу по умолчанию, запрашивает размер данных, выделяет память и запрашивает данные глифа. Одна функция используется для получения метрик, растра и контура глифа. Метод DrawGlyphROP выводит растр глифа, возвращенный методом GetGlyphOutline, с заданными цветами (основным и фоновым) и растровой операцией. Основной и фоновый цвет имитируют атрибуты контекста устройства GDI. Растровая операция имитирует режим заполнения фона (прозрачный или непрозрачный) или любой другой экзотический режим вывода на ваше усмотрение. Функция DrawGlyphROP проверяет формат растра, определяя формат (бит/пиксел) и количество уровней серого цвета. На основании полученных данных строится цветовая таблица, содержащая только основной и фоновый цвета или оттенки серого, расположенные между ними и вычисленные методом линейной интерполяции. В стеке создается структура BITMAPINFO для DIB с прямым порядком строк развертки, после чего растр глифа выводится функцией StretchDIBits с заданной растровой операцией. Учтите, что поле высоты в структуре BITMAPINFO- HEADER имеет обратный знак по отношению к высоте ограничивающего прямо-
Нетривиальный вывод текста 859 угольника глифа; тем самым мы сообщаем GDI, что строки развертки растра следуют в прямом порядке (вместо традиционного обратного порядка). Метод DrawGlyph, основанный на DrawGlyphROP, имитирует вспомогательные операции при выводе растра. Предполагается, что при вызове функции используется режим выравнивания TA_LEFT | TABASELINE. Метод проверяет, не был ли в контексте устройства выбран непрозрачный режим заполнения фона. В этом случае область, в которой выводится глиф, закрашивается цветом фона. После создания кисти, цвет которой определяется текущим цветом текста, растр глифа выводится в прозрачном режиме с использованием тернарной растровой операции 0хЕ20746. Вспомните, что означает код безымянной растровой операции 0хЕ20746: если пиксел источника равен 1, использовать цвет кисти; в противном случае приемник остается без изменений. В данном случае растровая операция выводит основные пикселы текущим цветом текста (при помощи кисти) и не изменяет фоновых пикселов. Поскольку функция DrawGlyph должна быть как можно более универсальной, для вывода отдельных глифов нельзя было воспользоваться простой операцией SRCCOPY — если эта функция последовательно вызывается несколько раз при выводе строки, ограничивающий прямоугольник глифа может перекрываться с ограничивающим прямоугольником предыдущего глифа. Из-за этого функции DrawGlyph приходится задействовать прозрачную растровую операцию при выводе растра глифа. При вызове DrawGlyph не рекомендуется использовать непрозрачный режим заполнения фона, если функция выводит больше одного символа. Фон текста приходится выводить на уровне строки перед выводом самого первого глифа. Функция регулирует экранные координаты в соответствии с координатами базовой точки глифа в структуре GLYPHMETRICS и выводит растр методом DrawGlyphROP. Использовать класс KGlyph для получения информации и вывода растров глифов легко и удобно. В приведенном ниже простом примере строка, выведенная средствами GDI, сравнивается со строками, состоящими из глифов в разных растровых форматах. void Demo_GlyphOutline(HDC hDC) { KLogFont lf(-PointSizetoLogical(hDC. 96), "Times New Roman"); If.mjf.lfltalic = TRUE; lf.mJf.lfQuality - ANTIALIASED_QUALITY; KGDIObject font (hDC. lf.CreateFontO); int x = 20: int у = 160; int dx, dy; SetTextAlign(hDC. TA_BASELINE | TA_LEFT); SetBkColor(hDC. RGB(0xFF. OxFF. 0)); // желтый SetTextColor(hDC. RGB(0. 0. OxFF)); // голубой // SetBkMode(hDC, TRANSPARENT); KGlyph glyph;
860 Глава 15. Текст TextOut(hDC. x. у, "1248". 4); у+= 150; glyph.GetGlyph(hDC, Т. GGO_BITMAP); glyph.DrawGlyph(hDC. x. y. dx, dy);x+=dx; glyph.GetGlyph(hDC. '2'. GG0_GRAY2_BITMAP); glyph.DrawGlyph(hDC, x, y. dx. dy); x+=dx; glyph.GetGlyph(hDC. '4'. GG0_GRAY4_BITMAP): glyph.DrawGlyph(hDC, x, y. dx. dy); x+=dx; glyph.GetGlyph(hDC. '8*. GG0_GRAY8_BITMAP); glyph.DrawGlyph(hDC. x. y. dx. dy); x+=dx; } Функция создает сглаженный логический шрифт, выводит строку «1248» функцией TextOut GDI, а затем выводит четыре отдельных глифа «1», «2», «4» и «8» в форматах GGO_BITMAP, GG0_GRAY2_BITMAP, GG0_GRAY4_BITMAP и GG0_GRAY8_BITMAP. Результат показан на рис. 15.16. Рис. 15.16. Вывод растров глифов, сгенерированных функцией GetGlyphOutline Наверху слева изображена строка, выведенная средствами GDI. Похоже, во внутренней работе GDI использует формат GG0GRAY4BITMAP с 17 уровнями серого цвета. Слева внизу показан результат, выведенный функцией KGlyph. Хорошо видны «зазубрины» цифры «1», выведенной в формате GG0BITMAP, но три других формата практически не отличаются друг от друга. Справа показаны фрагменты растров глифов с 17 и 65 уровнями серого. Внимательный анализ цветного варианта рисунка показывает, что для вычисления промежуточных оттенков GDI не ограничивается линейной интерполяцией в цветовом пространстве RGB. Цвета, используемые GDI, обеспечивают более приятный и красочный результат по сравнению с реализованным в листинге 15.4.
Нетривиальный вывод текста 861 Контур глифа Функция GetGlyphOutline также может вернуть описание контура глифа в виде комбинации прямых и кривых Безье, соответствующих исходному описанию глифа в шрифте TrueType. У приложения появляются новые низкоуровневые возможности для работы с данными, очень близкими к физическому описанию шрифта TrueType. Использование контуров вместо растров находит немало интересных применений. Три флага параметра uFormat определяют формат контура глифа. Общий формат представляет собой последовательность так называемых многоугольников TrueType. Многоугольник TrueType начинается со структуры TTPOLYGONHEADER, за которой следует серия структур TTPOLYCURVE. Если указан формат GGONATIVE, многоугольники TrueType состоят только из прямых линий и квадратичных кривых Безье. Квадратичная кривая Безье определяется тремя точками — двумя конечными и одной контрольной, тогда как кубическая кривая Безье определяется четырьмя точками. Как было показано в главе 14, контур глифа в физических шрифтах TrueType описывается закодированным набором линий и квадратичных кривых Безье с инструкциями для подгонки по сетке. Таким образом, было бы неправильно утверждать, что формат GGONATIVE полностью соответствует описанию глифа TrueType; тем не менее работать с ним в приложениях гораздо удобнее, чем с физическими таблицами глифов. Если указан выходной формат GGOBEZIER, все квадратичные кривые Безье в многоугольниках TrueType преобразуются в кубические. По умолчанию полученный контур подвергается дополнительной обработке — к нему применяются специальные инструкции, улучшающие внешний вид глифа и обеспечивающие постоянство графического стиля при малых размерах шрифтов. Но если флаг GGONATIVE или GGOBEZIER задан в сочетании с GGOUNHINTED, инструкции не применяются. Контур глифа, возвращенный GetGlyphOutline, масштабируется по текущему размеру шрифта с применением матрицы преобразования. Что бы ни говорилось в документации, контур глифа не возвращается в единицах, использованных при его конструировании, и матрица преобразования не игнорируется. Координаты точек в контурах глифов возвращаются в виде 32-разрядных чисел повышенной точности с фиксированной точкой (16-разрядная целая часть со знаком и 16- разрядная дробная часть). К счастью, эти реальные координаты генерируются непосредственно по описанию глифа в шрифте TrueType и не приводятся к системе координат устройства. К ним даже можно самостоятельно применять преобразования, не беспокоясь о потере точности. Ниже приведены определения многоугольников TrueType. typedef struct tagPOINTFX { FIXED x; FIXED y; } POINTFX. FAR* LPPOINTFX; typedef struct tagTTPOLYCURVE { WORD wType; WORD cpfx; POINTFX apfx[l];
862 Глава 15. Текст } TTPOLYCURVE, FAR* LPTTPOLYCURVE; typedef struct tagTTPOLYGONHEADER { DWORD cb; DWORD dwType; POINTFX pfxStart; } TTPOLYGONHEADER, FAR* LPTTPOLYGONHEADER; Контур глифа возвращается в виде блока данных, заполненного последовательностью многоугольников TrueType. Количество многоугольников нигде не указывается явно, хотя размер блока данных известен. Каждый многоугольник TrueType соответствует одному замкнутому контуру в описании глифа. Структура данных имеет переменный размер и начинается со структуры TTPOLYHEADER, за которой следует серия структур TTPOLYCURVE. Поле cb структуры TTPOLYGONHEADER содержит размер многоугольника TrueType, в поле dwType хранится его тип (единственное допустимое значение — TT_P0LYG0N_TYPE), а поле pfxStart определяет начальную/конечную точку многоугольника. Поле pfxStart можно рассматривать как своего рода аналог функции MoveTo GDI. Структура TTPOLYCURVE имеет переменный размер и содержит информацию о cpfx точках. Она может относиться к одному из трех типов (поле wType). Если поле wType равно TT_PRIM_LINE, структура описывает ломаную; значение TT_PRIM_QSPLINE соответствует квадратичной кривой Безье, а значение TT_PRIM_CSPLINE — кубической кривой Безье. Ломаную можно рассматривать как последовательность команд LineTo GDI. Кубическая кривая Безье всегда состоит из N х 3 точек, в GDI ее аналогом является команда Poly- BezierTo. В простейшем случае квадратичная кривая Безье определяется двумя точками; вместе с последней точкой многоугольника они образуют сегмент кривой. Вообще говоря, все точки, за исключением последней, в кривых TT_PRIM_ QSPLINE интерпретируются как контрольные. Если в определении кривой несколько контрольных точек следуют подряд, между каждой парой вставляются дополнительные конечные точки. Впрочем, эта тема обсуждалась в главе 14 при описании формата глифов TrueType. Основная проблема с расшифровкой многоугольника TrueType — перебор сегментов и их преобразование в линии или квадратичные кривые Безье, поддерживаемые GDI. В GDI существует функция PolyDraw, которая выводит серию отрезков и кубических кривых Безье, представленных двумя массивами. В массиве POINT хранятся координаты, а в массиве BYTE — флаги. Если бы нам удалось декодировать контур глифа в подобную структуру, то приложение могло бы вывести его одним вызовом функции GDI. В листинге 15.5 приведено объявление класса KGlyphOutline, предназначенного для расшифровки и вывода контуров глифов. Полная реализация класса находится на прилагаемом компакт-диске. Листинг 15.5. Класс KGlyphOutline: работа с контурами глифов template <int MAX_POINTS> class KGlyphOutline { public: POINT m_Point[MAX_POINTS]; BYTE mjlag [MAXJOINTS]:
Нетривиальный вывод текста 863 int mjiPoints; private: void AddPoint(int x. int y. BYTE flag); void AddQSplineCint xl. int yl, int x2. int y2); void AddCSpline(int xl. int yl, int x2. int y2. int x3. int y3); void MarkLast(BYTE flag); void Transform(int dx. int dy); public: int DecodeTTPolygonCconst TTPOLYGONHEADER * IpHeader, int size); BOOL Draw(HDC hDC. int x, int y); int DecodeOutline(KGlyph & glyph) }: Переменные класса KGlyphOutline соответствуют параметрам функции Poly- Draw — это массив POINT, массив BYTE и количество точек. Метод AddPoint добавляет новую точку с флагом, определяющим ее тип. Метод AddCSpline добавляет кубическую кривую Безье с тремя точками. Метод AddQSpline преобразует квадратичную кривую Безье в кубическую и вызывает AddCSpline. Метод MarkLast используется только для пометки последней точки, замыкающей фигуру. Координаты KGlyphOutline, заданные сначала в виде чисел с фиксированной точкой, сохраняются как 32-разрядные целые, поскольку с исходной структурой FIXED неудобно работать. Метод Transform преобразует число с фиксированной точкой в целое и добавляет начальное смещение. Вы можете легко реализовать дополнительные преобразования — например, для масштабирования или поворота всех точек. Метод DecodeTTPolygon управляет расшифровкой данных, полученных функцией GetGlyphOutline. Он перебирает содержимое структур, вставляет неявные контрольные точки для квадратичных кривых Безье и вызывает три вспомогательные функции для построения кривых. Каждый многоугольник TrueType помечается как замкнутый флагом PTCLOSEFIGURE. Это гарантирует нормальное завершение линий при использовании утолщенного геометрического пера. Листинг 15.5 выглядит проще кода расшифровки данных TrueType, описанного в главе 14, поскольку самая сложная часть преобразования — расшифровка низкоуровневых данных TrueType, преобразование и подгонка по сетке — выполняется шрифтовыми драйверами, драйверами графических устройств и GDI. В следующем фрагменте текстовая строка выводится в виде контура с использованием класса KGlyphOutline. Результаты вызовов OutlineTextOut и функции TextOut GDI показаны на рис. 15.17. BOOL OutlineTextOut(HDC hDC, int x. int y. const TCHAR * str. int count) { if ( count<0 ) count = Jxslen(str); KGlyph glyph; KGlyph0utline<512> outline; while ( count>0 ) {
864 Глава 15. Текст if ( glyph.GetGlyph(hDC. * str. GGO_NATIVE)>0 ) if ( outline.DecodeOutline(glyph) ) outline.Draw(hDC, x. y); x += glyph.m_metrics.gmCellIncX; У += glyph.mjmetrics.gmCelllncY: str ++; count --; } return TRUE; } Outline Рис. 15.17. Вывод контура текста с использованием GetGlyphOutline Форматирование текста В этой главе мы рассмотрели простейший вывод текста (функция TextOut), более сложный вывод на уровне индексов глифов и позиционированием отдельных символов (функция ExtTextOut) и даже низкоуровневые операции с растрами и контурами глифов с применением функции GetGlyphOutline. Короче, мы рассмотрели практически все, что необходимо знать о выводе текста в GDI. Пришло время посмотреть, как эти возможности применяются на практике (если, конечно, вы не предпочитаете работать на уровне таблиц TrueType — но об этом уже говорилось в главе 14). Этот раздел посвящен всевозможным проблемам, связанным с форматированием текста, то есть размещением символов в соответствии с заданными требованиями. Вывод текста с табуляцией Табуляция широко используется в простейших текстовых редакторах для выравнивания текста по столбцам, облегчающего восприятие данных. Впрочем, та-
Форматирование текста 865 булированный вывод текста применяется и во многих современных пакетах. GDI содержит специальные функции для получения метрик и вывода текстовых строк, содержащих внутренние символы табуляции. LONG TabbedTextOut(HDC hDC. int x, int у. LPCTSTR IpString. int nCount, int nTabPosition. LPINT IpnTabStopPositions, int nTabOrigin); DWORD GetTabbedTextExtent(HDC hDC. LPCTSTR IpString. int nCount. int nTabPosition. LPINT IpnTabStopPositions); По сравнению с TextOut функция TabbedTextOut получает три новых параметра. В массиве, заданном параметрами IpnTabStopPositions и nCount, хранятся последовательные горизонтальные координаты позиций табуляции. Параметр nTabOrigin содержит смещение, прибавляемое ко всем позициям табуляции. Если в массиве хранятся относительные координаты позиций, nTabOrigin задает начальную точку для отсчета позиций табуляции, благодаря чему массив перестает зависеть от конкретной позиции вывода. Если выводимая строка содержит символы табуляции (\t), GDI выводит начало строки, пока не обнаружит символ табуляции. Для этого GDI просматривает таблицу позиций табуляции, находит в ней ближайшую позицию и продолжает вывод текста с этой позиции. Это повторяется до тех пор, пока не будет выведена вся строка. Если количество символов табуляции в строке превышает количество элементов в таблице, GDI вычисляет дополнительные позиции табуляции на основании последней заданной. Другими словами, если вы хотите использовать равноудаленные позиции табуляции, достаточно передать массив из одного элемента. Обратите внимание: функция не гарантирует, что символы после п-то символа табуляции будут выводиться в п-й позиции табуляции. Вместо этого GDI ищет в таблице следующую позицию табуляции, ближайшую к текущей позиции в тексте. Позиции табуляции могут быть отрицательными; в этом случае GDI выравнивает текст по правому краю перед заданной позицией, вместо выравнивания по левому краю после нее. Функция TabbedTextOut возвращает 32-разрядное число, старшее слово которого содержит высоту, а младшее — ширину строки. Оба значения задаются в логических координатах. Функция GetTabbedTextExtents возвращает размеры табулированного текста, не выводя его. Простой пример использования TabbedTextOut для вывода текста в столбцах с помощью табуляций. int tabstop[] = { -120. 125. 250 }; const TCHAR * lines[] - { "Group" "\t" "Result" "\t" "Function" "\t" "Parameters". "Font" "\t" "DWORD" "\t" "GetFontData" "\t" "(HDC hDC. ...)". "Text" "\t" "BOOL" "\t" "TextOut" "\t" "(HDC hDC. ...)" }: int x=50. y=50; for (int i=0: i<3; i++) у +- HIWORD(TabbedTextOut(hDC. x. у lines[i]. _tcs1en(lines[i]). sizeof(tabstop)/sizeof(tabstop[0]), tabstop, x));
866 Глава 15. Текст Программа выводит три строки текста в четыре столбца в позициях х, х + 120, х + 125 и х + 250, причем в позиции х + 120 текст выравнивается по правому краю. Обратите внимание: начальная координата х передается функции GetTabbed- TextOut в последнем параметре, поэтому позиции в массиве задаются относительно начала текста. Вы должны проследить за тем, чтобы позиции табуляции были достаточно удалены друг от друга, а текст выводился аккуратно упорядоченным по столбцам. Простое абзацное форматирование В пользовательском интерфейсе Windows часто требуется вывести длинный текст в прямоугольнике, способном вместить несколько строк. Примеров множество: вывод текста на кнопках, однострочные и многострочные статические элементы и текстовые поля и т. д. В системе управления окнами Windows (user32.dll) поддерживаются две функции для простейшего форматирования текста, в основном используемые при построении пользовательского интерфейса. int DrawText(HDC hDC. LPCTSTR IpString, int nCount, LPRECT IpRect. UINT uFormat); typedef struct { UINT cbSize; int iTabLength; int iLeftMargin; int iRightMargin; UINT uiLengthDrawn; } DRAWTEXTPARAMS; int DrawTextEx(HDC hDC, LPTSTR IpchText, int cchText. LPRECT IpRect, UINT dwDTFormat. LPDRAWTEXTPARAMS IpDTParams); Функция DrawText выводит текстовую строку в прямоугольной области, заданной параметром IpRect в логических координатах. Последний параметр uFormat содержит до двух десятков флагов, которые определяют интерпретацию управляющих символов, режим расширения символов табуляции и замены частей строки многоточиями и т. д. За подробной информацией об этих флагах обращайтесь к электронной документации. Функция DrawTextEx получает дополнительный параметр — указатель на структуру DRAWTEXTPARAMS, которая определяет расстояние между позициями табуляции, левые и правые поля, а также используется для возвращения длины выведенной строки. Функции DrawText и DrawTextEx рассчитаны на вывод однострочного и многострочного текста. Однострочный текст часто встречается в меню, на панелях инструментов, кнопках, статических полях и т. д. Эти две функции учитывают стандартные требования к этим строкам, включая вертикальное и горизонтальное выравнивание, отсечение, расширение символов табуляции, интерпретацию префиксов (знак & означает подчеркивание следующего символа) и три режима интерпретации многоточий. Многострочные тексты встречаются в многострочных статических и текстовых полях. Функции DrawText и DrawTextEx обеспечивают простое и удобное форматирование абзацев с разбивкой на слова. На рис. 15.18 приведены примеры использования функции DrawText.
Форматирование текста 867 Open с :\Win\system32\gdi32.c Open c:\Wi n\sy ste m 3 2\g d i 3 Э Open c:\Win\syst..\gdi32.dll &Open c:\Win\system32\gdi... Open c:\Win\system32\gdi... IThe DrawText function draws formatted text in the jspecified rectangle. It Ifnrmflts thp. tp.vt pir.nnrdinn tn The DrawText function draws formatted text in the specified rectangle. It fnrmflts thp tp.yt nr.nnrrlinn tn The DrawText function draws formatted text in the specified rectangle. Itl Рис. 15.18. Форматирование текста функциями DrawText/DrawTextEx В левой части рисунка приведены примеры форматирования однострочного текста с приведенными далее комбинациями флагов. В частности, рисунок иллюстрирует вертикальное выравнивание, расширение табуляций, маскировку префикса и разные варианты использования многоточий. Флаг DTMODIFYSTRING даже позволяет изменить исходную строку, привести ее в соответствие с выведенными символами и использовать в другом месте. const TCHAR * mess = "SOpen \tc:\\Win\\system32\\gdi32.dll": RECT rect = { 20. 120. 20+180. 120 + 32 }; int opt[] = { DT_SINGLELINE | DTJOP. DT_SINGLELINE | DTJ/CENTER | DTJXPANDTABS. DT_SINGLELINE | DT_B0TT0M | DTJXPANDTABS | DT_PATH_ELLIPSIS. DT_SINGLELINE | DTJIOPREFIX | DTJXPANDTABS | DT_WORDJLLIPSIS. DTJINGLELINE | DTJIDEPREFIX | DTJXPANDTABS | DTJNDJLLIPSIS. }: Справа на рисунке показано, как DrawText преобразует длинный текст в абзац, состоящий из нескольких строк. В трех приведенных вариантах используются разные флаги горизонтального выравнивания. Хотя функции DrawText/DrawTextEx обладают удобными средствами форматирования многострочного текста, их возможности ограничиваются обслуживанием простых нужд пользовательского интерфейса. Для WYSIWYG этого явно недостаточно. Рис. 15.19 иллюстрирует это утверждение. На рисунке текст, оформленный шрифтом с кеглем 6, 8, 12 и 15 пунктов, выводится в текстовых областях, размер которых пропорционален кеглю (ширина = 42 х кегль, высота = 5х кегль). В идеальном случае абзац должен делиться на строки в одних и тех же местах, но на рисунке видно, что положение разрывов строк изменяется. Для вывода лицензионного соглашения в диалоговом окне сойдет, но в серьезном текстовом редакторе это неприемлемо. Пользователи будут сильно огорчены, если текст по-разному форматируется при разных масштабах изображения или при печати на принтерах с разным разрешением. Конечно, проблема связана с тем, что возвращаемые GDI простые целочис-
868 Глава 15. Текст ленные метрики страдают от накопления погрешности. Чтобы форматирование текста соответствовало принципу WYSIWYG, необходимо принять особые меры. IT he DrawTexl function draws formatted text in the specified rectangle. It formats I the text according to the specified method (expanding tabs, justifying characters, breaking lines, and so forth). |The DrawText function draws formatted text in the specified rectangle. I It formats the text according to the specified method (expanding tabs, [justifying characters, breaking lines, and so forth). | [The DrawText function draws formatted text in the specified rectangle. It I ormats the text according to the specified method (expanding tabs, ustifying characters, breaking lines, and so forth). | Рис. 15.19. Неточности форматирования текста при использовании функции DrawText Зависимость DrawText/DrawTextEx от конкретного разрешения отчасти объясняет, почему диалоговые окна, предназначенные для экрана с разрешением 96 dpi, плохо выглядят на экране с разрешением 120 dpi. При смене разрешения изменяется высота стандартных шрифтов. Из-за погрешностей округления связь между логическим разрешением, высотой шрифта и шириной шрифта не является строго линейной. Следовательно, текст, выводимый в 5 строк на экране 96 dpi, при 120 dpi может превратиться в 4 строки или, что еще хуже — в 6 строк. Аппаратно-независимое форматирование текста При форматировании многострочного абзаца аппаратно-независимый алгоритм должен генерировать одну и ту же раскладку текста при любом разрешении графического устройства или настройке системы координат. Если нам удастся решить эту задачу, содержимое экрана будет точно совпадать с результатами, напечатанными на принтере, изображение будет одинаковым при разных настройках экрана, а раскладка текста сохранится при изменении масштаба. Алгоритм аппаратно-независимого форматирования текста основан на получении точных метрик текста и их использовании для вычисления разрывов строк и управления выводом на экран. Мы рассматривали возможность создания эталонного логического шрифта, размер которого совпадает с размером em- квадрата шрифта TrueType (сетки, в которой определяются глифы). Текстовые метрики, полученные на основании эталонного шрифта, обладают необходимой точностью. Располагая точными текстовыми метриками, можно вычислить метрики для шрифта с заданным кеглем в виде вещественных чисел, также обладающих высокой точностью. При наличии такой информации функции GetTextExtentPoint32, ExtTextOut и даже DrawText заменяются более точными реализациями.
Форматирование текста 869 В листинге 15.6 приведены объявления двух классов, реализующих аппарат- но-независимое форматирование текста. Полные реализации находятся на компакт-диске. Листинг 15.6. Классы аппаратно-независимого форматирования текста class KTextFormator { typedef enum { MaxCharNo = 256 }; float m_fCharWidth[MaxCharNo]; float m_fHeight; public: BOOL SetupPixeKHDC hDC. HFONT hFont. float pixelsize): BOOL SetupCHDC hDC. HFONT hFont. float pointsize); BOOL GetTextExtent(HDC hdc. LPCTSTR pString. int cbString. float & width, float & height); BOOL TextOut(HDC hDC. int x. int у. LPCTSTR pString. int nCount): DWORD DrawTextCHDC hDC. LPCTSTR pString. int nCount. const RECT * pRect. UINT uFormat): class KLineBreaker { LPCTSTR m_pText; i nt mjlength; int m_nPos: BOOL SkipWhite(void); // Пропуск пробелов void GetWord(void); // Чтение следующего слова BOOL Breakable(int pos); // Попытка разбиения слова public- float textwidth. textheight; KLineBreakerCLPCTSTR pText. int nCount); BOOL GetLineCKTextFormator & formator, HDC hDC. int width, int & begin, int & end); }: Класс KTextFormator реализует аппаратно-независимые версии трех функций GDI. В переменных этого класса, инициализируемых методом Setup, хранятся данные о ширине символов и высоте текста в формате с плавающей точкой. Метод KTextFormator: -.GetTextExtent является аналогом функции GDI GetTextExtentPoint32, но возвращает числа с плавающей точкой. Два других метода являются аналогами функций TextOut и DrawText GDI. Метод Setup создает эталонный шрифт с размером em-квадрата и запрашивает ширину 256 символов текущего набора (по предположению — однобайтового). Расширение класса для поддержки Unicode и двухбайтовых наборов потребует дополнительных усилий. Наконец, метод масштабирует значения ширины символов на основании текущего кегля и разрешения устройства. Обратите
870 Глава 15. Текст внимание: кегль передается методу Setup в числе параметров вместо того, чтобы получать информацию по манипулятору текущего шрифта. Если запросить высоту текста по манипулятору и вычислить по ней кегль, результат может оказаться неточным. Предполагается, что в кегле уже была учтена настройка текущей системы координат. Реализация методов GetTextExtent и TextOut весьма проста. Метод GetTextExtent просто суммирует значения ширины символов в строке. В аппаратно-незави- симой версии TextOut нельзя использовать исходную функцию TextOut GDI, поскольку GDI задействует данные о ширине символов в системе координат устройства. К счастью, в GDI имеется функция ExtTextOut, получающая массив расстояний между символами. Следовательно, нам остается лишь заполнить массив вещественными значениями ширины и вызвать ExtTextOut. Программа накапливает суммарную ширину символов, для каждого символа преобразует ее в целое число и вычисляет расстояние по полученной величине. Это гарантирует, что отклонение для каждого символа не превысит половины пиксела. Основные сложности в реализации DrawText связаны с разбиением текста на строки по значениям левого и правого поля. Задача решается при помощи другого класса KLineBreaker. Главный метод этого класса, KLineBreaker: :GetLine, определяет, сколько символов разместится по заданной ширине. GetLine добавляет слова до тех пор, пока длина строки не превысит максимально допустимую, а затем пытается найти такое разбиение последнего слова, чтобы оставшиеся символы помещались в заданной области. Простейший способ разбиения слов реализуется методом Breakable. Текущая реализация KTextFormator:: DrawText последовательно получает строки, вызывая метод KLineBreaker::GetLine, и выводит их методом TextOut. Следующая функция помогает сравнить функцию DrawText GDI с аппаратно- независимыми средствами класса KTextFormator. void Demo_Paragraph(HDC hDC. bool bPrecise) { const TCHAR * mess = "The DrawText function draws formatted text in the specified " "rectangle. It formats the text according to the specified " "method (expanding tabs, justifying characters, breaking "lines, and so forth)."; int x = 20; int у - 20; SetBkMode(hDC, TRANSPARENT); for (int size=6; size<=21; size+=3) // 6, 9, 12, 15. 18, 21 { int width = size * 42; int height = size * 5; KLogFont lf(-PointSizetoLogical(hDC. size), "MS Shell Dig"); lf.mjf.lfQuality - ANTIALIASED_QUALITY; KGDIObject font (hDC. If .CreateFontO); RECT rect = { x. y. x+width. y+height };
Эффекты при выводе текста 871 if ( bPrecise ) { KTextFormator format; format.Setup(hDC, (HFONT) font.mJiObj, size); format.DrawText(hDC. mess, _tcslen(mess). & rect, DT_WORDBREAK | DT_LEFT); } else DrawText(hDC, mess, _tcslen(mess), & rect. DT_WORDBREAK | DT_LEFT); Box(hDC, rect.left-1, rect.top-1, rect.right+1, rect.bottom+1); у = rect.bottom + 10; } } Функция Demo_Paragraph получает логический параметр bPrecise. Если этот параметр равен TRUE, для вывода многострочного текста используется класс KTextFormator; в противном случае используется функция DrawText GDI. Результаты применения стандартной реализации GDI вы видели на рис. 15.19. WYSIWYG- версия, реализованная методом KTextFormator::DrawText, изображена на рис. 15.20. ГТЙй OsirtfTft*1 bt*ViS.tt-: <.! ?«№':: $.№:«:#;&*•; :W.?5.4« #№ tttSCfA&P.il .r^K'AWsyNf. I I?: b:WK& K# Vtfi ::*tt.V:*::::y Jft И'!* фяйй&йЗ fitfUwfi ?<J%iWUi::* Vj itfji. I |The DrawTexHunclion draws formatted text in Ihe specified rectangle. I It formats the text according to the specified method (expanding tabs, justifying characters, breaking lines, and so forth). [The DrawText function draws formatted text in the specified rectangle. It formats the text according to the specified method (expanding tabs, pustitying characters, breaking lines, and so forth). [The DrawText function draws formatted text in the specified rectangle. It formats the text according to the specified method (expanding tabs, Justifying characters, breaking lines, and so forth). Рис. 15.20. Форматирование текста с использованием класса KTextFormator Рисунок наглядно показывает, что при использовании вещественных метрик смена разрешения устройства и масштаба никак не сказывается на форматировании абзацев и выводе текста. Эффекты при выводе текста В предыдущем разделе было показано, как управлять различными аспектами вывода текста на уровне GDI. Следующий вопрос — как воспользоваться этими возможностями для создания эффектов при выводе текста?
872 Глава 15. Текст В простейших текстовых редакторах текст выглядит тривиально, черные буквы на белом фоне. Вам остается лишь рассчитать, как правильно отформатировать этот текст. Впрочем, существуют многочисленные эффекты, позволяющие оформлять заголовки или выделять фрагменты текста. В этом разделе рассматривается изменение цвета и формы текста, а также использование текста в виде растров и кривых. Цвет текста В контекстах устройств GDI предусмотрены атрибуты, определяющие цвет текста, цвет фона и режим заполнения фона при выводе текста. Например, чтобы текст выводился синим цветом, достаточно вызвать функцию SetTextCo1or(hDC, RGB(0xFF,0,0)). В GDI также предусмотрена возможность вывода сглаженного текста с использованием промежуточных цветов между цветами текста и фона, устраняющих неровности на границах глифов. Чтобы вывести сглаженный текст, при создании логического шрифта следует указать параметр качества ANTIALIASED_ QUALITY. В GDI цвета текста и фона могут быть только однородными. В режиме с 256 цветами GDI перед выводом всегда заменяет указанный текст однородным цветом из палитры. Кроме того, в GDI не предусмотрена возможность закраски текста произвольной кистью. Конечно, это вызывает определенные проблемы при выводе текста в пользовательском интерфейсе, поэтому была предусмотрена специальная функция для вывода «затушеванных» текстовых строк. Функция GrayString, реализованная системой управления окнами (user32.dll), позволяет раскрашивать текст кистями и удалять из глифов некоторые пикселы (предполагается, что затушеванный текст будет выводиться на недоступных элементах). Поскольку GDI не поддерживает закраски текста кистью, функция GrayString создает совместимый контекст устройства, преобразует текст в растр и работает с полученным растром. Как и другие графические функции, не относящиеся к GDI, функция GrayString обладает достаточно простыми возможностями и предназначается в основном для вывода текста на экран. В частности, GrayString не поддерживает отрицательные значения метрик А и С. Несмотря на отсутствие прямой поддержки, цветной текст можно вывести и другими средствами GDI. В одном из простых решений используется растровая операция, а раскраска выполняется в три этапа. Если вы хотите вывести текст кистью цвета Р, выполните следующие действия. 1. Закрасьте нужную область кистью Р с растровой операцией PATINVERT. Приемник переходит в состояние DAP. 2. Выведите текстовую строку в прозрачном режиме с черным цветом текста. Фон сохраняет цвет DAP, а текст окрашен в черный цвет (0). 3. Снова воспользуйтесь кистью Р с растровой операцией PATINVERT. Фон восстанавливает цвет D, а текст окрашивается в цвет Р. Аналогичный способ применяется для вывода непрозрачного текста с применением кистей для закраски фона и текста, а также для заполнения текста растровыми изображениями или градиентными заливками. Главные трудности связаны
Эффекты при выводе текста 873 с вычислением ограничивающего прямоугольника, поскольку сколько-нибудь общее решение должно учитывать отрицательные значения метрик А и С. В листинге 15.7 приведена одна из возможных реализаций этой идеи. Функция GetOpaqueBox вычисляет ограничивающий прямоугольник выводимой строки. Функция ColorText показывает, как закрасить текст кистью. Аналогичная функция для заполнения текста растровым изображением в книге не приводится. Листинг 15.7. Закраска текста кистью BOOL GetOpaqueBoxCHDC hDC. LPCTSTR lpString, int cbString. RECT * pRect, int x, int y) { long height; ABC abc; if ( ! GetTextABCExtent(hDC, lpString, cbString. & height. & abc) ) return FALSE; switch ( GetTextAlign(hDC) & (TA_LEFT | TA_RIGHT | TA_CENTER) ) case TA_LEFT case TA_RIGHT case ТА CENTER break; x -= abc.abcB; break; x -= abc.abcB/2; break; default: assert(false); switch ( GetTextAlign(hDC) & (TA_T0P | TA_BASELINE | TAJOTTOM) ) { case TA_T0P case TA_B0TT0M case ТА BASELINE break; у = - height; break; TEXTMETRIC tm: GetTextMetrics(hDC. & tm); у = - tm.tmAscent; } break; default: assert(false); pRect->left = x + min(abc.abcA. 0); pRect->right = x + abc.abcA + abc.abcB + max(abc.abcC. 0); pRect->top = y; pRect->bottom = у + height; return TRUE; BOOL ColorText(HDC hDC. int x. int y. LPCTSTR pString. int nCount. HBRUSH hFore) { HGDIOBJ hOld - SelectObject(hDC, hFore); RECT rect; GetOpaqueBox(hDC. pString. nCount, & rect. x. y); Продолжение &
874 Глава 15. Текст Листинг 15.7. Продолжение PatBlt(hDC. rect.left. rect.top, rect.right-rect.left, rect.bottom - rect.top, PATINVERT); int oldBk = SetBkModeChDC. TRANSPARENT); COLORREF oldColor = SetTextColorChDC, RGB(0, 0, 0)); TextOut(hDC. x, y. pString, nCount); SetBkModeChDC, oldBk); SetTextColor(hDC. oldColor); BOOL rslt = PatBltChDC. rect.left. rect.top. rect.right-rect.left. rect.bottom - rect.top. PATINVERT); SelectObject(hDC. hOld); return rslt; } Функция GetOpaqueBox проверяет метрику А первого символа и метрику С последнего символа и смотрит, не отрицательны ли они. Для проверки используется функция GetTextABCExtent, описанная в этой главе. Кроме того, мы учитываем разные комбинации флагов вертикального и горизонтального выравнивания и вносим соответствующие поправки в параметры прямоугольника. Функция ColorText дважды закрашивает прямоугольник при помощи функции PatBlt с растровой операцией PATINVERT. Перед выводом текста необходимо правильно задать режим заполнения фона и цвет текста и восстановить исходные значения после его завершения. На рис. 15.21 приведены примеры использования функций GrayString, ColorText и BitmapText. Рис. 15.21. Закраска текста функциями GrayString, ColorText и BitmapText В функциях ColorText и BitmapText задействованы три операции графического вывода, и при непосредственном выводе в экранный контекст устройства возникает неприятное мерцание. Проблема решается кэшированием вывода в промежуточном контексте устройства или использованием других приемов, не требующих многократной прорисовки. Учтите, что вследствие применения рас-
Эффекты при выводе текста 875 тровых операций сглаживание шрифтов не рекомендуется, поскольку на рисунке появятся пикселы довольно странных цветов. Начертания При создании логического шрифта приложение может выбрать различные варианты начертания (насыщенность, курсив, сглаживание, подчеркивание и перечеркивание) при помощи атрибутов структуры L0GF0NT. Семейство шрифтов TrueType обычно состоит из четырех физических файлов для поддержки четырех начертаний: обычного, полужирного, курсивного и полужирного курсивного. Система сопоставляет требования пользователя с атрибутами установленных шрифтов и находит оптимальное соответствие. Если шрифт с указанной насыщенностью недоступен, GDI пытается синтезировать его простейшим способом (только если доступный шрифт имеет обычную насыщенность, а запрашиваемый является полужирным). Имитация настолько проста, что различия заметны лишь при малом размере шрифта — GDI просто выводит строку дважды с горизонтальным смещением в один пиксел. Если у вас имеется только полужирный шрифт, GDI не сможет синтезировать по нему шрифт с обычным начертанием. Курсивные шрифты синтезируются гораздо проще. Как говорилось выше, последний параметр функции GetGI yphOutl i ne определяет матрицу преобразования 2x2, что позволяет подвергнуть глиф преобразованию сдвига. Имитация курсивного начертания сводится к небольшому сдвигу с учетом изменения в метриках шрифта. При описании функции GetGI yphOutl i ne упоминалось и о сглаживании, поддерживаемом шрифтовыми драйверами. В GDI для сглаживания текста применяются растры глифов с 17 уровнями серого. Чтобы запросить сглаживание для шрифта TrueType, передайте флаг ANTIALIASEDQUALITY в поле качества; контекст устройства при этом должен находиться в режиме High Color или True Color. Подчеркивание и перечеркивание реализуются GDI на основании данных, содержащихся в структуре OUTLINETEXTMETRIС шрифта TrueType. В некоторых приложениях поддерживаются разные стили подчеркивания и перечеркивания, но все они синтезируются по одним и тем же данным. Кроме эффектов, поддерживаемых на уровне логического шрифта, в приложениях часто реализуются и другие текстовые эффекты — негативный вывод, тени, рельеф (приподнятый и утопленный), обводка контуров и т. д. Негативный вывод реализуется легко — достаточно поменять местами цвет текста с цветом фона и вывести текст в непрозрачном режиме. Для точного воспроизведения эффектов теней и рельефа приходится работать на уровне растров и моделировать источники света. В текстовых редакторах обычно используется упрощенный подход — одна и та же строка выводится несколько раз с разными смещениями и цветами. Функция OffsetTextOut выводит текстовую строку до трех раз. Первые пять параметров аналогичны параметрам функции TextOut. Следующие две группы из трех параметров задают смещение и одет строки при повторном выводе. Сначала функция выводит первую смещенную строку с параметрами (x + dxl,y + dyl,crtl), затем вторую смещенную строку с параметрами (х + dx2,y + dy2,crt2), после чего
876 Глава 15. Текст выводит исходную строку от точки (х,у) с исходным цветом текста. Программа рисует увеличенный прямоугольник, а две смещенные строки рисуются в прозрачном режиме. BOOL OffsetTextOut(HOC hDC, int x. int у. LPCTSTR pStr. int nCount. int dxl, int dyl, COLORREF crl, int dx2, int dy2. COLORREF cr2) { COLORREF cr = GetTextColor(hDC); int bk = GetBkMode(hDC); if ( bk==0PAQUE ) { RECT rect; GetOpaqueBoxChDC. pStr, nCount. & rect. x, y); rect.left += min(min(dxl, dx2), 0) rect.right += max(max(dxl. dx2). 0) rect.top += min(min(dyl. dy2). 0) rect.bottom+= max(max(dyl. dy2). 0) ExtTextOutChDC. x. y. ET0_0PAQUE, & rect, NULL. 0. NULL); } SetBkMode(hDC. TRANSPARENT); if ( (dxl!=0) || (dyl!=0) ) { SetTextColor(hDC. crl); TextOut(hDC. x + dxl. у + dyl. pStr. nCount); if ( (dxl!=0) || (dyl!=0) ) { SetTextColor(hDC. cr2); TextOut(hDC. x + dx2. у + dy2. pStr. nCount); } SetTextColor(hDC. cr); BOOL rslt = TextOut(hDC. x. y. pStr. nCount); SetBkMode(hDC. bk); return rslt; } В следующем фрагменте показано, как при помощи функции OffsetTextOut создаются эффекты тени, приподнятого и утопленного рельефа. // Тень SetBkColor(hDC. RGB(0xFF. OxFF. 0)); // Желтый фон SetTextColorChDC. RGB(0. 0. OxFF)); // Синий текст OffsetTextOut(hDC. x. у. "Shadow". 6. 4. 4. GetSysColor(C0L0R_3DSHAD0W). // Тень 0. 0. 0);
Эффекты при выводе текста 877 // Приподнятый рельеф SetBkColor(hDC. RGB(0xD0. OxDO. 0)); // Темно-желтый фон SetTextColor(hDC, RGBCOxFF. OxFF, 0)); // Желтый текст OffsetTextOut(hDC. х, у. "Emboss", 6, -1, -1, GetSysColor(COLOR_3DLIGHT), 1.1. GetSysColor(COLORJDDKSHADOW)); // Углубленный рельеф SetBkColorChDC. RGBCOxFF. OxFF. 0)); // Желтый фон SetTextColor(hDC. RGB(0xD0. OxDO. 0)); // Темно-желтый текст OffsetTextOutChDC. x. y. "Engrave". 7. -1. -1. GetSysColor(C0L0R_3DDKSHAD0W). 1. 1. GetSysColor(C0L0R_3DLIGHT)): Тень имитируется предварительным выводом серого текста со смещением (4,4). Для создания эффекта приподнятого рельефа сначала выводится более светлый текст со смещением (-1, -1), а затем — более темный текст со смещением (1,1). Эффект утопленного рельефа имитируется аналогично, просто цвета меняются местами. Смещения, использованные в этом примере, подходят только для обычного вывода на экран. При выводе с увеличением или печати на принтере в них необходимо внести соответствующие изменения. На рис. 15.22 изображены различные варианты стилевого оформления текста: обычный, полужирный, сглаженный, курсив, полужирный курсив, подчеркнутый, перечеркнутый, негативный, тень, приподнятый и утопленный рельеф. Style Style Shadow Style Style Style Style Рис. 15.22. Начертания текста Геометрические эффекты До настоящего момента мы рассматривали только пропорциональный текст, выводимый вертикально. Настало время познакомиться с выводом текста с разной ориентацией и углом поворота символов, с искажением исходных пропорций и т. д.
878 Глава 15. Текст- Структура LOGFONT содержит два атрибута, связанных с углом вывода текста. В поле 1 fEscapement задается угол наклона базовой линии всей строки, а в поле 1 fomentation — угол наклона базовой линии отдельных символов. В архитектуре GDI эти углы могут различаться. Например, строка может выводиться горизонтально (IfEscapement = 0), но каждый символ в ней может быть повернут на 10°. Впрочем, независимое определение углов поддерживается только в расширенном графическом режиме и поэтому доступно только в Windows NT/2000. В совместимом графическом режиме поле 1 fEscapement определяет оба угла. Углы задаются целыми числами в десятых долях градуса. Для большинства приложений такой точности вполне достаточно. Нужные значения углов заносятся в структуру LOGFONT перед созданием логического шрифта. Сделать это нетрудно, но при частом изменении углов в функции вывода такое решение оказывается хлопотным и неудобным. Возможен другой подход — оставить тот же логический шрифт и определить другое мировое преобразование с поворотом логической системы координат. Одним из самых интересных применений смены наклона текста является размещение символов вдоль кривой (такая возможность поддерживается во многих графических пакетах). В GDI любую кривую можно преобразовать в траекторию GDI, заключив графические команды между вызовами BeginPath и EndPath. Полученная траектория преобразуется в ломаную функцией FlattenPath. После этого можно воспользоваться функцией GetPath для получения всех точек, определяющих траекторию (операции с линиями и кривыми подробно описаны в главе 8). Располагая массивом с данными траектории, можно получить смещение отдельного символа и вычислить координаты соответствующего отрезка ломаной. По координатам отрезка (х0,у0) - (х^) вычисляется точка вывода и угол наклона символа. В листинге 15.8 приведена группа функций для размещения текста вдоль кривой, определяемой текущей траекторией в контексте устройства. Листинг 15.8. Размещение текста вдоль траектории double dis(double xO, double yO, double xl, double yl) { xl -= xO; У1 -= yO; return sqrt( xl * xl + yl * yl ); } const double pi = 3.141592654; BOOL DrawChar(HDC hDC. double xO, double yO, double xl. double yl, TCHAR ch) { xl -= xO; yl -= yO; int escapement = 0; if ( (xKO.Ol) && (xl>-0.01) ) if ( yl>0 ) escapement = 2700;
Эффекты при выводе текста 879 else escapement = 900; else { double angle = atan(-yl/xl); escapement = (int) ( angle * 180 / pi * 10 + 0.5); } LOGFONT If; GetObject(GetCurrentObject(hDC, OBJJONT), sizeof(lf), &lf); if ( If.IfEscapement != escapement ) { If.IfEscapement = escapement; HFONT hFont = CreateFontIndirect(&lf); if ( hFont—NULL ) return FALSE; DeleteObject(SelectObject(hDC. hFont)); } TextOut(hDC. (int)xO, (int)yO. &ch. 1); return TRUE; } void PathTextOut(HDC hDC. LPCTSTR pString. POINT point[]. int no) { double xO = point[0].x; double yO = point[0].y; for (int i=l; i<no; i++) { double xl = point[i].x; double yl = point[i].y; double curlen = dis(x0. yO. xl. yl); while ( true ) { int length; GetCharWidth(hDC. * pString. * pString. & length); if ( curlen < length ) break; double xOO = xO; double yOO = yO; xO += (xl-xO) * length / curlen: yO += (yl-yO) * length / curlen; DrawChar(hDC. xOO. yOO. xO. yO. * pString): curlen -= length; Продолжение^
880 Глава 15. Текст Листинг 15.8. Продолжение pString ++; if ( * pString==0 ) { i = no; break; Основные операции со шрифтом в листинге 15.8 сосредоточены в функции DrawChar. При вызове функции передается манипулятор контекста устройства, две точки, определяющие отрезок, и код символа. По координатам точек функция вычисляет угол наклона, создает логический шрифт и при необходимости выбирает его вместо старого логического шрифта. После того как нужный шрифт выбран в контексте устройства, вывод одного символа сводится к простому вызову TextOut. В первой версии PathTextOut кривая, вдоль которой размещаются символы, определяется простым перебором точек массива. Вторая версия PathTextOut (на компакт-диске) предполагает, что текст размещается вдоль текущего объекта траектории в заданном контексте устройства. Перед тем как вызвать первую версию PathTextOut, она при помощи функций GDI преобразует траекторию и получает ее данные. На рис. 15.23 продемонстрировано изменение угла наклона всей строки и отдельных символов, применение функции PathTextOut, а также вертикальное азиатское письмо и шрифты с искаженными пропорциями. Тех,0 м $ fc!i Sj а _ о., -I- * ад 8 § & # оЪ Ч Я Я- 75/oW *ФЛ#\ $ т ш юо% w <V . % * 111 125% W 20 <%,„ l0^deg ^е 006 deg Рис. 15.23. Геометрические эффекты Слева внизу десять строк выводятся под углами от 0 до 90°. В диапазоне от 0 до 40° текст выводится с нулевым наклоном символов и с изменяющимся на-
Эффекты при выводе текста 881 клоном базовой линии строки. Все символы расположены вертикально, но после каждого символа позиция вывода смещается вправо и вверх в соответствии с заданным углом наклона базовой линии. Также обратите внимание на увеличивающийся прямоугольник фона. При различающихся углах наклона строки и отдельных символов текст не рекомендуется выводить в непрозрачном режиме. В диапазоне от 50 до 90° строки выводятся с одинаковыми углами наклона. Кривая, вдоль которой размещаются символы длинной строки в центре рисунка, демонстрирует работу функции PathTextOut. Фоновые прямоугольники показывают, что при отсутствии крутых изгибов текст достаточно плавно размещается вдоль кривой. Текст, выводимый под углом 270°, направлен сверху вниз, как в традиционной китайской и японской письменности. Впрочем, каждый символ дополнительно разворачивается на 90° против часовой стрелки. Для шрифтов, поддерживающих двухбайтовые кодировки (в частности, китайскую и японскую), предусмотрены специальные имена, при которых символы разворачиваются на 90° и принимают вертикальное положение. Все, что для этого требуется, — поставить символ «@» перед именем гарнитуры. Например, вместо «SimSun» используется имя «©SimSun». На рис. 15.23 приведены две строки из стихотворения времен династии Тан в китайской традиционной письменности (символы следуют сверху вниз, строки заполняют лист справа налево). Код следующего фрагмента выводит вертикально ориентированный текст. KLogFont 1f(-PointSizetoLogica1(hDC. 24), "@SimSun"); lf.mJf.lfQuality = ANTIALIASED_QUALITY; lf.mjf .lfCharSet = GB2312_CHARSET; If.mjf. If Escapement = 2700; lf.mjf.lfOrientation = 2700; KGDIObject font (hDC. If .CreateFontO); SetBkColor(hDC, RGB(0xFF. OxFF. 0)); WCHAR linel[] = { 0x59Dl. 0x82CF, 0x57CE. 0x5916. 0x5BD2. 0x5C71. 0x5BFA }; WCHAR line2[] = { 0x591C. 0x534A. 0x949F. 0x58F0. 0x5230, 0x5BA2. 0x8239 }; Text0utW(hDC. xO. yO. linel. 7): xO -= 45; TextOutWChDC, xO. yO, line2, 7); xO -= 50; TextOut (hDC. xO. yO. "270 degree". 10): При создании логического шрифта поле IfWidth структуры L0GF0NT обычно равно 0; это означает, что GDI следует подобрать шрифт с таким же аспектным отношением, как у графического устройства. Аспектным отношением называется отношение ширины пиксела к его высоте. Аспектное отношение текущего контекста устройства можно получить при помощи функции GetAspectRatioFilterEx. Практически все современные графические устройства обладают аспектным отношением 1:1, поэтому шрифт, созданный с нулевым полем IfWidth, обладает правильным соотношением ширины и высоты. При передаче ненулевой величины в поле IfWidth GDI сопоставляет ее со средней шириной шрифта и имитирует шрифт с искажением пропорций. Чтобы созданный шрифт был шире или уже исходного, следует перед заполнением поля IfWidth запросить среднюю ширину шрифта, хранящуюся в поле tmAveCharWidth структуры TEXTMETRIC. В еле-
882 Глава 15. Текст дующем фрагменте создается логический шрифт, ширина которого составляет заданный процент от ширины текущего шрифта. HFONT ScaleFontCHDC hDC. int percent) { LOGFONT If; Get0bject(6etCurrent0bject(hDC, OBJJONT). sizeof(lf), & If); TEXTMETRIC tm; GetTextMetrics(hDC, & tm); lf.lfWidth = (tm.tmAveCharWidth * percent + 50)/100; return CreateFontlndirectC&lf); } В правой части рис. 15.23 приведен текст, оформленный шрифтами, ширина которых составляет от 25 до 125 % от нормальной. В совместимом графическом режиме настройка анизотропной логической системы координат не обеспечивает масштабирования шрифтов — вам придется самостоятельно создать шрифт с нужным аспектным отношением. В расширенном графическом режиме вывод текста подчиняется настройке логической системы координат, как и все остальные графические элементы. При точном форматировании текста вычисление ширины может сопровождаться ошибками округления; для получения более точных данных воспользуйтесь вещественными метриками (см. раздел «Форматирование текста»). Работа с текстом в растровом формате Средства вывода текста в GDI подчиняются ограничениям, затрудняющим реализацию некоторых эффектов на «чисто текстовом» уровне. Например, если задать в совместимом графическом режиме логическую систему координат с противоположным направлением осей, все линии и растры выводятся справа налево и снизу вверх, но текстовые строки все равно будут выводиться сверху вниз и слева направо. Это весьма неприятно, особенно если вы хотите реализовать эффект зеркального отражения фрагмента с текстом. Существуют и другие ограничения — текст нельзя закрашивать кистью (см. предыдущий раздел), к нему нельзя применять растровые операции и альфа-наложение. Подобные проблемы решаются преобразованием текста в растровое изображение и выполнением операций на уровне растров. Текст преобразуется в растры двумя способами. Первый способ — получение растров отдельных глифов функцией GetGlyphOutline и имитация вывода текста посредством вывода растров. Второй способ — преобразование всей строки в один растр. Вывод текста с использованием растров глифов Класс KGlyph, разработанный в этой главе, позволяет легко написать функцию вывода текстовой строки по растрам глифов. Ниже приведена функция Bitmap- TextOutROP, выводящая растры глифов с применением тернарной растровой операции. На компакт-диске имеется упрощенная версия BitmapTextOut, которая выводит растры глифов в прозрачном режиме при помощи метода Kglyph: :DrawGlyph. BOOL BitmapTextOutROP(HDC hDC, int x. int y, const TCHAR * str, int count, DWORD гор)
Эффекты при выводе текста 883 { if ( count<0 ) count = Jxslen(str); KGlyph glyph; COLORREF crBack = GetBkColor(hDC); COLORREF crFore = GetTextColor(hDC); while ( count>0 ) { if ( glyph.GetGlyph(hDC. * str. GGO_BITMAP)>0 ) glyph.DrawGlyphROP(hDC, x + glyph.m_metrics.gmptGlyphOrigin.x. у - glyph.m_metrics.gmptGlyphOrigin.y, гор. crBack, crFore); x += glyph.mjnetrics.gmCelllncX: У +* glyph.m_metrics.gmCellIncY; str ++; count --; } return TRUE; } Функции BitmapTextOut и BitmapTextOutROP переводят задачу из текстовой области в область работы с растровыми изображениями. Тем самым решаются проблемы, связанные с зеркальным отражением в логической системе координат, и появляется возможность применения растровых операций. Впрочем, при использовании растровых операций при выводе растров глифов необходимо действовать осторожно, поскольку при выводе строки фоновые пикселы разных глифов могут перекрываться. Функция BitmapTextOutROP выбирает цвет, на который должны отображаться фоновые пикселы, в зависимости от цвета фона контекста устройства. Например, если вы хотите использовать операцию SRCAND, выберите белый цвет фона (RGB(OxFF.OxFF.OxFF)); в этом случае фоновые пикселы не будут влиять на вывод. Следующий фрагмент убеждает в том, что средства GDI не позволяют выполнить зеркальное отражение текста, а также иллюстрирует методику зеркального отражения текста с использованием функции BitmapTextOut и применение растровых операций при выводе текста. // Зеркальное отражение текста { SaveDC(hDC); SetMapMode(hDC. MM_ANIS0TR0PIC); SetWindowExtEx(hDC. 1.1, NULL); SetViewportExtEx(hDC. -1. -1. NULL); SetViewportOrgEx(hDC, 300. 100. NULL); ShowAxes(hDC. 600. 180); int x= 0. у = 0; const TCHAR * mess = "Reflection":
884 Глава 15. Текст SetTextAligrUhDC, TA_LEFT | TA_BASELINE); SetTextColorChDC, RGB(0. 0. OxFF)); // Синий (темный) BitmapTextOut(hDC. x, y. mess. Jxslen(mess). GG0_GRAY4_BITMAP); SetTextColor(hDC. RGB(0xFF. OxFF. 0)); // Желтый (светлый) TextOut(hDC. x, y. mess. _tcslen(mess)); RestoreDC(hDC. -1); // Растровые операции при выводе текста int x = 10; int у = 300; KLogFont 1f(-PointSizetoLogica1(hDC, 48). "Times New Roman"); If.mjf. If Weight = FW_B0LD; lf.m_lf.lfQuality= ANTIALIASED_QUALITY; KGDIObject font (hDC, If .CreateFontO); const TCHAR * mess = "Raster"; SetBkColor(hDC, RGB(0xFF. OxFF. OxFF)); SetTextColor(hDC. RGB(0xFF. 0. 0)); BitmapTextOutROP(hDC. x. y. mess. _tcslen(mess). SRCAND); SetBkColor(hDC. RGB(0. 0. 0)); SetTextColor(hDC. RGB(0. OxFF. 0)); BitmapTextOutROP(hDC. x+5. y+5. mess. _tcslen(mess). SRCINVERT); Фрагмент начинается с настройки анизотропного режима отображения, в котором ось х направлена справа налево, а ось у — снизу вверх. Сначала мы выводим синий (темный) текст функцией BitmapTextOut, а затем та же строка выводится в желтом (светлом) цвете функцией TextOut GDI. Строки выводятся в одинаковых логических координатах, но как видно из рис. 15.24, на экране они появляются в разных местах и с разной ориентацией. .лот?., чет. 49** 4*PP г*чл^ ч№* ^ЯЯЯР^ Л». I иорээфщ * ^шлиш_ #11 г-ж **w if ж i i *ш гж % * Soft-Shadow Рис. 15.24. Эффекты с использованием растровых изображений глифов fuzzy
Эффекты при выводе текста 885 Код второй половины фрагмента выводит красную строку, используя растровую операцию SRCAND, а затем повторяет тот же текст в зеленом цвете со смещением (5,5) и растровой операцией SRCINVERT. Чтобы фоновые пикселы не влияли на вывод, при операции SRCAND используется белый цвет фона, а при операции SRCINVERT — черный цвет фона. Преобразование текста в растр Если приложение хочет обработать растровые данные перед выводом в контексте устройства или сохранить их для последующего использования, вместо операций с отдельными глифами лучше преобразовать в растровый формат сразу всю строку. К полученному растру можно применить всевозможные графические алгоритмы — например, описанные в главах 10-13 этой книги. В листинге 15.9 приведено объявление класса KTextBitmap, преобразующего текстовую строку в DIB-секцию, с простым фильтром размытия. В классе KTextBitmap объединяются различные приемы работы с растровыми изображениями и текстом, рассмотренные в книге. Полная реализация имеется на прилагаемом компакт-диске. Листинг 15.9. Класс KTextBitmap: применение растровых эффектов к тексту // Преобразование текстовой строки в растровое изображение class KTextBitmap { public: HBITMAP HDC HGDIOBJ int int int int BYTE * m hBitmap; m hMemDC; mJiOldBmp: m width; mjieight; m dx; m dy; m_pBits; BOOL Convert(HDC hDC. LPCTSTR pString, int nCount, int extra); void ReleaseBitmap(void); KTextBitmapO; -KTextBitmapO; void Blur(void); BOOL Draw(HDC hDC, int x. int y, int rop=SRCC0PY); }: Вся основная работа выполняется классом KTextBitmap::Convert. Класс получает манипулятор контекста устройства, строку, количество символов и количество дополнительных пикселов, добавляемых с четырех краев сгенерированного растра. Дополнительное место используется при увеличении растра в результате применения графических алгоритмов. Функция вычисляет размеры растра по размерам фонового прямоугольника текста, создает 32-разрядную DIB-секцию, создает совместимый контекст устройства и копирует атрибуты из текущего контекста устройства в совместимый контекст. Местонахождение текста в растре регулируется в соответствии со значением метрики А первого символа, чтобы предотвратить возможное отсечение части глифа. После завершения под-
886 Глава 15. Текст готовки строка выводится в растре простым вызовом TextOut. Метод KTextBitmap: : Convert ограничивается простейшим преобразованием — он работает только с контекстами устройств в режиме отображения ММ_ТЕХТ. Чтобы продемонстрировать возможности по обработке текста уже после его преобразования к растровому формату, в класс KTextBitmap был добавлен метод Blur, который при помощи шаблона Average применяет усредняющий фильтр 3 х 3 к каналам RGB 32-разрядной DIB-секции. В нижней части рис. 15.24 иллюстрируется преобразование текста в растр и результат размытия. Слева находится исходная строка. Средняя строка прошла двукратную процедуру размытия, а правая строка была обработана фильтром четыре раза. В нижней части рисунка показан эффект размытой, нечеткой тени, полученный при помощи класса KTextBitmap, — для этого растр проходит 8-кратную процедуру размытия. KTextBitmap bmp; SetBkMode(hDC, OPAQUE); SetBkColor(hDC. RGB(0xFFp OxFF. OxFF)); // Белый SetTextColor(hDC, RGB(0x80. 0x80. 0x80)); // Серый const TCHAR * mess = "Soft-Shadow"; bmp.Convert(hDC. mess, _tcslen(mess), 7); for (int i=0; i<8; i++) bmp.BlurO; bmp.Draw(hDC, x, y); SetBkMode(hDC. TRANSPARENT); SetTextColor(hDC. RGB(0. 0. OxFF)); // Синий TextOut(hDC. x-5, y-5. mess, _tcslen(mess)); После того как KBitmapText сгенерирует DIB-секцию, текстовая задача переходит в чисто растровую область. К полученному растру можно применить графические алгоритмы, описанные в главе 12, создать альфа-каналы и даже вывести на поверхность DirectDraw. Эффекты рельефа на фоновых растрах Как упоминалось при описании эффектов рельефа в этом разделе, в стандартных операциях вывода текста применяется только однородный цвет. При выводе текста на фоновом растре часть изображения будет неизбежно закрыта. В таких случаях часто используются эффекты рельефа (приподнятый и утопленный) — на рисунке выделяются только светлые и темные края символов, а их внутренняя область остается заполненной фоном рисунка. Используя класс KTextBitmap в сочетании с растровыми операциями, можно сгенерировать изображение, состоящее только из пикселов светлых и темных краев, и вывести его в контексте устройства. В листинге 15.10 приведена функция TransparentEmboss, предназначенная для создания как приподнятого, так и утопленного рельефа.
Эффекты при выводе текста 887 Листинг 15.10. Создание приподнятого и утопленного рельефа void TransparentEmboss(HDC hDC, const TCHAR * pString. int nCount, COLORREF crTL, COLORREF crBR. int offset, int x. int y) { KTextBitmap bmp; // Сгенерировать маску с левым верхним и правым нижнем краем SetBkModeChDC. OPAQUE); SetBkColor(hDC. RGB(OxFF. OxFF. OxFF)); // Белый фон SetTextColor(hDC, RGB(0, 0. 0)); bmp.Convert(hDC, pString, nCount, offset*2); // Черный край // (левый верхний) SetBkModeChDC. TRANSPARENT); bmp.RenderText(hDC, offset*2. offset*2. pString, nCount); // Черный край // (правый нижний) SetTextColor(hDC. RGBCOxFF. OxFF, OxFF)); // Белый основной текст bmp.RenderText(hDC. offset, offset. pString, nCount); // Применить маску с левым верхним и правым нижним краем bmp.Draw(hDC. x, у, SRCAND); // Создать цветной растр с левым верхним и правым нижним краем SetBkColorChDC. RGB(0, 0. 0)); // Черный фон SetTextColorChOC. crTL); bmp.Convert(hDC, pString. nCount. offset); // Левый верхний край SetBkMode(hDC. TRANSPARENT); SetTextColor(hDC. crBR); bmp.RenderText(hDC. offset*2. offset*2. pString. nCount); // Правый // нижний край SetTextColor(hDC. RGB(0. 0. 0)); bmp.RenderText(hDC, offset, offset, pString, nCount); // Черный // основной текст // Вывести цветные края (левый верхний и правый нижний) bmp.Draw(hDC. x, у, SRCPAINT); } Функция TransparentEmboss работает по тому же принципу, который используется при выводе курсоров мыши и значков. Обычно при создании рельефного оформления текстовая строка выводится трижды — сначала в позиции (х - dx, у - dy) цветом левого верхнего края, затем в позиции (х + dx,y + dy) цветом правого нижнего края и, наконец, в позиции (х,у) основным цветом текста. При создании прозрачного рельефа достаточно вывести только два края, левый верхний и правый нижний, не перекрывающиеся с основным текстом. Сначала функция строит маску из черных пикселов на белом фоне. Маска выводится растровой операцией SRCAND на основном изображении и удаляет из него пикселы, расположенные на краях. Затем на черном фоне строится другая маска, в которой пикселы краев окрашены в соответствующие цвета, обеспечивающие создание эффекта рельефа. Маска объединяется с основным изображением растровой операцией SRCPAINT. На рис. 15.25 показан результат применения функции TransparentEmbossing.
888 Глава 15. Текст Рис. 15.25. Создание прозрачного рельефа (приподнятого и утопленного) Текст как совокупность кривых Некоторые задачи, связанные с выводом текста, не удается нормально решить ни в текстовом, ни в растровом формате. К числу таких задач относится прорисовка контура глифа, применение не аффинных преобразований при выводе или имитация объема. Подобные задачи лучше всего решаются преобразованием текста в совокупность отрезков и кривых. Существует три способа преобразования текстовой строки в отрезки и кривые. Первый способ — непосредственная работа с данными шрифта TrueType — рассматривался в главе 14. Второй способ — получение контуров глифов функцией GetGlyphOutline — описан в разделе «Нетривиальный вывод текста». Оба способа дают чрезвычайно точные описания контуров, к которым легко применить преобразования или специальные эффекты, не беспокоясь о потере точности. Применение траекторий GDI при выводе текста Третий способ преобразования текстовой строки в совокупность отрезков и кривых гораздо проще — выводимый текст преобразуется в объект траектории GDI. Если заключить функцию вывода текста TrueType/OpenType между функциями BeginPath/EndPath, то контуры глифов (с фоновым прямоугольником, если он присутствует) вместо вывода в контексте устройства будут включены в объект траектории GDI. Завершив построение траектории, приложение может воспользоваться функциями StrokePath, Fill Path и StrokeAndFillPath для прорисовки контуров и/или заполнения внутренней области глифов. Если данные траектории потребуются для преобразования, переведите внутреннее представление траектории в массив POINT с массивом флагов при помощи функции GetPath. Преобразованный контур можно вывести функцией PolyDraw.
Эффекты при выводе текста 889 Текст TrueType легко преобразуется в траекторию, поскольку эта возможность поддерживается на уровне GDI. Приведенный ниже простой пример ограничивается простым выводом контура. BeginPath(hDC); // Начать построение траектории TextOutChDC. x. у, mess. _tcslen(mess)); // Преобразовать один вызов EndPath(hDC); StrokePath(hDC); // Прорисовать контур текста текущим пером // (по умолчанию - черным) Преобразование текста в совокупность отрезков и кривых, представленную траекторией GDI, дает возможность для применения множества специальных эффектов. На рис. 15.26 представлены девять разных вариантов вывода текста. В примере 1 использована функция StrokePath со стандартным черным пером, хорошо подходящим для прорисовки контуров. В примере 2 использована функция Fill Path; результат очень похож на обычный текст, хотя и с некоторой утратой точности и отсутствием сглаживания. В примере 3 функция StrokeAndFillPath прорисовывает контур и заполняет внутреннюю область символов. В примере 4 контур прорисован пунктирным геометрическим пером, вследствие чего очертания глифа состоят из точек среднего размера. Примеры 5 и 6 демонстрируют разные атрибуты геометрических перьев (закругленные и заостренные соединения). В примере 7 контур сначала обводится толстым черным пером, а затем по толстому контуру рисуется тонкая белая линия, создающая эффект двойной прорисовки. Рис. 15.26. Вывод текста, преобразованного в траекторию Примеры 8 и 9 очень похожи. В обоих случаях функция StrokePath рисует серию контуров, начиная темными и толстыми и завершая светлыми и тонкими. В результате возникает более реалистичный объемный эффект. В примере 9 поверх полученного рисунка выводится исходный текст, создавая иллюзию углубления.
890 Глава 15. Текст Преобразование данных траекторий И все же возможности работы с траекториями средствами GDI ограничены. Например, объект траектории создается в системе координат устройства; после того как он создан, смена отображения логической системы координат в систему координат устройства не влияет на вывод траектории. Вам не удастся сместить траекторию даже на один пиксел. К счастью, в GDI предусмотрена функция GetPath- Data для получения данных, определяющих траекторию. В главе 8 мы создали простой класс KPathData для работы с данными траектории. Один из методов этого класса, MapPoints, применял двумерное преобразование координат ко всем точкам траектории. Данные, полученные в результате преобразования, передаются функции PolyDraw GDI для вывода. Метод MapPoints применяет преобразование только к контрольным точкам траектории, что подходит только для аффинных преобразований, сохраняющих линии и кривые Бе- зье. В результате произвольного двумерного преобразования прямая может превратиться в кривую. Но если мы ограничиваемся преобразованием контрольных точек, то при соединении преобразованных точек все равно получится прямая, а не кривая. Для выполнения произвольных преобразований линии и кривые следует разделить на достаточно мелкие сегменты и добавлять дополнительные точки, обеспечивающие более точное воспроизведение формы преобразованной кривой. В листинге 15.11 приведено определение класса KTransCurve, выполняющего общие двумерные преобразования, а также новый метод KPathData:: Draw. Полная реализация находится на компакт-диске. Листинг 15.11. Родовой класс преобразования траектории class KTransCurve { int m_orgx; // Первая точка фигуры int m_orgy; float xO; // Текущая последняя точка float yO; float m_dstx; float m_dsty; int m_seglen; // Длина сегмента virtual MapCfloat x, float y, float & rx, float & ry); virtual BOOL DrvLineTo(HDC hDC. int x. int y); virtual BOOL DrvMoveTo(HDC hDC. int x, int y); virtual BOOL DrvBezierTo(HDC hDC, POINT p[]); BOOL BezierTo(HDC hDC. float xl. float yl. float x2. float y2. float x3. float y3): public: KTransCurveCint seglen); BOOL MoveTo(HDC hDC. int x. int y); BOOL BezierTo(HDC hDC. int xl. int yl. int x2. int y2. int x3. int y3):
Эффекты при выводе текста 891 BOOL CloseFigureCHDC hDC); BOOL LineTo(HDC hDC, int x3. int y3): J.BOOL KPathData::Draw(HDC hDC. KTransCurve & trans, bool bPath) { if ( m_nCount==0 ) return FALSE: if ( bPath ) BeginPath(hDC): for (int i=0; i<m_nCount: i++) { switch ( m_pFlag[i] & - PT_CLOSEFIGURE ) { case PT_M0VET0: trans.MoveTo(hDC. m_pPoint[i].x, m_pPoint[i].y); break: case PTJ.INET0: trans.LineTo(hDC. m_pPoint[i].x. m_pPoint[i].y); break: case PT_BEZIERTO: trans.BezierToChDC. m_pPoint[i ].x. m_pPoint[i ].y. m_pPoint[i+l].x. m_pPoint[i+l].y. m_pPoint[i+2].x. m_pPoint[i+2].y); i+=2; break; default: assert(false): } if ( m_pFlag[i] & PT_CLOSEFIGURE ) trans.CloseFigure(hDC): } if ( bPath ) EndPath(hDC): return TRUE: } Класс KTransCurve решает две задачи: преобразование отдельных точек виртуальным методом Map и вывод, который может изменяться переопределением трех графических примитивов. Метод KPathData: :Draw передает данные траектории, возвращаемые функцией GetPathData, экземпляру класса KTransCurve — как для преобразования, так и для вывода. Ниже приведен простой пример использования классов преобразования траекторий.
892 Глава 15. Текст class KWave : public KTransCurve { int m_dx, m_dy; public: KWave(int seg, int dx. int dy) : KTransCurve(seg) { m_dx = dx; m_dy = dy; } virtual MapCfloat x, float y. float & rx, float & ry) { rx = x + m_dx; ry * у + m_dy + (int) (sin(x/50.0) * 20); // Использование класса KWave для вывода преобразованного контура текста BeginPath(hDC); TextOut(hDC, х. у, mess, _tcslen(mess)); // Построить траекторию EndPath(hDC); KPathData pd; pd.GetPathData(hDC); // Запросить данные траектории StrokeAndFi11Path(hDC); // Вывести исходные данные { KWave wave(8, 360. 0); // Преобразование pd.Draw(hDC, wave, true); // Применить преобразование, // построить новую траекторию StrokeAndFiHPath(hDC); // Вывести новую траекторию } Класс KWave объявляется производным от KTransCurve, и в нем переопределяется ключевой метод Map. Конструктору передаются длины сегмента и смещения, прибавляемые к каждой точке. Метод Map прибавляет заданные смещения, а к вертикальной координате прибавляет дополнительную синусоидальную составляющую с периодом 50 пикселов. Данное преобразование не является аффинным, поскольку оно превращает горизонтальные линии в тригонометрические кривые. На рис. 15.27 показан эффект, созданный классом KWave, а также эффекты других классов, производных от KTransCurve. Исходный контур глифа изображен в левом верхнему углу рисунка. Справа от него показан результат применения синусоидального преобразования класса KWave. Обратите внимание: горизонтальные линии в буквах «V», «U» и «Е» преобразованы в кривые, а не в прямые. В левой нижней части рисунка к каждому кандидату на преобразование применяется небольшое случайное смещение (см. класс KRandom на компакт-диске). Правый нижний рисунок относится к теме следующего подраздела.
Эффекты при выводе текста 893 Рис. 15.27. Преобразование траекторий с использованием класса KTransCurve Трехмерный текст Итак, мы знаем, как получить контур текстовой строки, и у нас имеется класс для преобразования точек и разбиения кривой на сегменты. Все это позволяет легко создавать несложные объемные эффекты при выводе текста. Из всех типов трехмерных поверхностей проще всего создаются экстру зиои- ные поверхности. В общем случае экструзионная поверхность генерируется на основе плоской базовой кривой перемещением в трехмерном пространстве вдоль заданной траектории. Допустим, у вас имеется кривая на плоскости (х,у), расположенной в трехмерном пространстве (x,y,z) при z = 0. Кривая перемещается вдоль оси z к плоскости z = -10. В результате перемещения создается экструзионная поверхность, образованная всеми точками промежуточных кривых в процессе движения. В классе KTransCurve вывод прямых и кривых в конечном счете сводится к функции DrvLineTo, обеспечивающей вывод отдельного отрезка. Результат перемещения отрезка, заданного точками (х0,у0) и (х^), вдоль оси z на расстояние depth представляет собой прямоугольник, определяемый четырьмя углами: (xl.yl.O). (x2.y2.0). (x2.y2.depth), (xl.yl.depth) Экструзионная поверхность представляет собой совокупность всех таких прямоугольников в трехмерном пространстве; нам остается лишь отобразить их на плоскость. Если наблюдатель находится в точке (ex,ey,ez), для отображения точки (px,py,pz) из трехмерного пространства в двумерное можно воспользоваться проекцией перспективы. В листинге 15.12 приведена частичная реализация класса KExtrude, обеспечивающего применение проекции перспективы и простейший вывод экструзион- ных поверхностей. Полный код находится на компакт-диске. Листинг 15.12. Создание экструзионных поверхностей и объемных эффектов при выводе текста class KExtrude : public KTransCurve { int m_dx, m_dy; int m_xO. m_yO; Продолжение #
894 Глава 15. Текст Листинг 15.12. Продолжение int m_depth; int m_eye_x; int m_eye_y; int m_eye_z; public: KExtrude(int seglen. int dx. int dy, int depth, int ex. int ey, int ez) : KTransCurve(seglen); virtual Map(float x, float y, float & rx. float & ry); virtual BOOL DrvBezierTo(HDC hDC, POINT p[]); virtual BOOL DrvMoveToCHDC hDC. int x. int y); void Map3D(long & x. long & y. int z) { x = ( m_eye_z * x - m_eye_x * z) / ( m_eye_z - z); у = ( m_eye_z * у - m_eye_y * z) / ( m_eye_z - z); vi ( rtual BOOL DrvLineTo(HDC hDC. int POINT p[5] « Map3D(p[0].x. Map3D(p[l].x. Map3D(p[2].x. Map3D(p[3].x. Map3D(p[4].x. m_xO = xl; m_yO = yl; { m_xO. mxO.fr p[0].y. p[l] p[2] p[3] p[4] y. y. У. y. return Polygon(hDC P m yO. xl. У0 }: 0); 0); m_depth); m depth); 0); . 4); xl. yi. int yl) xl. yi m_x0. m_y0. Метод KExtrude: :Ma3D выполняет перспективную проекцию из точки наблюдения с координатами (т_еуе_х,т_еуе_у,т_еУе_2)- Метод KExtrude: :DrvLineTo рисует отдельные прямоугольники, образующие поверхность, после отображения трехмерных точек на плоскость. Обратите внимание: класс KExtrude не создает нового объекта траектории; каждый прямоугольник выводится отдельным вызовом Polygon. Дело в том, что режимы заполнения многоугольников GDI не справляются с некоторыми видами экструзионных поверхностей. Как показано на рис. 15.27, класс KExtrude позволяет создавать простейшие объемные эффекты, однако его упрощенная реализация не поддерживает отсечения невидимых поверхностей и вычисления освещенности — для этого лучше воспользоваться каким-нибудь профессиональным пакетом. В книге эта тема не рассматривается. Текст как регион Преобразование текста в траекторию открывает и другие возможности — к вашим услугам богатый набор функций GDI для работы с регионами. Замкнутую
Итоги 895 траекторию в контексте устройства можно преобразовать в объект региона (функция PathToRegion) или воспользоваться ей для отсечения (функция SetClipPath): HRGN PathToRegionCHDC hDC); BOOL SelectClipPath(HDC hDC, int iMode); Функция PathToRegion преобразует текущую траекторию в объект региона, заданный в системе координат устройства. Полученный регион может использоваться для отсечения, проверки принадлежности или для других целей. Второй параметр iMode управляет режимом объединения региона, полученного в результате преобразования, с текущим регионом отсечения. Например, флаг RGNAND указывает на то, что за новый регион отсечения следует принять пересечение траектории с текущим регионом отсечения. Преобразование текста в регион через траекторию упрощает некоторые операции. Например, в простейшем способе заполнения текста растровым изображением строка преобразуется в регион, после чего строка выводится с назначенным регионом отсечения. Ниже приведено другое решение, которое обходится без раздражающего мерцания. BOOL BitmapText2(HDC hDC, int x, int у. LPCTSTR pString. int nCount, HBITMAP hBmp) { RECT rect; GetOpaqueBox(hDC. pString, nCount. & rect, x. y); HDC hMemDC = CreateCompatibleDC(hDC); HGDIOBJ hOld - SelectObjectChMemDC, hBmp); BeginPath(hDC): SetBkModeChDC, TRANSPARENT); TextOut(hDC. x, y, pString, nCount); // Создать траекторию EndPath(hDC); SelectClipPath(hDC. RGN_C0PY); // Преобразовать траекторию в регион BOOL rslt - BitBlt(hDC, rect.left, rect.top, rect.right-rect.left, rect.bottom - rect.top, hMemDC, 0, 0, SRCCOPY); SelectObjectChMemDC, hOld); DeleteObject(hMemDC); return rslt; } Итоги Глава, посвященная выводу текста в Win32 GDI, подошла к концу. Мы подробно рассмотрели множество тем, от создания логических шрифтов, получения метрических данных и простейшего вывода до нетривиального форматирования и применения всевозможных эффектов.
896 Глава 15. Текст В этой главе было убедительно показано, что ограниченные возможности WYSIWYG-форматировании текста в GDI обусловлены целочисленными метриками, используемыми в работе GDI. Чтобы преодолеть эти ограничения, приложение может запросить точные значения метрик по эталонному шрифту, размер которого совпадает с размером em-квадрата. Точные значения метрик обеспечивают форматирование текста, не зависящее от разрешения графических устройств и текущего масштаба. Для создания специальных эффектов текст обычно преобразовывается в растр или в контур. В этой главе были разработаны некоторые классы для решения стандартных задач — получения растров и контуров отдельных глифов средствами GDI, преобразования целых строк в растры и объекты траекторий GDI. Мы рассмотрели родовой класс для применения к траекториям не аффинных преобразований; этот класс даже позволяет создавать простые трехмерные эффекты. С этой главой завершается наше знакомство со всеми основными группами графических примитивов GDI — пикселами, линиями и кривыми, растрами и текстом. В следующей главе мы поговорим о том, как сохранить команды GDI в стандартных и расширенных метафайлах, как происходит обработка и воспроизведение метафайлов. В главе 17 мы вернемся к теме аппаратно-независимого форматирования текста в контексте печати. Глава 18 посвящена интерфейсу DirectDraw. Пример программы К этой главе прилагается демонстрационная программа Text (табл. 15.5). Впрочем, главное — это даже не программа, а набор родовых функций и классов, созданных в этой главе. Таблица 15.5. Программа главы 15 Каталог проекта Описание Samples\Chapt_15\Text В меню File содержатся команды для вызова расширенного диалогового окна выбора шрифта, демонстрации системы подстановки шрифтов PANOSE, диалоговых окон для анализа структуры TEXTMETRIC и — самое важное — демонстрационных окон. Выбрав команду File ► Demo, вы получаете доступ к 20 с лишним окнам, наглядно демонстрирующим множество тем — от использования стандартных шрифтов до создания специальных эффектов посредством преобразования текста в кривые
Глава 16 Метафайлы Приложения часто обмениваются друг с другом графическими данными, для чего графические данные требуется сохранять в файлах. Формат BMP, широко используемый в Windows, подходит лишь для обмена растровыми данными. Для поддержки как растровых, так и векторных графических данных используются специальные графические форматы — 16-разрядные метафайлы Windows и 32- разрядные расширенные метафайлы Windows. Эти форматы широко применяются в библиотеках графических заготовок, при работе с буфером обмена (clipboard), при обмене данных между сервером и клиентом OLE, а также в работе спулера. Настоящая глава посвящена двум форматам метафайлов Windows, различным способам их создания, использования и расшифровки. Общие сведения о метафайлах У любого графического приложения есть свои сильные и слабые стороны. Но когда опытный пользователь правильно подходит к работе, каждое приложение вносит свой вклад в достижение конечного результата. Например, в CorelDraw можно создать векторный рисунок со сложными специальными эффектами, в PhotoShop — отретушировать фотографическое изображение, в Visio — построить диаграмму или блок-схему, а в Word — отредактировать текст. Когда эти приложения работают вместе, им необходим некий общий формат, в котором они могли бы обмениваться графическими данными. Формат BMP годится лишь для растров и фотографий, и то не для всех — он плохо подходит для фотографий высокого разрешения из-за отсутствия качественного сжатия. Универсальный формат обмена графическими данными должен поддерживать все основные графические элементы, в том числе пикселы, линии, кривые, замкнутые фигуры, текст и растры. Формат метафайлов Windows (WMF) был разработан компанией Microsoft для 16-разрядных версий Windows. Метафайл представляет собой последова-
898 Глава 16. Метафайлы тельность записанных команд GDI из всех основных категорий графических примитивов 16-разрядного интерфейса GDI. Возможности метафайлов Windows в значительной мере ограничиваются их зависимостью от устройств и приложений (похожие проблемы существуют и для аппаратно-зависимых растров). Метафайл Windows не располагает информацией о размере изображения, исходном разрешении или состоянии палитры устройства, на котором он записывался. При воспроизведении метафайла на другом устройстве с другим набором цветов и разрешением приложение не сможет масштабировать изображение до нужных размеров и обеспечить правильную цветопередачу. Существуют и другие ограничения, накладываемые GDI. В 32-разрядных версия Windows, начиная с Windows NT 3.51, компания Microsoft использует новый 32-разрядный формат метафайлов, называемый расширенным форматом метафайлов (Enhanced Metafile, EMF). По сравнению с WMF этот формат поддерживает 32-разрядную систему координат и новые 32-разрядные функции GDI, содержит заголовок с геометрическими данными и палитрой и даже обеспечивает некоторую поддержку OpenGL. Хотя WMF продолжает широко использоваться в библиотеках графических заготовок, формат EMF, в меньшей степени зависящий от устройства и поддерживающий новые функции GDI, завоевывает все большую популярность. В этой главе основное внимание уделяется формату EMF, хотя иногда упоминается и WMF. Существует две взаимосвязанных концепции метафайла: метафайл как объект GDI и метафайл как внешний формат файлов. В принципе можно провести аналогию с DIB-секциями как объектами GDI и растровыми файлами в формате BMP, хотя метафайл как объект GDI находится с физическим файлом в более «близких отношениях». Создание расширенного метафайла Расширенный метафайл является таким же объектом GDI, как, например, объект DIB-секции или объект траектории. Объект расширенного метафайла однозначно определяется своим манипулятором объекта GDI, относящимся к типу HENHMETAFILE. Функция GetObjectType возвращает для расширенного метафайла идентификатор типа OB JENHMETAFILE. Расширенный метафайл представляет собой последовательность 32-разрядных команд GDI. Следовательно, основным способом создания расширенных метафайлов является запись серии команд GDI. В GDI предусмотрены две функции, которые начинают и завершают запись расширенного метафайла. HDC CreateEnhMetaFileCHDC hdcRef, LPCTSTR lpFileName, CONST RECT * lpRect. LPCTSTR IpDescription); HENHMETAFILE CloseEnhMEtaFile(HDC hDC); Функция CreateEnhMetaFile создает специальный контекст устройства, используемый при записи расширенного метафайла. Она всего лишь готовит условия для создания расширенного метафайла — по аналогии с тем, как функция BeginPath начинает построение объекта траектории GDI. Первый параметр, hdcRef, ссылается на эталонный контекст устройства, данные которого потребуются при записи EMF. Если параметр hdcRef равен NULL, GDI принимает в качестве эталона
Общие сведения о метафайлах 899 текущий экран. Во втором параметре, IpFileName, может передаваться имя файла на диске или NULL. Если передается имя файла, после завершения записи файловый вариант метафайла сохраняется на диске; если передается NULL, метафайл создается в памяти. Третий параметр определяет размеры расширенного метафайла в единицах 0,01 мм. Заданный прямоугольник (кадр) сохраняется в расширенном метафайле и определяет начало координат и размеры области, в которой воспроизводится EMF. Если вместо прямоугольника передается NULL, GDI вычисляет ограничивающий прямоугольник по всем командам вывода; полученный прямоугольник может и не совпадать с тем, который подразумевался при создании метафайла. Следующая функция преобразует прямоугольник в логических координатах контекста устройства в физические единицы 0,01 мм. // Преобразовать прямоугольник из логических координат // в физические единицы 0.01 мм void MaplOum(HDC hDC, RECT & rect) { int widthmm = GetDeviceCaps(hDC, H0RZSIZE); int heightmm = GetDeviceCaps(hDC, VERTSIZE); int widthpixel = GetDeviceCaps(hDC, H0RZRES); int heightpixel= GetDeviceCaps(hDC. VERTRES); LPtoDP(hDC. (POINT *) & rect. 2); rect.left =( rect.left *widthmm *100+widthpixel/2) / widthpixel; rect.right = ( rect.right *widthmm *100+widthpixel/2) / widthpixel; rect.top = ( rect.top *heightmm*100+heightpixel/2) / heightpixel; rect.bottom = ( rect.bottom*heightmm*100+heightpixel/2) / heightpixel; } Обратите внимание на применение индексов H0RZSIZE, VERTSIZE, H0RZRES и VERTRES функции GetDeviceCaps для преобразования координат в физические единицы, используемые GDI для заполнения полей заголовочной структуры расширенного метафайла. Для экранных устройств разрешение (в пикселах на дюйм) обычно не совпадает с логическим разрешением, возвращаемым для индексов LOGPIXELX и LOGPIXELY. Например, значения LOGPIXELX и LOGPIXELY обычно равны 96 dpi для экранного режима с мелкими шрифтами, а значение H0RZRES всегда равно 320 мм; если H0RZSIZE = 1152 пиксела, то для создания EMF разрешение экрана равно 91,44 dpi. Последний параметр функции CreateEnhMetaFile содержит необязательное текстовое описание, сохраняемое в метафайле. Обычно описание состоит из имен приложения и документа, разделенных нуль-символом. Следовательно, если передается строка, отличная от NULL, ее следует завершить двумя нулями. Если вызов завершается успешно, CreateEnhMetaFile создает манипулятор контекста устройства для расширенного метафайла. Этот манипулятор передается всем функциям GDI, вызываемым в процессе записи. После завершения записи вызовите функцию CloseEnhMetafile, которая закрывает манипулятор контекста и возвращает манипулятор расширенного метафайла. Построение метафайла отчасти напоминает построение объекта траектории GDI. Впрочем, метафайл гораздо сложнее объекта траектории GDI, описываемого массивом точек и массивом флагов. По этой причине GDI использует для
900 Глава 16. Метафайлы построения метафайла специальный контекст устройства (тогда как траектория создается в обычном контексте, переведенном в режим построения траектории). Ниже приведен простой пример создания расширенного метафайла. Функция TestEMFGen принимает за эталонное устройство текущий экран и вычисляет размер кадра по размерам окна рабочего стола. После создания метафайлового контекста устройства в центре поверхности устройства выводится простой прямоугольник. Функция создает объект расширенного метафайла и сохраняет его в файле на диске. При воспроизведении метафайла в масштабе 1:1 в центре области 320 х 240 мм рисуется прямоугольник. HENHMETAFILE TestEMFGen(void) { RECT rect; HDC hdcRef = GetDC(NULL); GetClientRect(GetDesktopWindow(). &rect); MaplOum(hdcRef. rect); HDC hDC - CreateEnhMetaFileChdcRef, "c:\\test.emf\ & rect, MEMF.EXE\OTestEMF\On); ReleaseDC(NULL. hdcRef): if ( hDC ) { GetCIi entRect(GetDesktopWi ndow(). &rect); Rectangle(hDC. rect.right/3, rect.bottom/3. rect.right*2/3. rect.bottom*2/3); return CloseEnhMetaFile(hDC); } return NULL: } Воспроизведение расширенного метафайла Созданный метафайл воспроизводится в контексте устройства по своему манипулятору. Вы также можете открыть расширенный метафайл, хранящийся в файле на диске, и получить манипулятор расширенного метафайла. Ниже перечислены соответствующие функции. HENHMETAFILE GetEnhMetaFileCLPCTSTR IpszMetaFile); BOOL DeleteEnhMetaFileCHENHMETAFILE hemf); BOOL PlayEnhMetaFileCHDC hdc. HENHMETAFILE hemf. CONST RECT * lpRect); Функция GetEnhMetaFi le открывает файл с заданным именем, создает объект расширенного метафайла для работы с данными и возвращает его манипулятор. Аналогичный манипулятор возвращается и при завершении построения расширенного метафайла функцией CloseEnhMetaFile. После вызова CloseEnhMetaFile или GetEnhMetaFi le заданный файл считается используемым GDI и не может быть удален до удаления объекта функцией Del eteEnhMetaFi I e.
Общие сведения о метафайлах 901 Завершив работу с расширенным метафайлом, приложение должно вызвать функцию DeleteEnhMetaFile (по аналогии с функцией DeleteObject, вызываемой для других объектов GDI). Функция DeleteEnhMetaFile удаляет объект расширенного метафайла вместе со всеми ресурсами, выделенными для него GDI. После вызова функции физический файл на диске освобождается, но не удаляется. Чтобы удалить физический файл, следует вызвать функцию DeleteFile файловой системы. Если вместо имени файла при вызове CreateEnhMetaFile передается NULL, внешний файл удалять не нужно. Располагая манипулятором расширенного метафайла, можно воспользоваться функцией PlayEnhMetaFile для воспроизведения любой команды GDI этого метафайла в контексте устройства. Хотя прототип функции PlayEnhMetaFile выглядит просто, внутренний механизм ее работы чрезвычайно сложен. Функция PlayEnhMetaFile получает три параметра: манипулятор приемного контекста устройства, манипулятор исходного расширенного метафайла и прямоугольник, заданный в логических координатах приемного устройства. Прямоугольник соответствует кадру, указанному при создании метафайла функцией CreateEnhMetaFile. Следующий простой пример иллюстрирует связь между построением и воспроизведением EMF. void SampleDraw(HDC hDC, int x. int y) { Ellipse(hDC, x+25, y+25, x+75. y+75); SetTextAlign(hDC. TA_CENTER | TA_BASELINE); const TCHAR * mess = "Default Font"; TextOutChDC. x+50, y+50. "Default Font". _tcslen(mess)); } void DemoJMFScaleCHDC hDC) { // Построить EMF HDC hDCEMF - CreateEnhMetaFile(hDC. NULL. NULL. NULL); SampleDraw(hDCEMF. 0. 0); HENHMETAFILE hSample - CloseEnhMetaFile(hDCEMF); HBRUSH yellow - CreateSolidBrush(RGB(OxFF. OxFF. 0)); // Вывести команды, записанные в EMF { RECT rect = { 10. 10. 10+100. 10+100 }; FillRect(hDC. & rect. yellow); SampleDraw(hDC. 10. 10); // Оригинал } for (int test-0. x=120; test<3; test++) { RECT rect = { x. 10. x+(test/2+l)*100. 10+((test+1)/2+1)*100 };
902 Глава 16. Метафайлы FillRect(hDC. & rect. yellow); PlayEnhMetaFile(hDC. hSample, & rect); x = rect.right + 10; DeleteObject(yellow); DeleteEnhMetaFile(hSample); } Содержимое простейшего метафайла1 генерируется функцией SampleDraw. Предполагается, что эта функция рисует круг 50 х 50 единиц в центре квадрата 100 х 100 и выводит текстовую строку в центре круга шрифтом по умолчанию. Функция Demo_EMFSca1e управляет всем выводом. Сначала она создает EMF в памяти (то есть EMF без файла на диске) при помощи функции SampleDraw. Та же функция Sampl eDraw рисует то, что было сохранено в метафайле, в левом верхнем углу экрана — просто для сравнения. После этого функция в цикле воспроизводит EMF в трех прямоугольниках разных размеров (100 х 100, 100 х 200 и 200 х 200). Для наглядности соответствующие участки экрана закрашиваются желтым фоном. Результат показан на рис. 16.1. Default Font Default Font Default Font D Рис. 16.1. Воспроизведение расширенного метафайла с кадром по умолчанию Слева показан результат выполнения двух исходных команд рисования в EMF — круг с текстовой строкой в центре квадрата 100 х 100. На втором рисунке показан результат воспроизведения EMF в квадрате 100 х 100. Все отступы куда-то исчезли, а текст и круг масштабируются с искажением пропорций. Третий и четвертый рисунки выглядят примерно так же, хотя в них масштабирование выполняется по прямоугольникам других размеров. Все это произошло из-за того, что EMF был сгенерирован GDI без указания прямоугольника кадра; вернее, прямоугольник кадра был вычислен автоматически по ограничивающему прямоугольнику всех графических команд. В нашем примере ограничивающий прямоугольник определяется координатами {11, 25, 1 Здесь и далее под термином «метафайл» подразумевается расширенный метафайл, то есть EMF. — Примеч. перев.
Общие сведения о метафайлах 903 88, 74}, а прямоугольник кадра — {3,06, 6,94, 24,44, 20,56} (в миллиметрах). Обратите внимание: отношение ширины к высоте равно 1,56:1 вместо 1:1, а все отступы от краев были исключены из кадра. При воспроизведении EMF функцией PlayEnhMetaFile GDI отображает прямоугольник кадра на прямоугольник, переданный при вызове PlayEnhMetaFile. Правильная настройка прямоугольника кадра в этом примере выполняется следующим образом: RECT rect = { 0. 0. 100. 100 }; // Логический прямоугольник кадра Mapl0um(hDC, rect); // Перевести в единицы 0.01 мм hDCEMF = CreateEnhMetaFileChDC. NULL. &rect. NULL); // Создать с кадром На рис. 16.2 показаны результаты воспроизведения EMF с правильно заданным прямоугольником кадра. Default Font Default Font Default Font D e fa u It Font Рис. 16.2. Воспроизведение расширенного метафайла с правильно заданным кадром Как видно из рисунка, определение кадра 100 х 100 при построении EMF гарантирует, что при воспроизведении EMF в квадрате будет выдержан масштаб и основные пропорции графических элементов. Во всяком случае, круг на рисунке масштабируется превосходно. Впрочем, текстовая строка явно искажается, хотя каждый символ вроде бы расположен в правильной позиции; это связано с тем, что при построении EMF применялся шрифт, выбранный в контексте устройства по умолчанию. Подробности рассматриваются в разделе «Строение расширенных метафайлов». Получение информации о расширенном метафайле Вы убедились в том, что связь между кадром EMF и прямоугольником, указанным при вызове PlayEnhMetaFile, чрезвычайно важна для правильного размещения и масштабирования расширенного метафайла. Если вы не знаете, как строился метафайл, вам придется каким-то образом получить информацию о нем — например, данные прямоугольника кадра. При построении EMF GDI записывает в него заголовок с основными атрибутами метафайла. Для получения этой информации в GDI определяются специальные функции.
904 Глава 16. Метафайлы typedef struct tagENHMETAHEADER DWORD DWORD RECTL RECTL DWORD DWORD DWORD DWORD WORD WORD DWORD DWORD DWORD SIZEL SIZEL DWORD DWORD DWORD SIZEL iType; nSize; rclBounds; rclFrame; dSignature; nVersion; nBytes; nRecords; nHandles; sReserved; nDescription; offDescription; nPalEntries; szlDevice; szlMillimeters; cbPixelFormat; offPixel Format; bOpenGL; szlMicrometers; ENHMETAHEADER; // // // // // // // // // // // // // // // // // // EMRJEADER Размер в байтах, включая дополнение Границы в единицах устройства // Прямоугольник кадра в единицах 0.01 мм ENHMETAJIGNATURE // Номер версии Размер метафайла в байтах Количество записей в метафайле Количество манипуляторов в таблице Длина строки описания Смещение строки описания Количество элементов в палитре метафайла Размер эталонного устройства в пикселах Размер эталонного устройства в миллиметрах 4.0 Размер РIXELFORMATDESCRIPT0R 4.0 Смещение PIXELF0RMATDESCRIPT0R 4.0 Флаг наличия команд OpenGL 5.0 Размер эталонного устройства в микрометрах UINT GetEnhMetaFileHeaderCHENHMETAFILE hemf. UINT cbBuffer. LPENHMETAFILEHEADER); UINT GetEnhMetaFileDescription(HENHMETAFILE hemf. UINT cchBuffer. LPTSTR IpszDescription); Типичный метафайл всегда начинается со структуры ENHMETAHEADER. Первые два поля ENHMETAHEADER соответствуют общей структуре формата EMF, в которой каждая запись должна начинаться с 32-разрядного идентификатора типа записи и 32-разрядного размера. С каждым типом записи EMF связывается уникальный идентификатор типа в интервале от EMRMIN до EMRMAX. В настоящее время EMR_MIN = 1, a EMR_MAX = 122. В EMF постоянно добавляются новые типы записей для новых возможностей GDI. Например, идентификатор EMRALPHABLEND = 114, соответствующий функции GDI AlphaBlend, не поддерживался в системах, предшествующих Windows 98 и Windows 2000. В поле nSize хранится общее количество байтов в записи EMF, включающее iType, nSize и другие открытые поля вместе с возможными дополнениями. Многие записи EMF имеют переменный размер, поэтому включение поля nSize позволяет GDI переходить от одной записи EMF к другой. Поле rcBounds содержит данные ограничивающего прямоугольника графических команд EMF в системе координат устройства. Для накопления данных ограничивающего прямоугольника в процессе вывода в контексте устройства применяется пара редко используемых функций GDI. Функция SetBoundsRect разрешает/запрещает накопление данных или присваивает/сбрасывает данные прямоугольника; функция GetBoundsRect возвращает накопленные данные. Разумеется, GDI сохраняет данные ограничивающего прямоугольника, накопленные в процессе построения EMF, в заголовке EMF. Используя данные ограничивающего прямоугольника, приложение может удалить белую «рамку» вокруг воспроизведенного метафайла.
Общие сведения о метафайлах 905 Поле rcl Frame содержит прямоугольник кадра, заданный приложением при вызове CreateEnhMetaFile. Если передается значение NULL, GDI автоматически вычисляет его по ограничивающему прямоугольнику. Прямоугольник кадра хранится в единицах 0,01 мм, что эквивалентно устройству с разрешением 2540 dpi. Используя данные кадра, приложение масштабирует EMF по предполагаемым физическим размерам при воспроизведении на другом устройстве. После прямоугольника кадра в заголовке расположены служебные данные. Поле dSignature должно содержать уникальную сигнатуру расширенного метафайла, 0x464d4520 в шестнадцатеричной записи или «EMF» в символьном формате. Поле nVersion содержит используемую версию EMF. Хотя существует несколько второстепенных версий EMF, эксперименты показывают, что поле nVersion всегда равно 0x10000. Например, не все метафайлы содержат три поля, относящихся к внедрению данных OpenGL, а последнее поле szMicrometers появилось только в Windows 98 и 2000. Несмотря на это, во всех расширенных метафайлах используются одинаковые номера версий. Поле nBytes содержит общую длину EMF в байтах. Количество записей в EMF вместе с заголовком, всеми командами GDI и завершающей записью хранится в поле nRecords. Манипуляторы объектов GDI интерпретируются в EMF особым образом. Вспомните, о чем говорилось в главе 3 — манипуляторы относятся к временным объектам GDI и не имеют смысла вне адресного пространства процесса, тем более на другом компьютере в неизвестный момент времени. При записи команд GDI, использующих манипуляторы объектов (например, SelectObject), манипуляторы заменяются индексами внутренней таблицы, существующей только во время воспроизведения EMF. При воспроизведении GDI создает таблицу, в которой хранятся реально используемые манипуляторы, и берет на себя преобразование индексов EMF в манипуляторы GDI. Для стандартных объектов GDI предусмотрено особое обозначение, поэтому в таблице достаточно хранить только манипуляторы нестандартных объектов. При удалении объекта GDI в EMF соответствующий элемент таблицы освобождается и может использоваться заново. Поле nHandles заголовка EMF определяет размер таблицы объектов GDI, которую необходимо создать для воспроизведения метафайла. Следовательно, это поле отражает не общее количество манипуляторов объектов GDI, используемых в EMF, а лишь максимальное количество открытых нестандартных объектов, открытых в EMF в произвольный момент времени. Первый элемент таблицы зарезервирован для хранения стандартного объекта белой кисти, поэтому минимальное значение nHandles равно 1. Следующие два поля относятся к текстовому описанию, передаваемому при вызове CreateEnhMetaFile, которое всегда хранится в EMF в кодировке Unicode. Если строка описания была задана, она хранится после открытых полей заголовка. Поле nDescription содержит количество символов, а поле offDescription — относительное смещение строки от начала заголовка. Если строка описания не передавалась, оба поля равны нулю. Как говорилось выше, существуют по крайней мере три версии структуры ENHMETAHEADER; если поле offDescription отлично от нуля, по нему можно судить о различиях между версиями. Самая старая версия не содержит полей для внедрения данных OpenGL; в ней поле offDescription
906 Глава 16. Метафайлы равно 88. Последняя версия, используемая в Windows 2000, поддерживает OpenGL и поле szMicrometers; в ней поле offDescription равно 108. Поле nPalEntries задает количество элементов в накапливаемой палитре, используемой в метафайле. Цветовая таблица хранится не в заголовке, а в последней записи EMF, поскольку при построении EMF размер таблицы еще не известен GDI. Если палитра в EMF не используется, это поле равно нулю. Следующие два поля содержат информацию об эталонном устройстве. Поле szlDevice определяет размеры поверхности устройства в пикселах, а поле szMil- "limeters — в миллиметрах. Для получения информации о драйвере устройства GDI вызывает функцию GetDeviceCaps с индексами H0RZRES, VERTRES, H0RZSIZE и VERTSIZE. В метафайлах, записанных с использованием экранного контекста, типичные значения szlDevice равны 1024 х 768 или 1152 х 864. Поле szMillimeters всегда равно 320 х 240 (диагональ 400 мм = 15,75 дюйма, даже если у вас установлен 21-дюймовый монитор). Следующие три поля, cbPixelFormat, offPixelFormat и bOpenGL, обеспечивают поддержку OpenGL в формате метафайлов. Если контекст не является контекстом устройства OpenGL, все три поля равны нулю. Последнее поле szMicroMeters появилось только в структуре версии 5.0. В нем задается размер поверхности эталонного устройства в микрометрах, что для экрана равно 320 000 х 240 000. Непонятно, зачем понадобилось это поле, если размеры поверхности устройства в миллиметрах уже известны. Если вы знаете манипулятор EMF, для получения данных заголовка EMF можно воспользоваться функцией GetEnhMetaFileHeader. Из-за включения описания и данных о формате пикселов OpenGL заголовок является структурой переменного размера, поэтому функция GetEnhMetaFileHeader вызывается дважды; в первый раз она возвращает фактический размер, а во второй — запрашиваемые данные. Впрочем, если дополнения вас не интересуют, можно обойтись одним вызовом и получить фиксированные поля структуры. При непосредственном обращении к заголовку описание EMF всегда возвращается в виде строки в кодировке Unicode. Если вы не хотите возиться с Unicode в ANSI-программах, воспользуйтесь функцией GetEnhMetaFileDescription. Эта функция тоже вызывается дважды: сначала для получения количества символов, а потом для получения строки описания. Помните, что описание обычно состоит из названия компании и имени документа, разделенных нуль-символом, поэтому вся строка завершается двумя нуль-символами. При выводе произвольного расширенного метафайла важно сохранить те физические размеры, в которых он был создан. Следующая функция запрашивает информацию из заголовка EMF и вычисляет по ней размеры экранного прямоугольника для текущего контекста устройства. void GetEMFDimension(HDC hDC, HENHMETAFILE hEmf. int & width, int & height) { ENHMETAHEADER emfh[5]: GetEnhMetaFileHeader(hEmf. sizeof(emfh). emfh); // Размеры изображения в единицах 0,01 мм width - emfh[0].rclFrame.right - emfh[0].rclFrame.left; height = emfh[0].rclFrame.bottom - emfh[0].rclFrame.top:
Общие сведения о метафайлах 907 // Преобразовать к пикселам текущего устройства int t = GetDeviceCaps(hDC. HORZSIZE) * 100; width = ( width * GetDeviceCaps(hDC, HORZRES) + t/2 ) / t; t - GetDeviceCaps(hDC, VERTSIZE) * 100; height = ( height * GetDeviceCaps(hDC. VERTRES) + t/2 ) / t; RECT rect = { 0, 0. width, height }; // Преобразовать в логические координаты DPtoLPChDC. (POINT *) & rect. 2); width = abs(rect.right - rect.left); height = abs(rect.bottom - rect.top); } Функция GetEMFDimension получает манипулятор контекста устройства, в котором вы собираетесь воспроизвести EMF. Она запрашивает данные заголовка EMF функцией GetEnhMEtaFileHeader и вычисляет по ним размеры кадра. Ширина и высота кадра сначала преобразуются в систему координат устройства, а затем — в логическую систему координат. Результаты, полученные при вызове GetEMFDimension, позволяют воспроизвести EMF с исходными размерами или в заданном масштабе. Ниже приведена общая функция для вывода EMF в заданном масштабе (для исходных размеров оба масштабных коэффициента равны 100). B00L DisplayEMF(HDC hDC. HENHMETAFILE hEmf. int x. int y. int scalex, int scaley) { int width, height; GetEMFDimension(hDC. hEmf. width, height); RECT rect = { x. y. x + (width * scalex + 50)/100. у + (height * scaley + 50)/100 }; return rslt: } Передача расширенных метафайлов Чтобы передать данные другому приложению или сохранить их для последующего использования, EMF можно сохранить в файле на диске, загрузить из файла, скопировать в буфер, вставить из буфера или присоединить к исполняемому файлу в виде ресурса. Сохранение графики в файле EMF В одном из параметров функции CreateEnhMetaFile можно передать имя файла, в котором сохраняется построенный метафайл. Это позволяет легко сохранять графические операции GDI в EMF. У типичного окна имеется функция вывода, обеспечивающая вывод графики на экран. Чтобы поддержать сохранение EMF в файле, включите в программу код для вызова диалогового окна, в котором
908 Глава 16. Метафайлы пользователь вводит имя файла, задайте размеры прямоугольника кадра, создайте контекст устройства EMF, воспользуйтесь той же функцией вывода и закройте контекст устройства EMF. Следующий фрагмент показывает, как это делается. HDC QuerySaveEMFFile(const TCHAR * desp. const RECT * rcFrame. TCHAR szFileName[]) { KFileDialog fd; if ( fd.GetSaveFileName(NULL. "emf\ "Enhanced Metafiles") ) { if ( szFileName ) _tcscpy(szFileName, fd.m_TitleName); return CreateEnhMetaFileCNULL, fd.m_TitleName. rcFrame, description); } else return NULL; } int KMyView::OnCommand(int and. HWND hWnd) { if (cmd==IDM_FILE_SAVE) { hDC = QuerySaveEMFFileCEMF SampleVO". & rect. NULL); if ( hDC ) { OnDraw(hDC. NULL); // Функция вывода HENHMETAFILE hEmf - CloseEnhMetaFile(hDC); DeleteEnhMetaFile(hEmf); // Манипулятор не нужен } } } В этом фрагменте обрабатывается команда меню IDMFILESAVE. Обработчик вызывает функцию QuerySaveEMFFile, которая запрашивает у пользователя имя создаваемого файла и возвращает метафайловый контекст устройства. Затем вызывается метод OnDraw, выполняющий основной вывод в окне. Программа использует класс для работы с диалоговыми окнами, построенный в одной из предыдущих глав. Зная манипулятор расширенного метафайла, EMF можно сохранить на диске функцией CopyEnhMetaFile: HENHMETAFILE CopyEnhMetaFile(HENHMETAFILE hemfSrc. LPCTSTR IpszFile); Функция CopyEnhMetaFile копирует содержимое EMF в файл на диске, заданный параметром IpszFile, и возвращает манипулятор нового объекта EMF. Если вместо имени файла передается NULL, копия создается в памяти. После завершения работы с копией EMF объект GDI удаляется вызовом DeleteEnhMetaFile, но файл на диске остается до его удаления вызовом Del eteFi 1 е. Объект EMF создается на основе существующего EMF-файла простым вызовом GetEnhMetaFile (см. выше).
Общие сведения о метафайлах 909 Загрузка EMF из ресурса Если EMF присоединяется к исполняемому файлу в виде двоичного ресурса, то при помощи функций FindResource, LoadResource и LockResource можно получить указатель на изображение и создать по нему объект EMF вызовом функции SetEnhMetaFileBits. HENHMETAFILE SetEnhMetaFiIeBits(UINT cbBuffer. CONST BYTE * lpData): Функция SetEnhMetaFileBits получает два простых параметра — размер метафайла и указатель на метафайл, находящийся в памяти. Функции LoadBitmap и Loadlmage Win32 не позволяют загружать EMF из ресурсов, поэтому ниже приведена простая функция для решения этой задачи. // Загрузить EMF, присоединенный в виде ресурса, с типом данных RCDATA HENHMETAFILE LoadEMF(HMODULE hModule, LPCTSTR pName) { HRSRC hRsc = FindResource(hModule, pName. RT_RCDATA); if ( hRsc==NULL ) return NULL; HGLOBAL hResData = LoadResource(hModule. hRsc); LPVOID pEmf = LockResource(hResData); return SetEnhMetaFileBits(SizeofResource(hModule, hRsc). (const BYTE *) pEmf); } Ресурс EMF не принадлежит к числу стандартных типов ресурсов, однако для него можно указать тип ресурса RCDATA (идентификатор типа RT_RCDATA). Функция LoadEMF показывает, как загрузить ресурс EMF и преобразовать его в объект расширенного метафайла GDI. Вывод EMF в статическом элементе управления Объект EMF можно связать со статическим элементом управления, который затем автоматически выводится в диалоговом окне или странице свойств. У вас появляется возможность вывода векторной графики без применения элементов, прорисовка которых выполняется владельцем, и добавления специального кода графического вывода. По сравнению с растрами и значками, часто используемыми при выводе статических элементов управления и кнопок, EMF обеспечивает большую гибкость при масштабировании в разных разрешениях экрана. Чтобы объект EMF отображался в статическом элементе управления, включите в стиль последнего флаг SSENHMETAFILE или присвойте ему в редакторе ресурсов тип «Enhanced Metafile». В процессе инициализации родительского окна элемента управления отправьте ему сообщение STMSETIMAGE с манипулятором EMF. Следующий фрагмент показывает, как это делается в обработчике сообщений диалогового окна. switch (uMsg) { case WMJNITDIALOG: { hEmf = LoadEMF((HMODULE) GetWindowLong(hWnd, GWL HINSTANCE). MAKEINTRESOURCEdDR EMFD);
910 Глава 16. Метафайлы SendDlgltemMessageChWnd. IDCJMF. STM_SETIMAGE, IMAGEJNHMETAFILE, (LPARAM) hEmf): return TRUE; } case WMJICDESTROY: if ( hEmf ) DeleteEnhMetaFile(hEmf); return TRUE; } На рис. 16.3 показано диалоговое окно со статическим элементом управления, в котором выводится объект EMF, использованный в одном из примеров главы 15. Рис. 16.3. Использование ресурсов EMF в статических элементах Вывод EMF в статическом элементе управления открывает перед вами возможности, недоступные для обычных растров. При помощи EMF можно рисовать в статических элементах управления линии, кривые, фигуры и текст. Как видно из рисунка, благодаря EMF значительно упрощается применение прозрачности. Имеется и другое преимущество — в статическом элементе EMF автоматически масштабируется с правильными размерами при переходе из экранного режима с мелкими шрифтами в режим с крупными шрифтами, и наоборот. При выводе растров в статических элементах управления это вызывает массу проблем. Обмен данными через буфер На удивление простой и эффективный способ передачи графических данных в формате EMF основан на использовании буфера обмена (clipboard). Большинство графических приложений поддерживает старый формат метафайлов Windows; некоторые приложения поддерживают расширенные метафайлы. При копировании данных из графического приложения копия сохраняется в буфере в
Строение расширенных метафайлов 911 формате WMF или EMF. Операционная система автоматически преобразует данные WMF в формат EMF, поэтому клиентское приложение всегда должно запрашивать из буфера данные в формате EMF. Работать с буфером обмена просто и удобно. Ниже приведены две функции, предназначенные для копирования и вставки данных EMF. void CopyToClipboard(HWND hWnd. HENHMETAFILE hEmf) { if ( OpenClipboard(hWnd) ) { EmptyClipboardO; SetClipboardData(CF_ENHMETAFILE, hEmf); Closed ipboardO; } } HENHMETAFILE PasteFromClipboard(HWND hWnd) { HENHMETAFILE hEmf = NULL; if ( OpenClipboard(hWnd) ) { hEmf = (HENHMETAFILE) GetClipboardData(CFJNHMETAFILE); if ( hEmf ) hEmf - CopyEnhMetaFile(hEmf, NULL); Closed ipboardO; } return hEmf; } Функция CopyToClipboard копирует EMF в буфер. Она открывает буфер обмена функцией OpenClipboard, удаляет его текущее содержимое функцией Empty- Clipboard, копирует данные функцией SetClipboardData и освобождает буфер функцией Closed ipboard. Функция PasteFromClipboard имеет похожую структуру. Она получает манипулятор EMF из буфера при помощи функции GetClipboardData. Но поскольку владельцем этого манипулятора по-прежнему остается буфер, мы должны создать копию метафайла в памяти. Поддержка копирования и вставки данных EMF открывает много интересных возможностей. Вы можете вставлять в свои приложения диаграммы, построенные в Visio, векторные рисунки Word Art и другие объекты, а также копировать графические данные в буфер и вставлять их в документы Word, чтобы обойтись без сохранения экрана при фиксированном разрешении. Строение расширенных метафайлов В предыдущем разделе были описаны основные операции с метафайлами (создание, отображение, загрузка, сохранение и передача через буфер обмена). Для простых применений EMF этого вполне достаточно. Однако формат EMF игра-
912 Глава 16. Метафайлы ет в GDI очень важную роль, поэтому для того, чтобы досконально понимать метафайлы и в полной мере использовать их возможности, необходимо разобраться в их внутреннем устройстве. В этом разделе рассматривается формат расширенных метафайлов Windows, преобразование команд GDI в EMF, перечисление записей и динамическая модификация EMF. Записи EMF Расширенный метафайл представляет собой простую последовательность записей EMF с одинаковой общей структурой. Каждая запись EMF начинается с двух фиксированных 32-разрядных полей, за которыми следует переменная часть, определяемая типом записи. Структура EMR описывает фиксированные поля следующим образом: typedef struct tagEMR { DWORD iType; // Тип записи EMRJXX DWORD nSize; // Размер записи в байтах } EMR; Первое поле iType определяет тип записи EMF. Каждому типу записи присваивается символическое имя и числовое значение, определяемые в GDI макросами языка С. Ниже приведена небольшая часть этих макросов; полный список приведен в файле WINGDI.H. // Типы записей расширенного метафайла #define EMRJEADER 1 #define EMR_POLYBEZIER 2 #define EMR_P0LYG0N 3 #define EMR_RESERVED120 120 #define EMR_COLORMATCHTOTARGETW 121 #define EMR_CREATECOLORSPACEW 122 #define EMR_MIN 1 #define EMR_MAX 122 Анализ файла WINGDI.H показывает, что во всех новых версиях ОС появляются новые типы записей EMF. Используемые типы записей никогда не изменяются, а список лишь дополняется новыми типами. Например, в Windows NT 3 последним определенным типом записи (EMRMAX) является EMRPOLYTEXTOUTW, в Windows NT 4.0 список завершается типом EMRPIXELFORMAT (104), а в Windows 2000 он расширяется до EMRCREATEC0L0RSPACEW (122). Существуют даже недокументированные типы записей с именами вроде EMRRESERVED120; возможно, они используются спулером при печати. Все незарезервированные типы записей EMF документируются в MSDN, а в файле WINGDI.H для них определяются соответствующие структуры. Структуры отдельных типов можно рассматривать как производные от общей структуры EMR. Ниже приведен пример — структура EMRSETPIXELV для функции SetPixelV. typedef struct tagEMRSETPIXELV { EMR emr;
Строение расширенных метафайлов 913 POINTL ptlPixel: COLORREF crColor; } EMRSETPIXELV, *PEMRSETPIXELV; Запись EMF может содержать дополнения, не входящие в число общих полей, определенных в структуре записи. Например, вместе с записью EMF для функции вывода растров в качестве дополнений передается блок описания растра и массив пикселов. Каждое дополнение обычно описывается двумя полями — смещением данных от начала записи и длиной записи в байтах. Примером служит строка описания в структуре ENHMETAHEADER. Эта простая и универсальная структура значительно упрощает присоединение, чтение и обработку данных переменного размера. Второе поле структуры EMR содержит общий размер записи EMF в байтах, включая два фиксированных поля, остальные открытые поля и все дополнения. Записи EMF всегда выравниваются по границе двойного слова. Минимальный файл EMF состоит из двух записей — заголовка и завершающей записи. Заголовок ENHMETAHEADER был описан в предыдущем разделе; структура завершающей записи приведена ниже. typedef struct tagEMREOF { EMR emr; DWORD nPalEntries; // Количество элементов в палитре DWORD offPal Entries; // Смещение данных палитры DWORD nSizeLast; // Размер последней записи } EMREOF. *PEMR0EF; Запись EMREOF не только сообщает о завершении метафайла, но и содержит накопленные данные об элементах палитры, использованных в EMF. На первый взгляд может показаться странным, что дополнение с данными палитры вставляется после поля off Pal Entries и перед полем nSizeLast (но об этом речь пойдет ниже). Хотите посмотреть, как выглядит реальный метафайл? Ниже приведен двоичный дамп простого метафайла с единственной командой SetPixelV. 00 04 08 18 28 2с 30 34 38 ЗА ЗС 40 44 48 50 58 5С iType nSize rclBounds rclFrame dSignature nVersion nBytes nRecords nHandles sReversed nDescription offDescription nPalEntries szlDevice szlMillimeters cbPixelFormat offPixel Format 1 0x84 { 3, 5. { 0, 0, 1 EMF' 0x10000 OxAC 3 1 0 OxC 0x6C 0 { 0x500, { 0x140, 0 0 3. 5} 0x49BB, 0x2311 } 0x400 } OxFO }
914 Глава 16. Метафайлы 60 bOpenGL 0 64 szlMicrometers { 0х4Е200. 0хЗА980 6С Description L 'EMF Sample\0\0' // EMRSETPIXELV 84 iType 88 nSize 8C ptlPixel 94 crColor // EMREOF 98 iType 9C nSize АО nPalEntries A4 offPal Entries A8 nSizeLast OxF 0x14 { 3. 5 } RGB(0xFF. ( OxOE 0x14 0 0x10 0x14 ). 0) Когда метафайл сохраняется в файле на диске или присоединяется к программе в виде ресурса, он имеет в точности такую структуру — никаких скрытых или дополнительных данных. Классификация типов записей EMF Итак, EMF представляет собой записанную последовательность команд GDI, однако процесс преобразования команд GDI в записи EMF не документирован. В Windows 2000 библиотека GDI32.DLL экспортирует 543 функции. Даже если отбросить некоторые функции, предназначенные только для поддержки драйверов принтеров пользовательского режима, количество экспортируемых функций GDI все равно в 4 раза превышает количество типов записей EMF. Чтобы понять процесс отображения команд GDI в типы записей EMF, прежде всего следует учесть, что все записи EMF делятся на несколько основных категорий. В табл. 16.1 перечислены все 12 категорий. Таблица 16.1. Классификация типов записей EMF Категория Типы записей EMF Объекты GDI EMR_CREATEBRUSHINDIRECT, EMR_CREATEDIBPATTERNBRUSHPT, EMR_CREATEMONOBRUSH, EMR_CREATEPALETTE, EMR_CREATEPEN, EMRJXTCREATEFONTINDIRECTW, EMRJXTCREATEPEN, EMR_DELETEOBJECT, EMR_RESIZEPALETTE, EMRJETPALETTEENTRIES Контексты EMR_MODIFYWORLDTRANSFORM, EMR_REAPLIZEPALETTE, EMR_RESTOREDC, устройств EMR_SAVEDC, EMRJCALEVIEWPORTEXTEX, EMRJCALEWINDOWEXTEX, EMRJELECTOBJECT, EMRJELECTPALETTE, EMRJETARCDIRECTION, EMR_SETBKC0L0R, EMRJETBKMODE, EMR_SETBRUSHORGEX, EMRSETLAYOUT, EMR_SETMAPMODE, EMR_SETMAPPERFLAGS, EMRJETMITERLIMIT, EMR_SETPOLYFILLMODE, EMRJETR0P2, EMRJETSTRETCHBLTMODE, EMR_SETTEXTALIGN, EMRJETTEXTCOLOR, EMRJETVIEWPORTEXTEX, EMRJETVIEWPORTORGEX, EMRJETWINDOWEXTEX, EMRJETWINDOWORGEX, EMRJETWORLDTRANSFORM Отсечение EMRJXCLUDECLIPRECT, EMRJXTSELECTCLIPRGN, EMR_INTERSECTCLIPRECT, EMR OFFSETCLIPRGN, EMR SELECTCLIPPATH, EMRJETMETARGN
Строение расширенных метафайлов 915 Категория Типы записей EMF Траектории ICM Линии и кривые Замкнутые фигуры Растры Текст OpenGL Недокументированные Прочее EMR_ABORTPATH, EMRJEGINPATH, EMR_C LOSE FIGURE, EMRJNDPATH EMR_CREATECOLORSPACE, EMR_CREATECOLORSPACEW, EMR_COLORCORRECTPALETTE, EMR_COLORMATCHTOTARGETW, EMR_DELETECOLORSPACE, EMR_SETCOLORADJUSTMENT, EMR_SETCOLORSPACE, EMR_SETICMMODE, EMR_SETICMPROFILEA, EMRJETICMPROFILEW EMR_ANGLEARC, EMR_ARC, EMR_ARCT0, EMR_FLATTENPATH, EMR_LINETO, EMR_M0VET0EX, EMR_POLYBEZIER, EMR_P0LYBEZIER16, EMR_POLYBEZIERTO, EMR_P0LYBEZIERT016, EMR_POLYDRAW, EMR_P0LYDRAW16, EMR_POLYLINE, EMR_P0LYLINE16, EMR_P0LYLINET0, EMR_P0LYLINET016, EMR_POLYPOLYLINE, EMR_P0LYP0LYLINE16, EMR_STROKEPATH, EMR_WIDENPATH EMR_CH0RD, EMRJLLIPSE, EMR_FILLPATH, EMR_FILLRGN, EMR_FRAMERGN, EMR_GRADIENTFILL, EMR_INVERTRGN, EMR_PAINTRGN, EMR_PIE, EMR_P0LYG0N, EMR_P0LYG0N16, EMR_P0LYP0LYG0N, EMR_P0LYP0LYG0N16, EMR_RECTANGLE, EMR_ROUNDRECT, EMRJTROKEANDFILLPATH EMR_ALPHABLEND, EMR_BITBLT, EMRJXTFLOODFILL, EMR_MASKBLT, EMR_PLGBLT, EMR_SETDIBITSTODEVICE, EMRJETPIXELV, EMRJTRETCHBLT, EMRJTRETCHDIBITS, EMRJRANSPARENTBLT EMR EXTTEXTOUTA, EMRJXTTEXTOUTW, EMR_POLYTEXTOUTA, EMR_POLYTEXTOUTW EMR_GLSBOUNDEDRECORD, EMR_GLSRECORD, EMR_PIXELFORMAT EMR_RESERVED_105, EMR_RESERVED_106, EMR_RESERVED_107, EMR_RESERVED_108, EMR_RESERVED_109, EMR_RESERVED_110, EMR_RESERVED_119, EMR_RESERVED_120 EMR EOF, EMR GDICOMMENT, EMR HEADER Сравнивая приведенные в таблице типы записей с функциями Win32 API, можно понять, о чем следует помнить и какие решения следует принимать при разработке EMF. Некоторые из моих личных заметок приведены ниже. О В формате EMF хранятся только постоянные данные. В нем нет переменных выражений, прямых ссылок на манипуляторы GDI или каких-либо зависимостей от результатов вызова функций. О Все вычисления и запросы обрабатываются в процессе построения EMF, а в EMF сохраняются только результаты. По этой причине в EMF не предусмотрены записи для информационных функций GDI — таких, как GetDeviceCaps, GetBkMode, GetBkColor и т. д. Построение EMF не сводится к простой записи потока команд; это комбинация записей с вычислениями. Если ваш графический код содержит условные вычисления или зависит от значений, возвращаемых при вызове функций, состояние эталонного контекста устройства фиксируется на момент построения. Нельзя гарантировать, что воспроизведение записанного EMF приведет к тому же результату, что и исходный код. О Только кисти, перья, шрифты, палитры и цветовые пространства (для ICM) кодируются в виде объектов GDI (то есть в EMF для них создаются записи
916 Глава 16. Метафайлы создания, выбора и удаления объектов). Траектории также неявно интерпретируются как объекты GDI, однако в EMF не предусмотрена запись для получения данных траектории функцией GetPath, поскольку это требует использования переменных. «Тяжеловесные» объекты GDI — аппаратно-зависимые растры (DDB), DIB-секции, регионы, совместимые контексты устройств и расширенные метафайлы — не сохраняются в EMF в виде объектов GDI. Например, в EMF не существует записи для создания DDB-растров или вложенных метафайлов. Как будет показано ниже, полные данные DDB, DIB- секции или региона передаются в виде дополнений к записям тех графических функций, в которых они используются. Вызовы функций с участием совместимых контекстов устройств или других контекстов, кроме приемного, просто выполняются без записи в EMF. О Поддерживаемые модулем управления окнами (USER32.DLL), графические функции не сохраняются в EMF непосредственно. В частности, в табл. 16.1 не встречаются записи для таких функций, как DrawEdge, DrawFrameControl, DrawCaption, Drawlcon, DrawText и т. д. Некоторые из этих функций пользуются услугами GDI; соответствующие вызовы GDI сохраняются в EMF. Другие задействуют системные функции, работающие в обход пользовательской части GDI, где происходит запись EMF. Скажем, в модуле USER поддерживается системная функция NtUserDrawCaption. Графические вызовы, обходящие пользовательскую часть GDI, в EMF не сохраняются. О Поддержка OpenGL в EMF представлена специальными данными заголовка и тремя специальными типами записей EMF. DirectX в EMF не поддерживается. О Команды печати в EMF не поддерживаются, хотя GDI и спулер могут сохранить задание печати в EMF-файле спулера и воспроизвести его позднее при помощи драйвера устройства. Расшифровка записей EMF Зная манипулятор EMF, можно вызвать функцию GDI GetEnhMetaFileBits и получить все записи EMF для последующей обработки. Функция GetEnhMetaFileBits определяется следующим образом: UINT GetEnhMetaFileBits(HENUMMETAFILE hEmf. UINT cbBuffer, LPBYTE IpbBuffer); Сначала приложение получает размер EMF, вызывая GetEnhMetaFileBits с последним параметром, равным NULL, а затем получает данные EMF следующим вызовом. Приведенный ниже фрагмент иллюстрирует методику перебора всех записей EMF. int DumpEMF(HENHMETAFILE hEmf. ofstream & stream) { int size - GetEnhMetaFileBits(hEmf. 0. NULL); if ( size<=0 ) return 0; BYTE * pBuffer = new BYTE[size]; if ( pBuffer==NULL )
Строение расширенных метафайлов 917 return 0; GetEnhMetaFileBits(hEmf, size, pBuffer); const EMR * emr = (const EMR *) pBuffer; TCHAR mess[MAX_PATH]; int recno = 0; // Перебор всех записей EMF while ( (emr->iType>=EMR_MIN) && (emr->iType<=EMR_MAX) ) { recno ++; wsprintf(mess, "*3d: EMRJ03d U4d bytes)\n". recno, emr->iType. emr->nSize); stream « mess; if ( emr->iType— EMRJOF ) break; emr = (const EMR *) ( ( const char * ) emr + emr->nSize ); } delete [] pBuffer; return recno; } Функция DumpEMF перебирает все записи EMF и выводит номер, тип и размер каждой записи в файловый поток C++. Эта функция всего лишь показывает, что перебор записей помогает разобраться во внутренней структуре EMF. На компакт-диске имеется мощное средство для анализа EMF, реализованное в классе KEmfDC. Этот класс позволяет вывести содержимое EMF в виде иерархического дерева (TreeView) с расшифровкой записей EMF на команды и параметры GDI. На рис. 16.4 слева приведена расшифровка команд EMF, а справа — результат воспроизведения EMF. ъЖ>' *, nBytes 427460 |*]! nRecords 47 nHandles 3 sReserved 0 nDescription 17 off Description 108 nPalE nines 0 szlDevice {1152,864} szlMillimeters { 320,240} cbPixelFormat 0 offPixelFormat 0 bOpenGL 0 szMicroMeters {320000,240000} 2 h0b|[1 ]=CreateFont(-48A0,0,400,0,0,0,0,4,0,0,1 • 3 SelectOb|ect(hDC, h0b|[1 ]),12 bytes 4 StretchDIBits(hDC, 50,50,554,278,0,0,554,278 5 SetBkModefhDC, OPAQUE)/! 2 bytes 6 SetBkColorfhDC, RGB(0xFF,0xFF,0xFF)),12 byte • 7 SetTextColorfhDC, RGB(0,0,0)),12 bytes mjOl-rfil jT Рис. 16.4. Расшифровка и просмотр EMF в программе
918 Глава 16. Метафайлы На рисунке воспроизведен метафайл с рельефным текстом, созданный одной из программ главы 15. Программа просмотра и расшифровки EMF входит в один из примеров этой главы. Откройте EMF-файл на диске или вставьте EMF из буфера обмена — программа расшифрует его записи и воспроизведет их. В левой части рисунка видна часть заголовка EMF и пять расшифрованных команд GDI. Как показывает заголовок, метафайл состоит из 47 записей, имеет длину 427 460 байт и записан для экрана 1152 х 864 пикселов. Среди записей EMF мы видим функции создания логического шрифта, выбора его в контексте устройства, вывода растров и настройки цвета/режима заполнения фона для последующего вывода. В правой части окна изображен результат воспроизведения метафайла в масштабе 1:1. Простые объекты GDI в EMF Программа просмотра и расшифровки EMF (см. рис. 16.4) является ценным инструментом для анализа метафайлов и диагностики проблем, возникающих при работе с ними. Давайте рассмотрим структуру EMF подробнее. Только четыре типа объектов GDI — логическое перо, логическая кисть, логический шрифт и логическая палитра — сохраняются в EMF именно как объекты GDI. Функции создания объектов этих типов имеют похожее представление в записях EMR: список параметров начинается с индекса в таблице манипуляторов EMF, за которым следует логическое определение объекта. В качестве примера приведу структуру записи EMF для функции CreatePen: typedef struct tagEMRCREATEPEN { EMR emr; // Стандартный заголовок DWORD ihPen: // Индекс в таблице манипуляторов LOGPEN lopn; // Логическое определение } EMRCREATEPEN; При воспроизведении EMF GDI создает небольшую таблицу манипуляторов, число элементов в которой определяется полем nHandles записи заголовка EMF. Индекс в записи EMRCREATEPEN относится именно к этой таблице манипуляторов EMF, а не к скрытой системной таблице объектов GDI. Первый элемент таблицы манипуляторов EMF резервируется. Таблица описывается структурой HANDLETABLE. typedef struct tagHANDLETABLE { HGDIOBJ objecthandleCl]: // Переменный размер, определяется nHandles } HANDLETABLE: Стандартные объекты GDI не хранятся в таблице манипуляторов EMF. Для ссылки на стандартный объект его идентификатор указывается с обратным знаком. В настоящее время документируются стандартные объекты GDI с GetStock- Object(WHITE_BRUSH) до GetStockObject(DC_PEN). В GDI32.H WHITEJRUSH определяется со значением 0, a DC_PEN — со значением 19, поэтому их индексы лежат в интервале от 0 до -19 соответственно. Это также объясняет, почему индекс 0 в таблице манипуляторов EMF зарезервирован. В EMF записи часто ссылаются на логические перья, кисти, шрифты или палитры. Из рис. 16.4 видно, что вторая запись EMF создает логический шрифт и заносит его в элемент 1 таблицы манипуляторов EMF; третья запись EMF выбирает объект, ссылка на который хранится в элементе 1, в контексте устройства.
Строение расширенных метафайлов 919 Применение простой таблицы манипуляторов EMF изящно решает проблему временной, недетерминированной природы манипуляторов объектов GDI. Впрочем, преобразование манипуляторов GDI в индексы — не такая простая задача, как кажется на первый взгляд. Во-первых, GDI не может просто зарегистрировать функцию создания объекта GDI, поскольку манипулятор может вообще не использоваться в эталонном контексте устройства. Запись создания объекта сохраняется в EMF лишь при первом фактическом использовании манипулятора. Это означает, что при каждой ссылке на манипулятор пера, кисти, палитры или шрифта GDI приходится просматривать таблицу манипуляторов EMF и проверять, был ли ранее зарегистрирован данный манипулятор. Если манипулятор задействуется впервые, GDI при помощи функции GetObject получает определение, по которому создавался манипулятор, и генерирует запись создания объекта; в противном случае берется индекс из таблицы. Функция DeleteObject тоже сохраняется в EMF с типом EMRDELETEOBJECT. Конечно, это происходит лишь в том случае, если манипулятор реально использовался. После воспроизведения EMF в таблице манипуляторов EMF могут остаться неудаленные элементы. GDI гарантирует, что таблица будет должным образом освобождена; это предотвращает утечку объектов GDI при воспроизведении EMF, обусловленную ошибками при записи. Приложение может проверить количество манипуляторов в таблице и узнать, какие манипуляторы остались в ней после воспроизведения. Это тоже помогает бороться с утечками ресурсов. Растры в EMF GDI поддерживает три типа растров: аппаратно-зависимые растры (DDB), аппа- ратно-независимые растры (DIB) и DIB-секции. DIB-растры не являются объектами GDI в том смысле, что GDI не управляет хранением их данных, однако DDB и DIB-секции принадлежат к числу объектов GDI. Тем не менее вы не встретите в EMF записей создания объектов DDB и DIB-секций. DDB по своей природе зависит от конкретного устройства и даже от его текущей конфигурации. Конечно, DDB нельзя напрямую сохранить в расширенном метафайле, который должен обеспечивать аппаратно-независимое представление графических данных. Другая проблема с DDB-растрами заключается в том, что они не могут непосредственно выводиться в контексте устройства; для работы с ними приходится привлекать совместимый контекст устройства. Возможно, именно из-за этого разработчики GDI не стали интерпретировать DDB и DIB- секции в расширенных метафайлах как объекты GDI. Вместо этого DDB и DIB- секции всегда преобразуются в неупакованные DIB-растры. В GDI неупакованный DIB-растр представлен двумя указателями — на структуру BITMAP INFO и на массив пикселов. При отсутствии манипулятора сослаться на растр в EMF невозможно; было решено, что данные растров следует передавать вместе с теми командами, в которых они используются. Давайте посмотрим, как функция BitBlt GDI кодируется в записи EMF EMRBITBLT. typedef struct tabEMRBITBLT { EMR emr;
920 Глава 16. Метафайлы RECTL LONG LONG LONG LONG DWORD LONG LONG XFORM COLORREF DWORD DWORD DWORD DWORD DWORD EMRBITBLT rclBounds; xDest; yDest; cxDest; , cyDest; dwRop; xSrc; ySrc; xformSrc; crBkColorSi iUsageSrc; offBmiSrc; cbBmiSrc; offBitsSrc cbBitsSrc; // Задается в единицах устройства // Преобразование исходного контекста устройства // Фоновый цвет исходного контекста устройства в RGB // Формат цветовой таблицы растра // (DIB_RGB_COLORS) // Смещение структуры BITMAPINFO // Размер структуры BITMAPINFO // Смещение графических данных растра // Размер графических данных растра Вспомните, что функция BitBlt GDI получает девять параметров: манипулятор приемного контекста устройства, четыре параметра приемного многоугольника, манипулятор исходного контекста устройства, базовую точку в исходном контексте и растровую операцию. В структуре EMRBITBLT приемный контекст устройства не нужен, поскольку он определяется косвенно; приемный прямоугольник представлен четверкой {xDest, yDest, cxDest, cyDest}, базовая точка источника представлена полями {xSrc, ySrc}, а растровая операция определяется полем dwRop. Остается разобраться с манипулятором исходного контекста (и восемью полями структуры EMRBITBLT). Источником при вызове BitBlt может быть совместимый контекст устройства с выбранным DDB-растром или DIB-секцией или же экранный контекст устройства. Маловероятно, чтобы им оказался контекст устройства принтера, поскольку контексты принтеров обычно недоступны для чтения. Совместимый контекст устройства можно представить растром, который в нем содержится, а экранный контекст устройства легко преобразуется в растр, состоящий из его текущих пикселов. В любом случае источник преобразуется в DIB-растр, описываемый последними полями EMRBITBLT. Поле iUsage обеспечивает интерпретацию цветовой таблицы, поля (off Bmi Src, cbBmiSrc) ссылаются на структуру BITMAPINFO, а поля (offBitsSrc, cbBitsSrc) идентифицируют массив пикселов. На результаты вызова BitBlt также может влиять состояние исходного контекста устройства. В поле xformSrc хранятся данные отображений из логических координат в координаты устройства. Параметр cbBkColorSrc определяет фоновый цвет исходного контекста устройства, который может использоваться для вывода цветных растров в монохромных контекстах устройств. Поле rcl Bounds содержит ограничивающий прямоугольник в системе координат эталонного устройства. Конечно, этот прямоугольник можно вычислить во время воспроизведения, но хранение его в EMRBITBLT несколько повышает быстродействие. Все растры в EMF представлены по одному образцу: флаг формата цветовой таблицы, дополнение с данными BITMAPINFO и дополнение с массивом пикселов. Недостаток подобного представления заключается в том, что растр сохраняется заново при каждом использовании. Если один и тот же растр применяется 100 раз,
Строение расширенных метафайлов 921 он будет 100 раз сохранен в EMF. Возможно, для небольших или однократно используемых растров это еще терпимо, но в современных графических пакетах, часто использующих растровые заливки, возникают серьезные проблемы. Как показал эксперимент для программы, при повторном выводе растра в EMF включается его полная копия. Таким образом, размер EMF возрастает пропорционально количеству применений растра. Похоже, при выводе горизонтальной полосы растра GDI усекает внедренные данные до меньших размеров, но в более сложных ситуациях в EMF включается весь растр. Поскольку с распространением Интернета и цифровых фотоаппаратов все чаще требуются растры с повышенной цветовой глубиной, а многие драйверы принтеров используют спулинг в формате EMF, при работе с растровыми изображениями в EMF прикладные программисты все чаще сталкиваются с проблемами быстродействия. Программа расшифровки и просмотра EMF, показанная на рис. 16.4, выводит размер каждой записи и размеры каждого растра, что упрощает диагностику проблем такого рода. Интересно другое: как при текущей структуре EMF решить эту проблему с минимальными изменениями в GDI и нельзя ли приложению каким-либо образом ограничить размеры EMF? Мы знаем, что дополнения (например, растры в записи EMRBITBLT) всегда хранятся после открытых полей, однако для ссылок на них используются 32-разрядные смещения. Если бы смещения могли быть отрицательными или выходить за границы текущей записи EMF, разные записи EMF могли бы совместно использовать одну копию растра. Также можно включить в EMF специальную запись создания растрового объекта, чтобы растр сохранялся в EMF только один раз и в дальнейшем на него можно было ссылаться по индексу, как на перо или кисть. Регионы в EMF Представление регионов в EMF имеет много общего с представлением растров. С регионами не ассоциируются манипуляторы; создание регионов и операции с ними просто выполняются без создания записей в EMF. При использовании региона в контексте устройства, в котором осуществляется запись, данные региона полностью включаются в запись EMF. Рассмотрим пример — запись EMREXTSELECTCLIPRGN, соответствующую функциям SelectClipRgn и ExtSel ectCl i pRgn. typedef struct tagEMREXTSELECTCLIPRGN { EMR emr; DWORD cbRgnData; // Размер данных региона в байтах DWORD i Mode; BYTE RgnData[l]: } EMREXTSELECTCLIPRGN; Как видно из определения EMREXTSELECTCLIPRGN, данные региона включаются в запись даже без стандартного поля смещения. Массив переменного размера RgnData в действительности содержит структуру RGNDATA, возвращаемую функцией GetRegionData.
922 Глава 16. Метафайлы Как и в случае с растрами, при многократном использовании одного региона в EMF сохраняется несколько копий его данных. Если ваше приложение работает со сложными регионами, будьте внимательны. Траектории в EMF Операции с траекториями в EMF достаточно близки к аналогичным функциям GDI. В EMF предусмотрены типы записей для функций BeginPath, CloseFigure, EndPath и функций прорисовки траекторий. Таким образом, можно говорить о неявной реализации траектории как объекта GDI. Функции SaveDC и RestoreDC тоже поддерживаются в EMF, что позволяет использовать данные одной траектории несколько раз. Поддержка функции SelectClipPath в записях EMF тоже сокращает необходимость во внедрении данных траекторий. Например, если приложение хочет использовать эллиптический регион отсечения, то вместо функций CreateEllip- ticRgn и SelectRegion оно может воспользоваться функциями BeginPath, Ellipse, EndPath и SelectClipPath и избежать включения данных региона в EMF. Если вы используете функцию GetPath для получения данных траектории, вызовите PolyDraw для ее прорисовки; данные траектории внедряются в запись EMRPOLYDRAW. Палитры в EMF В EMF предусмотрены типы записей для основных операций с палитрой в GDI (функции CreatePalette, SelectPalette, ResizePalette и RealizePalette). Логическая палитра интерпретируется в EMF как объект GDI. Однако GDI несколько иначе интерпретирует вызовы SelectPalette в EMF. Напомню, что при вызове SelectPalette передаются три параметра: манипулятор контекста устройства, манипулятор палитры и флаг. Флаг является признаком форсированного использования фоновой палитры. Если окно, в котором осуществляется вывод, является активным, а параметр bForceBackground функции SelectPalette равен FALSE, палитра позднее будет реализована в качестве основной. Все различия между основной и фоновой палитрами состоят в том, что только основная палитра может удалять нестатические цвета из системной палитры, чтобы реализовать больше однородных цветов для повышения точности цветопередачи; фоновая палитра может лишь занимать свободные позиции палитры или подбирать подходящие цвета среди уже существующих. В записи EMF для функции SelectPalette (тип EMRSELECTPALETTE) параметр bForceBackground не фиксируется. При воспроизведении EMF логическая палитра всегда выбирается в качестве фоновой. Смысл такого решения заключается в том, что воспроизведение EMF не должно приводить к изменению текущих цветов экрана, что привело бы к раздражающему мерцанию и искажению цветов. Управление основной палитрой не может осуществляться на уровне EMF, оно относится к более высокому уровню обработки сообщений (то есть выполняется в обработчиках WMPAINT и WMPALETTECHANGED). Если приложение действительно хочет согласовать текущую системную палитру с цветами EMF (то есть реализовать палитру EMF в качестве основной),
Строение расширенных метафайлов 923 к его услугам полная цветовая таблица, которую GDI сохраняет в последней записи EMRE0F. В процессе записи EMF GDI накапливает элементы палитры и заносит их в «палитру метафайла», входящую в запись EMRE0F. Приложение может получить палитру метафайла при помощи функции GetEnhMetaFilePaletteEntries. UINT GetEnhMetaFilePaletteEntries(HENHMETAFILE hemf, UINT cEntries, LPPALETTEENTRY lppe); А теперь подумайте, как эта функция реализуется в GDI? Конечно, по манипулятору EMF GDI может найти запись EMRE0F, но как именно это сделать при отсутствии прямых ссылок? Перебор всех записей EMF займет слишком много времени, если для этого придется загружать EMF в память с диска. Разгадка кроется в nSizeLast, последнем поле записи EMRE0F. Вспомните: поле nSizeLast расположено после цветовой таблицы. По общему размеру EMF, хранящемуся в заголовке EMF, GDI может определить адрес поля nSizeLast. В этом поле хранится размер записи EMRE0F, по которому легко определяется начало записи EMRE0F. Операции с палитрой подробно рассматриваются в главе 13. Следующий фрагмент показывает, как создать логическую палитру по цветовой таблице EMF. HPALETTE GetEMFPaletteCHENHMETAFILE hEmf. HDC hDC) { // Запросить количество элементов int no « GetEnhMetaFi1ePaletteEntries(hEmf, 0, NULL); if ( no<=0 ) return NULL; // Выделение памяти LOGPALETTE * pLogPalette - (LOGPALETTE *) new BYTE[sizeof(LOGPALETTE) + (no-1) * sizeof(PALETTEENTRY)]; pLogPalette->palVersion = 0x300; pLogPalette->palNumEntries = no; // Получение данных GetEnhMetaFilePaletteEntries(hEmf, no, pLogPalette->palPal Entry); HPALETTE hPal = CreatePalette(pLogPalette); delete [] (BYTE *) pLogPalette; return hPal; } Приложение может реализовать логическую палитру, возвращенную функцией GetEMFPalette, в качестве основной и организовать обработку сообщений палитры. И последнее, что следует сказать о палитрах и EMF: перед воспроизведением EMF функцией PlayEnhMetaFile GDI сбрасывает контекст устройства в состояние по умолчанию и восстанавливает его позднее. Следовательно, EMF не сможет воспользоваться содержимым логической палитры, выбранной в контексте устройства перед воспроизведением.
924 Глава 16. Метафайлы Системы координат в EMF В воспроизведении EMF участвуют два контекста устройств: эталонный и приемный. Эталонный контекст устройства используется при построении EMF, а в приемном контексте EMF воспроизводится функцией PlayEnhMetaFile. Эталонный контекст устройства не имеет внешних проявлений, но именно к нему относятся координаты, хранящиеся в записях EMF. Каждый контекст устройства обладает логической системой координат и системой координат устройства, поэтому в воспроизведении EMF участвуют по меньшей мере четыре системы координат: О логическая система координат эталонного контекста; О система координат устройства эталонного контекста; О логическая система координат приемного контекста; О система координат устройства приемного контекста. Я говорю «по меньшей мере четыре», поскольку ничто не гарантирует, что в эталонном контексте используется всего одна логическая система координат. EMF полностью поддерживает настройку и модификацию логических систем координат функциями SetWi ndowExtEx, SetViewportExtEx, SetWorldTransform и т. д. Возникает непростой вопрос: допустим, в EMF записывается команда вывода линии длиной 1 дюйм в режиме отображения MMLOENGLISH или MMHIENGLISH. Какую длину будет иметь линия при воспроизведении EMF функцией PlayEnhMetaFile? Еще более сложный вопрос: если мы выбираем регион отсечения, заданный в системе координат устройства, как он будет интерпретироваться при воспроизведении EMF? При воспроизведении EMF GDI может интерпретировать записи EMF, изменяющие системы координат, как отображение логической системы координат эталонного контекста в систему координат устройства. Пусть это отображение определяется матрицей преобразования xformSrc. Все точки логической системы координат приемного устройства также отображаются в систему координат устройства. Пусть матрица этого преобразования называется xformDst. Таким образом, отображение координатных пространств при воспроизведении EMF определяется связью между системой координат устройства эталонного контекста и логической системой координат приемного контекста. Назовем соответствующую матрицу преобразования xformPlay. На рис. 16.5 изображены два контекста устройств, четыре координатных пространства и три преобразования, участвующие в воспроизведении EMF. Матрица преобразования xformPlay вычисляется по прямоугольнику кадра, хранящемуся в заголовке EMF (rclFrame), и прямоугольнику, переданному при вызове PlayEnhMetaFile (IpRect). Остается лишь преобразовать rcl Frame из единиц 0,01 мм в систему координат устройства эталонного контекста. Следующий фрагмент показывает, как вычисляется матрица преобразования.
Строение расширенных метафайлов 925 Логическая система координат эталонного контекста Система координат устройства эталонного контекста Логическая система контекста координат приемного контекста Система координат устройства приемного ,."*г „,-"*' .у ..** S* rcl Frame в заголовке EMF ipRect, переданный при вызове PlayEnhMetaFile Рис. 16.5. Системы координат и преобразования, участвующие в воспроизведении EMF // Преобразование из координат устройства эталонного контекста // в логические координы приемного контекста BOOL GetPlayTransformation(HENHMETAFILE hEmf. const RECT * rcPic, XFORM & xformPlay) { ENHMETAHEADER emfh; if ( GetEnhMetaFileHeader(hEmf, sizeof(emfh), return FALSE; & emfh)<=0 ) try // Единицы 0,01 мм -> 1 мм -> проценты -> пикселы устройства double sxO = emfh.rclFrame.left / 100.0 / emfh.szlMillimeters.cx * emfh.szlDevice.ex; double syO = emfh.rclFrame.top / 100.0 / emfh.szlMillimeters.cy * emfh.szlDevice.cy; double sxl = emfh.rclFrame.right / 100.0 / emfh.szlMillimeters.cx * emfh.szlDevice.ex; double syl = emfh.rclFrame.bottom / 100.0 / emfh.szlMillimeters.cy * emfh.szlDevice.cy; // Отношения размеров источника к приемнику double rx = (rcPic->right - rcPic->left) / ( sxl - sxO ); double ry = (rcPic->bottom - rcPic->top) / ( syl - syO ); у * eM21 + eDx * eM22 + eDy // x' - x * eMll // у' = x * eM12 ■ j ...__ xformPlay.eMll = (float) rx; xformPlay.eM21 - (float) 0; xformPlay.eDx = (float) (- sxO rx rcPic->left); xformPlay.eM12 = (float) 0; xformPlay.eM22 = (float) ry: xformPlay.eDy = (float) (- syO * ry + rcPic->top);
926 Глава 16. Метафайлы } catch (...) { return FALSE; } return TRUE; } Обратите внимание: при воспроизведении вместо четырех систем координат мы работаем с тремя матрицами преобразований. Матрица xformDst определяется приложением в приемном контексте устройства перед воспроизведением EMF. Матрица xformSrc изначально определяет тождественное преобразование и динамически изменяется при воспроизведении записей EMF, связанных с изменением системы координат. Связующим звеном между этими матрицами является матрица преобразования xformPlay, которая определяется в заголовке EMF, передаваемого функции PlayEnhMetaFile. Как правило, вам не приходится думать о матрице преобразования приемного контекста, поскольку при выводе на приемной поверхности GDI обычно работает с логическими координатами. Все логические координаты в EMF перед выводом на приемной поверхности должны пройти через матрицы преобразований xformSrc и xformPlay. Некоторые координаты EMF (например, координаты в регионах отсечения) относятся к системе координат устройства эталонного контекста. Такие координаты необходимо преобразовать в систему координат устройства приемного контекста. GDI может выполнить это преобразование последовательным применением матриц xformPlay и xformDst. Как говорилось выше, все данные регионов в EMF хранятся в структуре RGNDATA, преобразуемой в объект региона GDI функцией ExtCreateRegion. Функция ExtCreateRegion удобна тем, что при вызове ей можно передать матрицу преобразования, применяемую ко всем координатам, а для объединения двух матриц преобразований можно воспользоваться функцией CombineTransform. Из-за операций масштабирования и отображения, используемых при работе с EMF, разрешение логической системы координат эталонного устройства оказывает значительное влияние на качество графики. Если метафайл строился в режиме отображения ММ_ТЕХТ на экране с разрешением 96 dpi, все координаты будут представляться целыми числами в этой системе координат и в этом разрешении. При воспроизведении EMF с увеличением и на устройствах высокого разрешения координаты масштабируются, что может нарушить выравнивание текста или создать неровности в контурах многоугольников и траекторий. Команды вывода в EMF EMF поддерживает широкий ассортимент графических команд GDI. Из 122 известных типов записей EMF 47 соответствуют графическим функциям GDI. Существует еще несколько типов записей, предназначенных для записи команд OpenGL, а также могут существовать недокументированные графические команды.
Строение расширенных метафайлов 927 Обычно все координаты в EMF хранятся в виде 32-разрядных значений. Впрочем, в восьми типах записей EMF основные данные хранятся в 16-разрядном формате, например, функция PolyPolygon представлена двумя типами записей EMF — 32-разрядной версией EMRP0LYP0LYG0N и 16-разрядной версией EMR- P0LYP0LYG0N16. Эти две версии различаются только форматом массива точек. Хотя 16-разрядные версии почти вдвое сокращают затраты памяти, если значения логических координат ограничиваются 16 битами, неизвестно, в каких именно системах они используется. Даже Windows 98 при спулинге в формате EMF генерирует записи EMRP0LYP0LYG0N вместо EMRP0LYP0LYG0N16. Для элементарных графических функций (таких, как LineTo и Rectangle) в записях EMF сохраняются только исходные координаты. Для более сложных функций GDI сохраняет в записях EMF ограничивающий прямоугольник в системе координат устройства. В частности, поле ограничивающего прямоугольника включается в записи EMRPOLYLINE, EMRP0LYG0N и EMRSTRETCHBLT. Ограничивающий прямоугольник иногда бывает очень полезен — например, по нему можно исключить из воспроизведения записи EMF, отсекаемые или выходящие за границы текущей области вывода. В частности, эта возможность может использоваться GDI при поэтапной передаче EMF-файла спулера, когда драйвер принтера при каждой пересылке принимает всего одну горизонтальную полосу, a GDI повышает эффективность пересылки за счет передачи только тех команд, которые соприкасаются с прямоугольником текущей полосы. Базовые функции вывода отдельных пикселов, SetPixel и SetPixelV, представлены одним типом записи EMF EMRSETPIXELV. Конечно, это объясняется тем, что зависимость от возвращаемого значения функции приходится исключать на стадии построения EMF. В формате EMF практически полностью поддерживаются функции GDI для вывода линий и кривых, разве что функция MoveToEx не возвращает предыдущей позиции курсора, а функция LineDDA не поддерживается. Впрочем, LineDDA не обеспечивает самостоятельного вывода, поскольку она всего лишь разбивает линию на последовательность координат пикселов в логической системе координат. Весь вывод осуществляется функциями косвенного вызова, предоставленными вызывающей стороной; эти команды будут сохранены в файле. В полной мере поддерживаются и функции GDI, предназначенные для заполнения замкнутых фигур. Например, функции Chord, Ellipse, Polygon и даже PolyPolygon представлены в EMF соответствующими типами записей. Несколько странно выглядит тот факт, что для функций, определяющих геометрическую фигуру по ограничивающему прямоугольнику (например, Rectangle и Ellipse), GDI при записи EMF исключает правую нижнюю сторону. Например, прямоугольник {0,0,50,50} сохраняется в виде {0,0,49,49}. Таким образом, прямоугольники, хранящиеся в EMF, интерпретируются с включением всех сторон. Аналогичная интерпретация используется GDI при выводе в расширенном графическом режиме. При обычном выводе в масштабе 1:1 GDI точно передает исходную форму таких записей EMF, но при увеличении могут возникнуть искажения. Например, два прямоугольника Rectangle(0,0,50,50) и Rectangle(50,0,100,50) должны соприкасаться всегда, даже при увеличении. Но поскольку в EMF они представлены в виде {0,0,49,49} и {50,50,99,49}, при увеличении между ними возникает крошечный промежуток.
928 Глава 16. Метафайлы Три функции заполнения замкнутых фигур — FillRect, FrameRect и InvertRect — не принадлежат к числу функций GDI. Они реализуются модулем управления окнами USER32.DLL, который выполняет вывод при помощи функций GDI. Эти вызовы GDI — создание и выбор кисти, простой блиттинг — и будут сохранены bEMF. Функции прорисовки регионов FillRgn, FrameRgn, InvertRgn и PaintRgn поддерживаются полностью. Объект региона GDI преобразуется в структуру данных региона и присоединяется к этим графическим командам в виде дополнения. Функции построения траекторий, в отличие от функций построения регионов, поддерживаются полностью. Также поддерживаются функции прорисовки траекторий Fill Path, StrokeAndFi 11 Path и StrokePath. Поскольку эти функции ссылаются на неявный объект траектории в контексте устройства, в их записях EMF сохраняется простейший ограничивающий прямоугольник. Для вычисления реального ограничивающего прямоугольника GDI пришлось бы вызывать GetPath в процессе построения EMF. В EMF поддерживаются все функции вывода растров, от простейшей BitBlt до AlphaBlend, кроме PatBlt. Функция PatBlt объединяется с более общей формой BitBlt, и для них используется одна и та же запись EMRBITBLT. Из-за этого объединения PatBlt представляется 100 байтами. Если бы существовала отдельная запись EMRPATBLT, она бы состояла из 66 байт. Текст в EMF Оставшиеся четыре типа записей EMF предназначены для вывода текста. Они представляют функции GDI ExtTextOut и менее известную функцию PolyTextOut в ANSI- и Unicode-версиях. Функция PolyTextOut представляет собой простую последовательность вызовов ExtTextOut, объединенных в один вызов. Вызовы TextOut преобразуются GDI в ExtTextOut. В EMF они представлены одним типом EMREXTTEXTOUT. Как говорилось выше, в GDI передавать массив межсимвольных расстояний при вызове ExtTextOut необязательно, но в EMF этот массив всегда заполняется правильными данными, что обеспечивает фиксированное, однозначное расположение всех символов в строке. Из рис. 16.1 и 16.2 видно, что при увеличении EMF расстояния между символами тоже масштабируются. Однако глифы символов на этих двух рисунках сохраняют прежние размеры, поскольку при выводе используется шрифт, выбранный по умолчанию в контексте устройства. Если бы при выводе использовался логический шрифт, определенный в программе, глифы бы нормально масштабировались. Мы знаем, что увеличение иногда приводит к искажениям, поскольку исходные данные получаются посредством округления более точных вещественных величин. Чтобы сгенерировать точные массивы межсимвольных расстояний, приложение может создать логический контекст устройства с высоким разрешением и воспользоваться приемами, описанными в предыдущей главе. Как показали эксперименты, в Windows 2000 с компонентом Uniscribe и установленной поддержкой нескольких языков после записей EMRTEXT0UT для функций TextOut и ExtTextOut добавляются вызовы создания, выбора и удаления логических шрифтов. В результате при каждом выводе текста в EMF может появляться десяток ненужных записей. В Windows NT и даже в ранних версиях Windows 2000 ничего похожего нет.
Строение расширенных метафайлов 929 Вызовы DrawText, DrawTextEx и TabbedTextOut в EMF преобразуются в серии вызовов ExtTextOut, чередующихся с вызовами SetTextAlign, SetBkMode, MoveToEx и даже SelectClipRgn. Вероятно, вас удивит количество записей EMF, представляющих всего один вызов DrawText или TabbedTextOutput. Аппаратная независимость EMF Познакомившись поближе с архитектурой EMF, давайте вернемся к главному вопросу — до какой же степени формат EMF является аппаратно-независимым? Аппаратная независимость — важнейший аспект архитектуры расширенных метафайлов. При описании аппаратной независимости EMF Microsoft утверждает, что сохраненный в EMF рисунок 2x4 дюйма сохраняет исходные размеры при печати на принтере с разрешением 300 dpi и при выводе на монитор SuperVGA. При вызове CreateEnhMetaFile указывается прямоугольник кадра, сохраняемый в заголовочной структуре EMF вместе с данными о разрешении и размерах поверхности. Приложение может запросить данные из заголовка, получить размеры рисунка в физических единицах, преобразовать их в логическую систему координат текущего контекста устройства и указать при вызове PlayEnhMetaFile; в этом случае метафайл будет иметь точно такие же размеры, с какими он был записан. EMF также можно масштабировать с разными коэффициентами. Словом, в отношении аппаратной независимости размеров EMF проявляет себя неплохо. С другой стороны, метафайл, при построении которого за эталон был взят экранный контекст, зависит от размеров экрана, для которых система всегда возвращает значения 320 х 240 мм. Разрешение, вычисленное по этим размерам, отличается от логического разрешения экрана, используемого в большинстве приложений. Другая проблема, которую также пытается решить EMF, — аппаратная зависимость цветов. Все аппаратно-зависимые растры в EMF преобразуются в ап- паратно-независимые растры. Цвета накапливаются и сохраняются в палитре метафайла, которую приложение может легко получить и реализовать перед воспроизведением EMF. Следовательно, если метафайл использует логическую палитру, его цвета можно достаточно успешно воспроизвести на другом устройстве с поддержкой палитры. Конечно, при воспроизведении цветов на устройствах High Color и True Color возникают небольшие проблемы. Но если метафайл был построен на устройстве True Color и не использует логическую палитру, его воспроизведение на устройствах с палитрой плохо обеспечивается на уровне базовых возможностей EMF. Даже если приложение выберет и реализует полутоновую палитру перед воспроизведением такого метафайла, GDI все равно возвращается к системной палитре из 20 стандартных цветов. Другой аспект аппаратной независимости — различия в реализациях вечно изменяющегося интерфейса Win32 API. Метафайл, созданный в Windows NT/ 2000, не всегда удается полностью воспроизвести в Windows 95/98, поскольку в нем могут использоваться дополнительные возможности, поддерживаемые только в Windows NT/2000. Чтобы ваш метафайл мог использоваться на всех активных платформах Win32, приходится ограничиваться функциями GDI, реализованными в Windows 95.
930 Глава 16. Метафайлы Проблемы возникают и со шрифтами. Запись создания логического шрифта EMREXTCREATEFONTINDIRECTW содержит очень подробное описание шрифта в виде структуры EXTLOGFONTW. В нее входит структура LOGFONT вместе с полным именем, стилем, версией, идентификатором разработчика и числом PANOSE. Руководствуясь этими данными, GDI находит точное или хотя бы очень близкое соответствие. Но если подходящий шрифт найти не удается, EMF не удастся нормально воспроизвести на другом компьютере. Спулер Windows NT/2000 решает проблему зависимости от шрифтов, внедряя шрифты в EMF-файл спулера перед его отправкой на сервер печати. Однако базовый формат EMF не поддерживает ни внедрения шрифтов, ни оперативной установки шрифтов в системе. В EMF отсутствуют записи для таких функций, как AddFontResource. Перечисление записей EMF В предыдущем разделе было показано, как происходит перебор всех записей EMF. В Win32 API поддерживается интересная функция EnumEnhMetaFile, которая позволяет приложению организовать перечисление всех записей при воспроизведении EMF в контексте устройства. Это действительно интересная и неповторимая функция, поскольку с ее помощью приложение может следить за воспроизведением EMF и вмешиваться в него в случае необходимости. typedef struct tagHANDLETABLE { HGDIOBJ objecthandle[l]; // Переменный размер } HANDLETABLE; typedef struct tagENHMETARECORD { DWORD iType; DWORD nSize; DWORD dParm[l]; // Переменный размер } ENHMETAFILERECORD; typedef int (CALLBACK* ENHMFENUMPROCKHDC hDC, HANDLETABLE * IpHTable, CONST ENHMETARECORD * IpEMFR, int nObj, LPARAM lpData): BOOL EnumEnhMetaFileCHDC hDC. HENHMETAFILE emf, ENHMFENUMPPRC IpEnhMetaFunc, LPVOID lpData. CONST RECT * IpRect); BOOL PlayEnhMetaFileRecord(HDC hDC. LPHANDLETABLE IpHandleTable. CONST ENHMETARECORD * IpEnhMetaRecord. UINT nHandles); Функция EnumEnhMetaFile получает пять параметров: манипулятор приемного контекста устройства, манипулятор EMF, указатели на функцию косвенного вызова и данные, предоставленные приложением, и прямоугольник воспроизведения EMF на приемной поверхности. По сравнению с PlayEnhMetaFile добавились два новых параметра — третий и четвертый. На самом деле функции EnumEnhMetaFile и PlayEnhMetaFile похожи — обе воспроизводят EMF в прямоугольной области приемного контекста устройства. Впрочем, PlayEnhMetaFile еще и вызывает заданную функцию для каждой записи EMF. Функция косвенного вызова в EnumEnhMetaFile тоже получает пять параметров: манипулятор приемного контекста устройства, указатель на таблицу мани-
Перечисление записей EMF 931 пуляторов EMF, указатель на текущую запись EMF, размер таблицы манипуляторов EMF и указатель на данные, предоставленные приложением. Таблица манипуляторов EMF предназначена для преобразования индексов объектов, используемых в EMF, в манипуляторы объектов GDI. Размер этой таблицы хранится в заголовочной записи EMF. Информация о каждой записи EMF передается функции косвенного вызова в структуре ENHMETARECORD. Запись EMF в этой структуре представляет собой 32- разрядный идентификатор типа и 32-разрядное поле размера, за которыми следует некоторое количество двойных слов. Отдельные записи EMF воспроизводятся функцией PlayEnhMetaFileRecord. Эта функция была включена в GDI для того, чтобы функция косвенного вызова могла вызвать ее и воспроизвести текущую запись EMF в приемном контексте устройства. Класс C++ для перечисления записей EMF Любая функция косвенного вызова, получающая данные от приложения, является хорошим кандидатом для объектной реализации, поскольку вы можете передать указатель this статической функции, которая передаст вызов виртуальной функции C++. Ниже приведен родовой класс C++ для перечисления записей EMF. class KEnumEMF { // Виртуальная функция для обработки записей EMF // Чтобы завершить перечисление, функция возвращает О virtual int ProcessRecordCHDC hDC, HANDLETABLE * pHTable. const ENHMETARECORD * pEMFR. int nObj) { return 0; } // Статическая функция косвенного вызова // передает управление виртуальной функции ProcessRecord static int CALLBACK EMFProc(HDC hDC, HANDLETABLE * pHTable. const ENHMETARECORD * pEMFR, int nObj, LPARAM IpData) { KEnumEMF * pObj = (KEnumEMF *) IpData: if ( IsBadWritePtrCpObj. sizeof(KEnumEMF)) ) { assert(false); return 0; } return pObj->ProcessRecord(hDC, pHTable, pEMFR, nObj); } public: BOOL EnumEMF(HDC hDC. HENHMETAFILE hemf. const RECT * IpRect) {
932 Глава 16. Метафайлы return ::EnumEnhMetaFile(hDC. hemf. EMFProc, this. IpRect); } }: Класс KEnumEMF содержит виртуальную функцию ProcessRecord, которая берет на себя роль функции косвенного вызова. Реализация по умолчанию возвращает 0, завершая перечисление записей EMF. Главная точка входа EnumEMF вызывает функцию EnumEnhMetaFi le GDI и передает ей статическую функцию EMFProc, которая передает управление виртуальной функции C++. Подобная инкапсуляция средств Win32 API в классах C++ хороша тем, что все операции с Win32 API выполняются всего в одном месте. Вы можете добавить новые переменные в производных классах, реализовать новые возможности переопределением виртуальных функций, не говоря уже о создании нескольких экземпляров класса. Замедленное воспроизведение EMF В простейшей реализации виртуальная функция KEnumEMF::ProcessRecord сводится к простому вызову PlayEnhMetaFileRecord. Фактически вы вручную реализуете PlayEnhMetaFile с небольшой задержкой, связанной с появлением дополнительного кода. Хотя это слово вызывает отрицательные ассоциации, правильно выбранная задержка помогает проследить за воспроизведением метафайлов. Приведенный ниже класс KDel ayEMF делает небольшую паузу перед воспроизведением записи. class KDelayEMF : public KEnumEMF { int m_delay; virtual int ProcessRecordCHDC hDC. HANDLETABLE * pHTable, const ENHMETARECORD * pEMFR. int nObj) { Sleep(m_delay); return PlayEnhMetaFileRecord(hDC, pHTable, pEMFR, nObj); } public: KDelayEMF(int delay) { m_delay = delay; } }: // Пример использования KDelayEMF delay(lO); del ay.EnumEMF(hDC. hEmf. IpPictureRect); Если вы когда-нибудь интересовались тем, как создаются качественные трехмерные эффекты при выводе текста, скопируйте объемный текст в программу EMF этой главы и проследите за замедленным воспроизведением. Построение трехмерной строки показано на рис. 16.6.
Перечисление записей EMF 933 Рис. 16.6. Замедленное воспроизведение EMF Трассировка воспроизведения EMF От простейшей задержки мы переходим на следующий уровень — трассировке воспроизведения EMF и выводе информации в текстовое окно. Класс KTraceEMF использует класс KEmfDC для расшифровки записей EMF и вывода данных в текстовом окне, реализованном классом KLogWindow. class KTraceEMF : public KEnumEMF { int KEmfDC int HGDIOBJ FLOAT m_nSeq; m emfdc; m value[32]; m_object[8]; m float[8]; virtual int ProcessRecordCHDC hDC, HANDLETABLE * pHTable, const ENHMETARECORD * pEMFR, int nObj) } CompareDC(hDC); m_pl_og->Log("*4d: *08x *3d % 6d \ m_nSeq++, pEMFR. pEMFR->iType, pEMFR->nSize); m_pLog->Log(m_emfdc.DecodeRecord((const EMR *) pEMFR)); m_pLog->Log("\r\n"); return PlayEnhMetaFileRecord(hDC. pHTable, pEMFR, nObj); public: KLogWindow * m_pLog; void CompareDC(HDC hDC);
934 Глава 16. Метафайлы KTraceEMFCHINSTANCE hlnst) } m_pLog = new KLogWindow; // Выделенная память освобождается // при обработке WMJOESTR0Y m_pLog->Create(hInst, "EMF Trace"); mjiSeq = 1; memset(m_value, memset(m_object, memsetdn float, OxCD, sizeof(m_value)); OxCD, sizeof(m_pbject)); OxCD, sizeof(m float)); Одна из дополнительных возможностей, реализованных в классе KTraceEMF, — сравнение атрибутов контекста устройства перед воспроизведением, между записями EMF и после воспроизведения. Атрибуты контекста устройства запрашиваются обычными функциями GDI (такими, как GetBkMode), сохраняются в трех массивах и сравниваются с предыдущими значениями. Наблюдая за изменениями в атрибутах контекста устройства, вы сможете лучше понять, как реализовано воспроизведение EMF. Ниже приведены неполные данные трассировки, полученные с использованием класса KTraceEMF. /////////////// Перед выводом /////////////// GraphicsMode WT.eMll WT.eM12 WT.eM21 WT.eM22 WT.eDx WT.eDy Pen Brush Font Palette 1 1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0x01b00017 0x01900010 0x018a0021 0xa50805ca /////////////// Начало вывода 111111111111111 GraphicsMode : 2 WT.eDx : 5.00000 WT.eDy : 5.00000 Font : 0x018a0026 Palette : 0x0188000b 1: 012e0000 1 132 // Заголовок 2: 012e0084 27 16 MoveToEx(hDC, 300, 50, NULL); 3: 012e0094 54 16 LineTo(hDC, 350. 50); 4: 012e00a4 27 16 MoveToExChDC. 300. 51. NULL); 5: 012e00b4 54 16 LineTo(hDC, 400. 51): 6: 012e00c4 43 24 Rectangle(hDC. 300. 60. 349. 7: 012e00dc 43 24 Rectangle(hDC. 350, 80. 399. 129); 8: 012e00f4 42 24 Ellipse(hDC, 410, 60. 459, 149); 9: 012e010c 42 24 Ellipse(hDC, 460, 60, 509, 149); 10: 012e0124 76 100 PatBltChDC. 300. 150. 100. 100. BLACKNESS); 11: 012e0188 76 100 PatBltChDC. 400. 160. 100. 100. PATINVERT); 109);
Перечисление записей EMF 935 12: 012е01ес 39 24 h0bj[l]=CreateSolidBrush(RGB(0x59.0x97. 0x64)); 13: 012е0204 37 12 SelectObjecUhDC, hObjCl]): Brush : 0x5fl0045e 71: 012e0978 14 20 // EMREOFC0. 16, 20) llllIII IIIII III После вывода 111111111111111 GraphicsMode : 1 WT.eDx : 0.00000 WT.eDy : 0.00000 Font : 0x018a0021 Palette : 0xa50805ca Происходит нечто весьма интересное. Перед вызовом EnumEnhMetaFile контекст устройства находится в совместимом графическом режиме с атрибутами по умолчанию (за исключением полутоновой палитры). Когда функция косвенного вызова приступает к обработке заголовочной записи EMF, контекст переключается в расширенный графический режим, матрица мирового преобразования обновляется, а манипуляторы шрифта/палитры заменяются стандартными объектами GDI. Это говорит о том, что в Windows NT/2000 GDI при воспроизведении EMF использует расширенный графический режим с мировым преобразованием, а другие атрибуты контекста устройства перед воспроизведением записей EMF всегда сбрасываются в состояние по умолчанию. Расширенный графический режим очень удобен для воспроизведения EMF. GDI просто объединяет три матрицы преобразования (см. рис. 16.5), назначает результат матрицей мирового преобразования при воспроизведении EMF и затем выводит все записи с исходными координатами, хранящимися в EMF. Остается лишь преобразовать регион отсечения из системы координат устройства эталонного контекста в систему координат устройства приемного контекста. В Windows 95/98 расширенный графический режим фактически не реализован, поэтому GDI приходится использовать режим отображения MM_ANISOTROPIC в сочетании со специальной настройкой отображения «окно/область просмотра», эквивалентной комбинированной матрице преобразования. В ходе трассировки также выводятся сведения об изменениях в объектах GDI, связанных с контекстом устройства. Мы видим, что GDI перед воспроизведением EMF всегда заменяет эти объекты стандартными объектами GDI. В частности, это объясняет, почему выбор логической палитры перед воспроизведением не обеспечивает вывода правильных цветов в EMF, не содержащих собственной логической палитры. Класс KTraceEMF можно наделить и другими полезными способностями — например, класс может отслеживать создание, выбор и удаление объектов GDI и искать возможные утечки ресурсов в EMF. Хотя GDI всегда освобождает манипуляторы, оставшиеся в таблице манипуляторов EMF, ликвидация утечки ресурсов поможет в отладке кода построения EMF. Динамическое изменение EMF Запись EMF, передаваемая функции косвенного вызова, доступна только для чтения; ее невозможно модифицировать и вернуть GDI. Однако приложение
936 Глава 16. Метафайлы может создать копию этой записи, изменить ее во время выполнения программы и передать GDI для вывода. Иначе говоря, программа может динамически изменить EMF и передать GDI измененный вариант. Ниже приведен простой класс, который преобразует все цвета текста, фона, перьев и кистей в оттенки серого. Если EMF не содержит цветных растров, в результате воспроизведения классом KGrayEMF цветной метафайл преобразуется в серый. Код преобразования цветных растров в оттенки серого приведен в главе 12. inline void MaptoGray(COLORREF & cr) { if ( (cr & OxFFOOOOOO) != PALETTEINDEX(O) ) // He является индексом { // палитры BYTE gray - ( GetRValue(cr) * 77 + GetGValue(cr) * 150 + GetBValue(cr) * 29 + 128 ) / 256; cr = (cr & OxFFOOOOOO) | RGB(gray, gray, gray); } } class KGrayEMF ; public KEnumEMF { virtual int ProcessRecordCHDC hDC. HANDLETABLE * pHTable. const ENHMETARECORD * pEMFR, int nObj) { int rslt; switch ( pEMFR->iType ) { case EMR_CREATEBRUSHINDIRECT: { EMRCREATEBRUSHINDIRECT cbi; cbi - * (const EMRCREATEBRUSHINDIRECT *) pEMFR; MaptoGray(cbi.lb.lbColor); rslt - PlayEnhMetaFileRecord(hDC. pHTable, (const ENHMETARECORD *) & cbi, nObj); } break; case EMR_CREATEPEN: { EMRCREATEPEN cp; cp - * (const EMRCREATEPEN *) pEMFR; MaptoGray(cp.lopn.lopnColor); rslt - PlayEnhMetaFileRecord(hDC. pHTable, (const ENHMETARECORD *) & cp, nObj); } break; case EMR_SETTEXTCOLOR: case EMR_SETBKC0L0R; { EMRSETTEXTCOLOR stc: Stc - * (const EMRSETTEXTCOLOR *) pEMFR;
Перечисление записей EMF 937 MaptoGray(stc.crColor); rslt = PlayEnhMetaFileRecorcKhDC. pHTable, (const ENHMETARECORD *) & stc. nObj); } break; default: rslt - PlayEnhMetaFileRecorcKhDC, pHTable. pEMFR. nObj); } return rslt; } }: Класс KGrayEMF является отдельным представителем целой категории классов преобразований, применяемых к EMF во время воспроизведения. Аналогичным способом можно изменить толщину пера в соответствии с параметрами графического устройства, заменить штриховые узоры, слишком мелкие для печати на принтере, исключить из документа все растровые вставки или отрегулировать цвета. Построение производных метафайлов Приемный контекст устройства функции PlayEnhMetaFile (а следовательно, и метода KEnumEMF: :EnumEMF) может быть любым допустимым контекстом, в том числе и метафайловым. Если вызвать PlayEnhMetaFile для контекста устройства EMF и передать специально написанную функцию косвенного вызова, вы фактически будете управлять процессом создания нового расширенного метафайла на основе записей существующего метафайла. Использовать подобные возможности всегда очень интересно, хотя наряду с новыми знаниями вас ждет немало сюрпризов. Ниже приведены функция FilterEMF и используемая ею вспомогательная функция MaplOumToLogical. void MaplOumToLogical(HDC hDC. RECT & rect) { POINT * pPoint = (POINT *) & rect; // Перейти от единиц 0,01 мм к пикселам текущего устройства for (int i=0; i<2; i++) { int t - GetDeviceCaps(hDC. HORZSIZE) * 100; pPoint[i].x = ( pPoint[i].x * GetDeviceCaps(hDC. HORZRES) + t/2 ) / t; t - GetDeviceCaps(hDC. VERTSIZE) * 100; pPoint[i].y = ( pPoint[i].y * GetDeviceCaps(hDC. VERTRES) + t/2 ) / t; } // Преобразовать в логическую систему координат DPtoLPChDC. pPoint, 2); }
938 Глава 16. Метафайлы HENHMETAFILE Fi1terEMF(HENHMETAFILE hEmf. KEnumEMF & filter) { ENHMETAHEADER emh; GetEnhMetaFileHeader(hEmf, sizeof(emh), &emh); RECT rcFrame; memcpy(& rcFrame, & emh.rclFrame, sizeof(RECT)); HDC hDC = QuerySaveEMFFileCFiltered EMF\0". & rcFrame. NULL); if ( hDO-NULL ) return NULL; MaplOumToLogicaKhDC, rcFrame): filter.EnumEMF(hDC. hEmf. & rcFrame); return CloseEnhMetaFile(hDC); } Функция FilterEMF строит новый метафайл на основании данных существующего метафайла. Процесс преобразования полностью контролируется экземпляром KEnumEMF или производного класса. Кадр нового метафайла определяется прямоугольником кадра исходного метафайла, преобразованным в логическую систему координат нового метафайла. Это гарантирует, что новый метафайл имеет те же размеры и те же отступы, что и исходный. Преобразование кадра выполняется вспомогательной функцией MeplOumToLogical. Например, если в качестве фильтра используется класс KGrayEMF, функция FilterEMF преобразует метафайл в оттенки серого цвета (если в нем отсутствуют цветные растры). Новый метафайл состоит из тех же объектов, но окрашивается в другие цвета. Однако в приведенном примере новый метафайл увеличивается на 424 байта и в нем появляется 26 дополнительных записей. Ниже приведена расшифровка новых записей EMF, полученная при помощи класса KTraceEMF. 2: SaveDC(hDC); 3: SetLayout(hDC. 0); 4: SetMetaRgn(hDC); 5: SelectObject(hDC. GetStockObjet(WHITE_BRUSH)); 6: SelectObject(hDC. GetStcckObjet(BLACK_PEN)); 7: SelectObjecUhDC. GetStockObjet(DEVICE_DEFAULT_FONT)); 8: SelectPalette(hDC. (HPALETTE)GetStockObjet(DEFAULT_PALETTE). TRUE); 9: SetBkColor(hDC. RGB(OxFF.OxFF.OxFF)); 10: SetTextColor(hDC, RGB(O.O.O)): 11: SetBkMode(hDC. OPAQUE): 12: SetPolyFillMode(hDC, ALTERNATE); 13: SetR0P2(hDC. R2_C0PYPEN): 14: SetStretchBltMode(hDC, STRETCH_ANDSCANS); 15: SetTextAlign(hDC. TAJIOUPDATECP | TA_LEFT | TAJOP); 16: SetBrushOrgEx(hDC. 0, 0. NULL); 17: SetMiterLimitChDC. 0.00000); 18: // Unknown record [120] 19: MoveToEx(hDC, 0. 0. NULL): 20: SetWorldTransform(hDC. 1, 0. 0. 1, 0, 0); 21: ModifyWorldTransform(hDC. 1, 0, 0. 1, 0, 0. 0x4 /*Unknown*/); 22: SetLayout(hDC. 0);
Перечисление записей EMF 939 23: // GdiComment(52, GDIC. 0x2) //GdiComment(8, GDIC, 0x3) RestoreDC(hDC, -1); DeleteObject(hObj[l]); Delete0bject(h0bj[2]); В этом листинге приведен точный список действий, выполняемых GDI при воспроизведении EMF. Поскольку воспроизведение происходит в метафайло- вом контексте, все операции не просто незаметно выполняются, а фиксируются в виде записей EMF. Подробнее о воспроизведении EMF Давайте проанализируем все операции, выполняемые GDI при воспроизведении EMF. Сначала GDI сохраняет текущее состояние контекста устройства функцией SaveDC. Напоследок прежнее состояние контекста восстанавливается функцией RestoreDC, поэтому сторона, вызвавшая PlayEnhMetaFile или EnumEnhMetaFile, не заметит никаких изменений в контексте устройства. Далее вызывается редко используемая функция SetMetaRgn. Напомню, что ме- тарегион представляет собой атрибут контекста устройства, который в сочетании с регионом отсечения обеспечивает двухуровневый контроль над отсечением. Функция SetMetaRgn преобразует текущий регион отсечения, заданный приложением перед вызовом EnumEnhMetaFile, в метарегион и сбрасывает регион отсечения в NULL. В процессе воспроизведения EMF регион отсечения в EMF может интерпретироваться как регион отсечения, но при фактическом выводе он всегда объединяется с метарегионом. Перед нами пример очень умного использования метарегионов — впрочем, не исключено, что метарегионы были разработаны как раз для воспроизведения EMF. Обратите внимание: ни прямоугольник кадра, ни прямоугольник воспроизведения не имеют ничего общего с отсечением. Десяток записей, следующих за вызовом SetMetaRgn, понять несложно. Все эти записи присваивают атрибутам контекста устройства значения по умолчанию, чтобы отделить воспроизведение EMF от текущих значений атрибутов. Записи EMF, соответствующие вызовам SetWorldTransform и ModifyWorldTransform, иногда бывают очень интересными. Поскольку мы выполняем все необходимые вычисления в FilterEMF, обе записи содержат матрицы тождественных преобразований. При действующих преобразованиях смещения или масштабирования матрицы изменились бы соответствующим образом. Как ни странно, в EMF отсутствует команда переключения в расширенный графический режим. Из выходных данных класса KTraceEMF (см. предыдущий пример) мы знаем, что GDI изменяет графический режим при воспроизведении EMF. Существует лишь одно разумное объяснение: разработчики хотели, чтобы новый метафайл можно было использовать и в Windows 95/98. Вообще-то Windows 98 GDI тоже создает записи EMF для мировых преобразований, но во время воспроизведения используется режим отображения MM_ANISOTROPIC. При воспроизведении EMF с использованием мировых преобразований особое внимание следует обращать на текст. Как говорилось выше, в совместимом графическом режиме текст не переворачивается даже в том случае, если в логической системе координат ось у направлена в противоположном направлении. В расширенном графическом режиме текст выводится в соответствии с направ-
940 Глава 16. Метафайлы лением осей, как и все остальные графические примитивы. В Windows NT/2000 проблема решается изменением мировой системы координат перед вызовом текстовых функций. В EMF могут использоваться любые режимы отображения. Кстати, обратите внимание на передачу недокументированного флага 4 при вызове ModifyWorldTransform. Три последних записи EMF восстанавливают прежнее состояние контекста устройства и удаляют два объекта GDI. Похоже, в исходном метафайле обнаружилась утечка ресурсов. Впрочем, ее причины кроются не в моем коде, а в действиях GDI при построении исходного метафайла. Моя программа получала две кисти системных цветов вызовами GetSysColorBrush. Поскольку кисти возвращаются из таблицы, принадлежащей USER32.DLL, они должны удаляться самим приложением. Кисти системных цветов ассоциируются с идентификатором процесса, равным 0, что позволяет совместно использовать их на уровне системы. Однако цвета системных кистей являются аппаратно-зависимыми, поэтому при построении EMF GDI преобразует их в обычные однородные кисти. При построении исходного метафайла GDI забывает удалить эти кисти. Остается рассмотреть еще две разновидности записей: недокументированные записи EMF и записи GdiComment. Недокументированные типы записей EMF На стадии инициализации контекста используется недокументированный тип записи EMF, определяемый в WINGDI.H с идентификатором EMRRESERVED120. Функция EnumEnhMetaFile позволяет легко узнать, что делает эта запись. Для этого следует включить проверку EMRRESERVED120 в функцию косвенного вызова, передать запись GDI функцией PlayEnhMetaFileRecord, установить точку прерывания и проанализировать несколько ближайших ассемблерных команд. Оказывается, функция PlayEnhMetaFileRecord проверяет, входит ли тип записи EMF в допустимый интервал (от EMRMIN до EMR_MAX), и вызывает функцию из таблицы. Для EMRRESERVED120 вызывается функция bPlay:: MRSETTEXTJUSTIFICATION, которая, в свою очередь, вызывает SetTextJustification. Смысл вызова этой функции неясен, поскольку при выводе текста используются явно заданные межсимвольные интервалы. Ниже приведен полный список недокументированных типов записей EMF. EMR_RESERVED_105 ESCAPE EMR_RESERVED_106 ESCAPE EMR_RESERVED_107 STARTDOC EMR_RESERVED_108 SMALLTEXTOUT EMR_RESERVED_109 FORCEUFIMAPPING EMR_RESERVED_110 NAMEDESCAPE 117 не используется EMR_RESERVED_119 SETLINKEDUFIS EMR_RESERVED_120 SETTEXTJUSTIFICATION GDIComment Специальная функция GDIComment предназначена для включения в EMF комментариев (дополнительных данных). Комментарий может содержать любую приватную информацию, известную обеим сторонам (читающей и записывающей). Например, приложение может включить в EMF PostScript-версию представлен-
EMF как средство программирования 941 ного объекта. Осведомленный получатель данных может воспользоваться данными PostScript вместо того, чтобы заниматься расшифровкой команд GDI. Microsoft документирует несколько стандартных комментариев GDI; все они начинаются с 32-разрядного идентификатора GDICOMMENTJDENTIFIER, который в текстовом виде соответствует цепочке символов «GDIC». Например, GDICOMMENT_ WINDOWS_METAFILE присоединяет к EMF данные в старом формате метафайлов Windows. Комментарий GDICOMMENTBEGINGR0UP сообщает о начале группы записей, а комментарий GDICOMMENT_ENDGROUP — о ее завершении. В нашем примере встречаются комментарии GD I COMMENTBEG INGR0UP (2) и GDI - COMMENTENDGROUP (3), которые отделяют вспомогательные записи, добавленные GDI, от исходных команд, переданных приложением. Построение новых метафайлов на базе существующих имеет множество нетривиальных практических применений. Например, к записям EMF могут применяться многочисленные оптимизации — исключение команд создания ненужных объектов или присваивания ненужных значений, а также удаление неиспользуемых частей растров. Возможны и другие применения — например, создание специальных эффектов с использованием не аффинных преобразований. В следующем разделе мы рассмотрим еще одно интересное применение — построение кода С по данным EMF. EMF как средство программирования Итак, мы выяснили, что EMF представляет собой графический объект, который может использоваться для кодировки, передачи и воспроизведения графических данных между разными приложениями, устройствами и операционными системами. Однако возможность записи команд GDI в одном удобном объекте находит и другие интересные применения. В этом разделе рассматриваются возможности применения EMF не только в области компьютерной графики, но и при программировании. Декомпилятор EMF В одном из разделов этой главы был представлен класс KEmfDC, выводящий расшифрованные записи EMF в виде иерархического дерева или в текстовое окно. На самом деле расшифровка записей EMF — дело второстепенное. Класс KEmfDC предназначен для декомпиляции EMF во фрагменты кода С. Компиляция и выполнение этих фрагментов приводит к тому же результату, что и воспроизведение EMF. Правила преобразования простых записей EMF в программный код С определяются особыми строковыми шаблонами. Рассмотрим несколько примеров. const Emrlnfo Pattern [] = { { EMRJETWINDOWEXTEX. "SetWindowExtTex(hDC, *d. Xd. NULL);" }, { EMRJXTCREATEFONTINDIRECTW. ,,#o=CгeateFont(XdДdДdДd.XdДbДbДbДbДbДbДbДbДS);,, }. { EMR_SETPIXELV . "SetPixeWhDC. *d. *d. #с;Г },
942 Глава 16. Метафайлы { EMR_POLYGON . "\nstatic const POINT Points Jn[]=#P;\n" "Polygon(hDC. Points Jn, %A\" }. }: Первый шаблон означает, что запись EMR_SETWINDOWEXTEX преобразуется в вызов SetWindowExtEx с двумя 32-разрядными целыми параметрами, значения которых берутся из записи EMF. Во втором примере запись EMR_EXTCREATEFONTINDIRECTW декомпилируется в команду присваивания результата CreateFont в запись таблицы объектов EMF. Список параметров CreateFont состоит из пяти 32-разрядных значений, восьми 8-разрядных значений и строки. В третьем примере запись EMR_SETPIXELV преобразуется в вызов функции SetPixelV, которой передаются два 32-разрядных целых параметра и дескриптор цвета. В последнем примере (EMR_ POLYGON) создается статический массив для хранения массива POINT переменного размера, используемого при вызове функции Polygon. Более сложные записи EMF, которые не удается преобразовать по шаблонам, обрабатываются специальными фрагментами кода. Растры сохраняются в отдельных файлах, которые затем подключаются в виде ресурсов. EMF преобразуется в функцию OnDraw, которой при вызове передается манипулятор контекста устройства. После добавления небольших фрагментов кода получается простая, но вполне законченная программа, которая создает простое окно и обрабатывает сообщение WM_PAINT для вывода декомпилированного метафайла. Ниже приведен пример вывода декомпилятора. Как видите, наша программа находит одинаковые растры и задействует одну копию (два вызова StretchDIBits используют один и тот же растр). void OnDrawCHDC hDC) { HGDIOBJ h0bj[5] = { NULL }; MoveToEx(hDC, 300. 50. NULL); LineTo(hDC. 350. 50); Rectangle(hDC. 300. 60. 349. 109); Ellipse(hDC. 410. 60. 459. 149); PatBl t(hDC.300.150.100.100.BLACKNESS); h0bj[l]=CreateSolidBrush(RGB(0x59.0x97.0x64)): SelectObject(hDC.hObj[1]); PatBlt(hDC.300.300.100.100.PATCOPY); SelectObjectChDC. GetStockObject(WHITE_BRUSH)); static const POINT Points_l[]={ 10. 200. 50. 200. 90. 200. 130. 200 }; PolylineChDC. PointsJL. 4); static KDIB Dib_l; Dib_l.Load(IDB_BITMAPl);// 350x250x8 Dib_l.StretchDIBits(hDC. 10.10.350,250. 0.0.350.250. DIB_RGB_COLORS.SRCCOPY); Dib_l.StretchDIBits(hDC. 10.270.350.250. 0.0.350.250. DIB RGB COLORS.SRCCOPY);
EMF как средство программирования 943 Возникает вопрос — кому и зачем нужно декомпилировать EMF в код C/C++? По многим причинам. По мере усложнения программ (особенно если разные компоненты разрабатываются разными группами или даже компаниями) становится очень трудно анализировать код на системном уровне. У инженера имеется множество приборов, помогающих ему разобраться в поведении системы; в распоряжении программиста только отладчики, средства отслеживания API и команды трассировки. При графическом выводе в EMF регистрируются все вызовы GDI, поступившие от разных компонентов системы. Декомпиляция EMF в более наглядный код С упрощает диагностику возникающих затруднений и поиск возможных решений. Декомпилированный код также помогает находить лишние вызовы GDI и неэффективные конструкции, а кроме того, выявлять те возможности GDI, которыми по разным причинам лучше не пользоваться. Диагностика проблем с печатью затруднена тем, что окончательный результат обеспечивается тесным взаимодействием приложения, GDI и драйвера принтера. Большинство драйверов принтеров поддерживают спулинг в формате EMF. Сохраните метафайл, отправленный на принтер, декомпилируйте его и проанализируйте результат — обычно это помогает решить многие проблемы с печатью. Метафайл можно рассматривать как своего рода «срез» программы, поскольку в нем сохраняются только команды графического вывода в контексте устройства, а все остальные функции просто выполняются без сохранения. Например, если вы используете функцию GetGlyphOutline для получения контуров глифов при выводе объемного текста в контексте EMF, в метафайле сохраняются только итоговые вызовы графических функций. Это бывает полезно, если вы хотите пропустить длительные вычисления и воспользоваться окончательными данными для повышения эффективности вывода. Декомпилированный метафайл упрощает обработку предварительно обработанных данных в приложениях. Так, в некоторых приложениях для изменения привычной прямоугольной формы окна используются данные сложных регионов, построенных заранее по растровым изображениям или векторным объектам. Некоторые графические функции API применяют графические данные, сгенерированные GDI. Скажем, DirectDraw не работает с объектами регионов GDI напрямую, но задействует структуру REGIONDATA для определения отсечения. Декомпиляция EMF также может принести немалую пользу в области оптимизации. Мы знаем, что процесс построения EMF в основном сводится к простой записи команд. Я встречал всего одну разновидность оптимизации — объекты не записываются в EMF, пока они не выбраны в контексте устройства. Кроме того, в некоторых ситуациях усекаются неиспользуемые части растров. В приложениях, критичных по размерам или быстродействию, некоторые проблемы решаются ручной оптимизацией декомпилированных метафайлов. Сохранение EMF-файла спулера Помимо обмена графическими данными между приложениями, метафайлы также используются для работы с заданиями печати в системах Win32. Принтеры обычно работают медленно — гораздо медленнее современных компьютеров. Когда приложение начинает печать, вместо того чтобы заставлять приложение дожидаться ее физического завершения, спулер Windows с помощью GDI пере-
944 Глава 16. Метафайлы сылает все графические запросы от приложения в метафайл. Этот процесс называется спулингом (spooling). Окончание спулинга завершает печать с точки зрения приложения. Пользователь продолжает работу с приложением, а спулер воспроизводит EMF-файл и передает команды драйверу принтера. В Windows 95/98 файлы спулера сохраняются в стандартном формате EMF. Каждая страница печатаемого документа записывается в отдельный файл. Обычно файлы спулера хранятся в каталоге временных файлов Windows с именами вида ^emfxxxx.tmp. Завершив построение страницы EMF, спулер вызывает функцию GDI gdi PI aySpool Stream для передачи страницы драйверу принтера. Точнее говоря, страница сначала пересылается процессору печати, который после необходимой обработки передает задание драйверу принтера. Процесс спулера называется «Spooler Process» и создает окно с именем класса «SpoolProcessClass». При желании внешняя программа может легко найти скрытое окно, созданное процессом спулера, установить для него перехватчик (hook) и подключить свою библиотеку DLL к работе спулера. Эта DLL может перехватывать вызовы gdi - PI aySpool Stream (приемы отслеживания и вмешательства в работу API описаны в главе 4 этой книги). Функция gdi PI aySpool Stream получает файл задания спулера, объединяющий все страницы задания печати в формате EMF. Хотя формат файла задания спулера не документирован, найти в нем имена файлов не так уж трудно. Таким образом, в Windows 95/98 мы можем подключиться к процессу спулера и получить имена всех EMF-файлов спулера перед тем, как они будут переданы драйверу принтера. Зная имя файла, вы можете скопировать файл в формате EMF и делать с ним все, что пожелаете. Спулер Windows NT/2000 работает несколько иначе. Он не создает скрытого окна, а обычные способы внедрения DLL не подходят, поскольку процесс спулера является системным. Даже файлы спулинга устроены иначе. Графические команды всего задания хранятся в одном файле, который не соответствует стандартному формату EMF. Для каждого задания печати спулер создает два файла. Файл с расширением .SHD содержит параметры, а файл с расширением .SPL — графические команды. К счастью, спулер Windows NT/2000 позволяет сохранять файлы после вывода задания печати для повторного использования, поэтому мы сможем получить файлы и без подключения к процессу спулера. По умолчанию файлы спулера создаются в каталоге SystemRoot\system32\spool\printers. Формат файлов спулера Windows NT/2000 можно описать как «формат ме- та-EMF». Файл состоит из последовательности метазаписей, начинающихся с 32-разрядного идентификатора типа и 32-разрядного размера, после которых следуют данные переменного размера. Данные EMF каждой страницы задания печати передаются с одной из этих метазаписей. Подобная архитектура позволяет хранить все задание печати в одном файле спулера. На рис. 16.7 показано окно утилиты EmfScope, предназначенной для сохранения и вывода файлов спулера. В Windows 95/98 EmfScope автоматически перехватывает файлы спулера и отображает их в своем окне по мере поступления. В Windows NT/2000 EmfScope работает с сохраненными файлами спулера. Утилита позволяет изменить масштаб вывода или прокрутить EMF-файл в окне. На рисунке изображена уменьшенная тестовая страница принтера.
Итоги 945 мШШ С \00005 spl 'ЧКЛккжте: ■2X0 Congrat.ulat.ions ! Windows 2000 Printer Test Page IX jO Рис. 16.7. Окно утилиты EmfScope с перехваченным EMF-файлом спулера Как упоминалось выше, диагностика проблем печати обычно сильно затрудняется тем, что в процессе печати в равной степени участвуют приложение, GDI и драйвер принтера. Теперь вы можете получить EMF-файл спулера, вывести его на экран, выбрать нужный масштаб, прокрутить и даже декомпилировать в код С. Конечно, это значительно упрощает поиск возможных неполадок с печатью. Например, если нужный объект отсутствует в файле, драйвер принтера здесь явно ни при чем, и проблемы следует искать в приложении или в GDI. Если для документа генерируется непропорционально большой EMF-файл, вероятно, это связано с неэффективным представлением каких-то команд GDI, поэтому причину следует искать в декомпилированном коде. Если же EMF нормально выглядит на экране, но печатается неверно, скорее всего, это связано с ошибками драйвера принтера. Итоги Эта глава посвящена метафайлам — чрезвычайно полезному средству графического программирования GDI. К сожалению, литература по программированию для Windows обычно не уделяет должного внимания метафайлам. Мы познакомились с основными концепциями двух форматов метафайлов и простыми примерами их практического применения, подробно изучили внутреннее строение метафайлов, рассмотрели процесс перечисления записей EMF, возможность де- компиляции EMF в код С. В завершение были рассмотрены способы сохранения EMF-файлов спулера.
946 Глава 16. Метафайлы Формат расширенных метафайлов Windows в основном разрабатывался для обмена графическими данными между приложениями или устройствами, чтобы изображения могли воспроизводиться с сохранением размеров и цветов. EMF хорошо справляется с этой задачей — настолько хорошо, что этот формат широко используется при печати. Однако формат EMF не позволяет обмениваться графическими данными для других целей — в частности, он не поддерживает редактирование внедренных объектов, поскольку в метафайле вместо высокоуровневых описаний объектов хранятся графические команды GDI. Последний раздел этой главы, посвященный использованию EMF в работе спулера, естественно приводит нас к теме следующей главы — печати. Дополнительная информация Другим распространенным метафайловым форматом является формат CGM (Computer Graphics Metafile). Он разрабатывался под покровительством ISO и ANSI как общий формат независимого от платформы обмена растровыми и векторными данными. Информацию о CGM можно получить на web-сайте www. cgmopen.org. Microsoft Platform SDK содержит нетривиальный пример — редактор EMF. Программа находится в каталоге Samples\Multimedia\MetaFile\MfEdit. Программа расшифровки EMF также входит в MSDN. Примеры программ К этой главе прилагаются три программы, две больших и одна маленькая (табл. 16.2). Родовые функции и классы, относящиеся к работе с EMF, находятся в файлах EMF.H и EMF.CPP. Таблица 16.2. Программы главы 16 Каталог проекта Описание Samples\Chapt_16\EMF Программа иллюстрирует создание, загрузку и сохранение EMF, обмен данными через буфер обмена, расшифровку и различные способы перечисления записей, построение новых метафайлов на основе уже существующих и декомпиляцию EMF Samples\Chapt_16\test Тестовая программа для преобразования декомпилированного метафайла в автономную Windows-программу Samples\Chapt_16\EMFScope Сохранение и вывод EMF-файлов спулера
Глава 17 Печать Операционная система Windows заметно упростила печать по сравнению со старыми версиями DOS, в которых каждое приложение снабжалось собственным комплектом драйверов. Но даже с учетом дополнительных удобств Win32 API коммерческие приложения подняли стандарты качества на такую высоту, что поддержка печати в приложении требует от программиста немалых усилий. Проблемы с реализацией печати в приложениях всегда считались одной из причин популярности MFC (Microsoft Foundation Classes) — библиотеки, обеспечивавшей более удобную (хотя и не идеальную) инкапсуляцию средств печати Win32. В этой главе рассматривается архитектура системы печати Win32 и множество практических задач — подключение к принтеру, вывод в контексте устройства принтера командами GDI и печать простейших примитивов GDI. В примерах этой главы показано, как создать систему аппаратно-независимой многостраничной печати программного кода с выделением синтаксических элементов и как напечатать изображение в формате JPEG. Знакомство со спулером Средства печати Windows образуют довольно сложную подсистему общей графической системы. Хотя с точки зрения обычного пользователя или даже программиста сложность системы печати не столь очевидна, при возникновении нетривиальных проблем с печатью вам придется познакомиться с длинным списком компонентов системы печати и разобраться в их взаимодействиях. В разделе «Архитектура системы печати» главы 2 этой книги приведено подробное описание архитектуры подсистемы печати вместе со списком компонентов. Самыми очевидными участниками процесса печати являются пользовательские приложения, GDI, графический механизм Windows, драйвер печати и принтер. Впрочем, есть и другие, менее известные компоненты — процесс спулера, клиентская библиотека DLL спулера, провайдер печати, процессор печати, языковой монитор, монитор порта и драйвер ввода-вывода.
948 Глава 17. Печать В этом разделе мы не станем подробно рассматривать все перечисленные компоненты, а уделим основное внимание связи обычных приложений Windows с подсистемой печати. Процесс печати Обработка даже простых заданий печати в операционной системе Windows — дело далеко не простое. Весь процесс печати от исходного запроса на печать до ее завершения состоит из десятка с лишним этапов. Ниже приведено краткое описание этого процесса. 1. Приложение запрашивает у клиентской библиотеки DLL спулера Win32 (через функции спулинга или стандартные диалоговые окна) информацию о принтерах. Стандартные диалоговые окна являются удобной оболочкой для использования API спулера. 2. Клиентская библиотека DLL спулера Win32, которая является DLL пользовательского режима, предоставляет приложениям и пользователям интерфейс к драйверу печати. С ее помощью можно получить информацию о принтерах, заданиях печати, параметрах печати и т. д. 3. После инициирования процесса печати приложение передает системе задание печати, используя для этого команды GDI. В GDI существует несколько специальных функций, сообщающих о начале и завершении задания печати, а также о разбиении документа на страницы. 4. GDI получает от приложения команды вывода, записывает их в файл формата EMF (файл спулинга) и передает его процессу спулера. 5. Процесс спулера представляет собой системную службу, управляющую обработкой заданий печати. Клиентская библиотека DLL взаимодействует с процессом спулера через механизм вызова удаленных процедур (Remote Procedure Call, RPC). Спулер передает задание маршрутизатору (router) спулинга. 6. Маршрутизатор должен правильно выбрать провайдера печати для обработки задания. 7. Провайдер печати отвечает за передачу задания печати на компьютер с физически подключенным принтером (локальным или удаленным). Кроме того, он управляет очередью заданий печати и реализует функции API для запуска, остановки и перечисления заданий печати. Операционная система предоставляет локального провайдера печати, сетевого провайдера печати Windows, сетевого провайдера печати Novell и провайдера печати HTTP. Если принтер не подключен к локальному компьютеру, сетевой провайдер печати отвечает за пересылку задания по сети. Окончательная обработка задания осуществляется локальным провайдером печати, который передает задание процессору печати. 8. Процессор печати отвечает за преобразование файла, находящегося в очереди, в формат данных, который может непосредственно обрабатываться принтером. В Windows 2000 чаще всего используется процессор печати EMF, который является частью локального провайдера печати. 9. Процессор печати обращается к GDI с требованием передать EMF-файл спулера в контекст устройства драйвера физического принтера.
Знакомство со спулером 949 10. Графический механизм Windows обращается к драйверу принтера за реализацией графических команд GDI, переданных в контекст устройства принтера, и возможной поддержкой вывода со стороны драйвера. § 11. Драйвер принтера отвечает за преобразование графических примитивов уровня DDI (Device Driver Interface) в низкоуровневые данные в формате, приемлемом для принтера. Драйвер принтера возвращает низкоуровневые данные спулеру. 12. Спулер передает низкоуровневые данные языковому монитору, который отвечает за поддержку двустороннего канала обмена данными между спулером и принтером. Языковой монитор передает данные монитору порта. 13. Монитор порта обеспечивает коммуникационный канал между спулером и драйвером порта ввода-вывода, который работает в режиме ядра и обладает доступом к аппаратному порту ввода-вывода, поддерживаемому принтером. 14. Драйвер порта ввода-вывода пересылает данные с компьютера на принтер. Кроме того, он получает от принтера статусную информацию и возвращает ее монитору порта и языковому монитору. 15. Микрокод принтера, работающий на встроенном процессоре, получает данные из порта ввода-вывода принтера, восстанавливает и преобразует во внутренний формат. При этом он управляет бесчисленными механическими, электрическими и электронными компонентами принтера, обеспечивающими реальный вывод точек на листе бумаги. Принтер может быть оснащен мощным RISC-процессором, большим объемом памяти и даже жестким диском, использовать многозадачную операционную систему реального времени и т. д. Язык управления принтером Язык, на котором управляющий компьютер общается с принтером, называется языком управления принтером (printer control language). Низкоуровневые данные принтера (то есть данные, готовые к печати) выражаются на языке управления принтером. Разные принтеры работают на разных языках, поддерживаемых микрокодом принтера. Некоторые принтеры могут поддерживать несколько языков управления принтером, переключение между которыми осуществляется специальными командами. Языки управления принтером делятся на три основных категории. Выбор языка управления принтером относится к числу важнейших архитектурных решений, принимаемых при проектировании принтера. Язык определяет возможности, сложность и стоимость принтера, степень сложности драйвера, скорость печати и требования к ресурсам управляющего компьютера. Текстовые языки управления принтером В простейших языках управления принтером используется простой текст с ограниченным набором команд форматирования. Языками этой категории управляются классические барабанные принтеры, позволяющие выводить только текст без векторной или растровой графики. Ветераны еще помнят, что на таких принтерах растровую графику приходилось имитировать тщательно подобранными комбинациями букв, образующими различные оттенки серого. Такие принтеры
950 Глава 17. Печать используются и в наши дни для вывода длинных финансовых отчетов на фальцованной бумаге или специальных бланках. Практически любой принтер может работать в текстовом режиме. Например, в окне DOS-сеанса можно скопировать текстовый файл в порт LPT1, и этот файл будет передан на принтер в исходном текстовом виде. Растровые языки управления принтером Вторая категория языков управления принтерами работает с растрами определенных форматов. Большинство принтеров, представленных на современном рынке, относится именно к этой категории. Матричные принтеры, принтеры DeskJet и другие струйные принтеры, простейшие лазерные принтеры — невзирая на принципиальные различия, все они относятся к категории растровых. Данные в растровом языке управления принтером обычно преобразуются к разрешению и цветовому пространству принтера. Например, принтер HP DeskJet может получать данные с разрешением 600 dpi в формате «1-разрядный черный/ 2-разрядный CMY» — это означает, что каждый квадратный дюйм состоит из 600 х 600 пикселов, каждый из которых представлен 7 битами. Растровые данные принтера представляют собой последовательность строк развертки, сжатую в определенный формат и разделенную на последовательность команд. Растровый принтер получает данные в строго определенном порядке — от верхней части страницы к нижней. Когда объем накопленных данных позволит напечатать очередную полосу, принтер выводит их и переходит к приему новых данных. Процедура повторяется до завершения страницы. На растровых принтерах обычно устанавливается память меньшего объема, в которой помещаются растровые данные небольшой части страницы, и более простой микрокод. На растровых принтерах драйверу приходится выполнять всю работу по преобразованию графических примитивов, полученных от приложения, в растровое изображение на уровне отдельных полос. При помощи GDI драйвер принтера делит страницу на полосы. Графические команды каждой полосы воспроизводятся в растре, проходят полутоновую обработку, преобразуются в цветовое пространство принтера и формулируются на языке управления принтером. При альбомной ориентации страница делится на вертикальные полосы вместо горизонтальных, а растровые данные после воспроизведения поворачиваются на 90°. Драйверы растровых принтеров бывают довольно сложными, а преобразование команд GDI в растровые данные с высоким разрешением и высокой цветовой глубиной может быть сопряжено со значительными затратами ресурсов управляющего компьютера. Объем данных и время передачи данных на принтер значительно возрастают с увеличением разрешения. Ниже приведен пример данных на языке управления принтером PCL3, используемом принтерами семейства DeskJet. PCL_RESET "<1B>EM PJL_ENTER_PCL3GUI "<1B>@PJL ENTER LANGUAGE=PCL3GUK0D><0A>M PCL_RESET "<1B>E" CmdStartDoc "<1B>&u600D<1B>*o5W<0409000000>" PCL_USJ_ETTER: "<1B>&12AM PCL_MEDS0URCE_TRAY1 "<1B>&11H" PCL MEDS0URCE PRELOAD "<1В>&1-2Ни
Знакомство со спулером 951 PCL_MEDIA_PLAIN "<1В>&10М" PCL_PQ_NORMAL "<1В>*оОМ" PCL_CRD_K662_C334 ,,<1В>*д2би/<0204025802580002012С012С0004012С>и "<012С0004012С012С0004>И PCL_ORIENT_PORTRAIT M<1B>&100" CmdStartPage H<lB>&10E<lB>*p0y0X<lB>&10L<lB>*rlA" Raster Data "<1B2A62306D32393779326D313776AC00>" "<0103C0EA0003FFFC00C0EA000103C031>" "<3776AC000103C0EA0003FFFC00C0EA00>" CmdEndPage "<1В>*гС<0О" PJLJX IT_LANGUAGE " <1В>П - 12345X" Команды PCL начинаются со служебного символа из набора ASCII (0x1 В в синтаксисе С). Печатная страница в PCL начинается с десятка команд инициализации, сообщающих принтеру информацию о разновидности языка, размерах и источнике бумаги, типе носителя, качестве печати, ориентации и т. д. Далее идет основной блок закодированных растровых данных, после чего следуют завершающие команды. Язык описания страниц Языки третьей категории принимают текстовые данные и векторную графику наряду с растровыми данными. Такие языки управления принтерами позволяют описать страницу с использованием разнообразных геометрических форм, текста, цветов и операций вывода, напоминающих команды GDI. Обычно они называются языками описания страниц (Page Description Language, PDL). К этой категории относятся PostScript, PCL5 и PCL6 с поддержкой векторной графики. Принтеры, поддерживающие современные языки описания страниц, должны быть значительно более мощными, чем принтеры с растровыми языками. Применение геометрических примитивов для описания страниц означает, что порядок следования графических команд в потоке данных невозможно предсказать заранее. Объем памяти принтера должен быть достаточным для того, чтобы хранить полную страницу графических примитивов, отсортированных в порядке их появления на странице, воспроизвести их в растровом формате, произвести полутоновую обработку и отправить на печать. В сущности, принтеру поручаются функции, обычно выполняемые драйвером растрового принтера. Микрокод принтера также должен обеспечить вывод всего текста, для чего он либо использует шрифтовой картридж принтера, либо загружает шрифты TrueType с управляющего компьютера. Поддержка мощного языка описания страниц в некоторых отношениях упрощает драйвер принтера, поскольку снимает необходимость в построении изображения на управляющем компьютере. Можно предположить, что печать должна выполняться быстрее и с меньшими затратами ресурсов управляющего компьютера. Тем не менее отображение команд GDI на команды языка описания стра-
952 Глава 17. Печать ниц иногда становится очень сложной задачей. Ситуация усложняется поддержкой шрифтов устройств, заменой и загрузкой шрифтов. Ниже приведен пример файла PostScript, сгенерированного драйвером PostScript для принтера HP Color LaserJet. <1В> M2345X@PJL JOB @PJL ENTER LANGUAGE - POSTSCRIPT *!PS-Adobe-3.0 mi tie: Document UCreator: Pscript.dll Version 5.0 ^Orientation: Portrait nPageOrder: Special UTargetDevice: (HP Color LaserJet 8500) (3010.104) 1 ULanguageLevel: 3 nEndComments UlncludeResource: font TimesNewRomanPSMT F /F0 0 /256 T /TimesNewRomanPSMT mF /F0S53 F0 [83 0 0 -83 0 0] mFS F0S53 setfont 650 574 moveto (Printer Control Language)[47 28 23 41 23 37 28 21 55 42 41 23 28 42 23 21 50 37 41 41 41 37 4]xshow showpage (ЩРаде: 1]П) = HPageTrailer mrailer moundingBox: 12 12 600 780 UPages: 1 (n[LastPage]n) = UEOF <lB>M2345X(apjL EOJ <1В>П2345Х После длинного заголовка, макросов и определений начинается последовательное преобразование графических команд GDI в команды PostScript. Оператор setfont выбирает шрифт Times New Roman в качестве текущего, оператор moveto задает позицию вывода, оператор xshow предназначен для вывода текста с точным указанием позиции символов, а оператор showpage начинает печать страницы. Прямой вывод в порт Как упоминалось выше, низкоуровневые данные, сгенерированные драйвером принтера, передаются монитором порта драйверу порта ввода-вывода и в конечном итоге пересылаются на принтер. Монитор порта стандартными файловыми операциями Win32 открывает манипулятор для драйвера ввода-вывода и пере-
Знакомство со спулером 953 дает ему данные. Если приложение знает язык управления принтером, оно может сделать то же самое и общаться с принтером напрямую. Следующий фрагмент показывает, как передать файл в порт принтера. BOOL SendFile(HANDLE hOutput, const TCHAR * filename, bool bPrinter) { HANDLE hFile - CreateFile(filename. GENERIC_READ, FILE_SHARE_READ, NULL. OPENJXISTING. FILE_ATTRIBUTEJORMAL. NULL); if ( hFile==INVALID_HANDLE_VALUE ) return FALSE; char buffer[1024]: for (int size = GetFileSize(hFile. NULL); size; ) { DWORD dwRead « 0. dwWritten = 0; if ( ! ReadFile(hFile. buffer. min(size. sizeof(buffer)). & dwRead. NULL) ) break; if ( bPrinter ) WritePrinter(hOutput. buffer. dwRead. & dwWritten); else WriteFile(hOutput. buffer. dwRead. & dwWritten. NULL); size -= dwRead; } return TRUE; } void Demo_WritePort(void) { KFileDialog fd; if ( fd.GetOpenFileName(NULL. "prn". "Raw printer data") ) { HANDLE hPort - CreateFileClptl;", GENERIC_WRITE. FILE_SHARE_READ. NULL. OPENJXISTING. FILE_ATTRIBUTE_NORMAL. NULL); if ( hPort!=INVALID_HANDLE_VALUE ) { SendFile(hPort. fd.m_TitleName. false); CloseHandle(hPort); } } } Функция SendFile открывает манипулятор для входного файла и копирует блоки данных в выходной файл. Функция Demo_WritePort выводит диалоговое окно, в котором пользователь выбирает файл для передачи на принтер, создает манипулятор порта ввода-вывода и вызывает SendFile. Запись низкоуровневых данных на языке управления принтером часто требуется при адаптации DOS-программ, а также в приложениях, ограничиваю-
954 Глава 17. Печать щихся текстовым выводом или полагающих, что они справятся с работой лучше обычного драйвера принтера. Приложение также может сохранить низкоуровневые данные, сгенерированные драйвером принтера (печать в файл), загрузить их и передать прямо на принтер без участия GDI. В Windows NT/2000 порт LPT1: не соответствует обычному аппаратному порту, хотя вызов WriteFile проходит через ту же системную функцию, что и запись в настоящий порт. Утилиты типа WINOBJ (www.sysinternals.com) показывают, что LPT 1: представляет собой символическую ссылку на адрес Device\ NamedPipe\Spooler\LPTl. Оказывается, вывод все равно осуществляется под управлением спулера. Устройство, похожее на настоящий порт, называется NONSPOOLED_ LPT1 и представляет собой символическую ссылку на адрес \Device\ParallelO. Если спулер не работает (например, если он был завершен командой net stop spooler), попытки открыть LPT1: завершаются неудачей. Печать с использованием спулера Другой вариант вывода на принтер заключается в использовании API спулера. Win32 API включает богатый набор функций спулера, при помощи которых приложение может получить информацию о состоянии спулера, управлять заданиями печати, задавать параметры принтеров или передавать данные прямо на принтер. В приложениях Windows эти функции вызываются редко, поскольку доступ ко многим стандартным возможностям можно получить через стандартные диалоговые окна или приложение Printers (Принтеры) панели управления. По этой причине мы рассмотрим лишь некоторые функции спулера. BOOL OpenPrinterCLPTSTR pPrintName, LPHANDLE phPrinter, LPPRINTER_DEFAULTS pDefault): DWORD StartDocPrinter(HANDLE hPrinter, DWORD Level. LPBYTE pDodnfo); BOOL StartPagePrinter(HANDLE hPrinter); BOOL WritePrinter(HANDLE hPrinter, LPVOID pBuf. DWORD cbBuf. LPWORD pcWritten); BOOL EndPagePrinter(HANDLE hPrinter); BOOL EndDocPrinter(HANDLE hPrinter); BOOL ClosePrinter(HANDLE hPrinter); BOOL AbortPrinter(HANDLE hPrinter); Функция OpenPrinter получает имя принтера и указатель на структуру PRINTER_ DEFAULTS с параметрами по умолчанию, а возвращает манипулятор объекта принтера, поддерживаемого DLL спулера Win32. Обратите внимание: этот манипулятор не принадлежит ни к объектам ядра, ни к объектам GDI, поэтому он может использоваться только функциями спулера. В текущей реализации Windows NT/ 2000 манипулятор принтера представляет собой обычный указатель на адресное пространство пользовательского режима. Функция StartDocPrinter сообщает спулеру о появлении нового документа, который следует поставить в очередь печати. В последнем параметре этой функции передается указатель на структуру D0C_INF0_1 или D0C_INF0_2, определяющую название документа, имя выходного файла и тип данных для задания печати. DLL спулера создает новое задание печати функцией AddJob и файл спулинга в каталоге спулера. Функция StartDocPrinter используется в паре с функцией
Знакомство со спулером 955 EndDocPrinter, которая завершает задание печати, удаляет его из спулера и освобождает все выделенные ресурсы. Функция StartPagePrinter сообщает спулеру о начале новой страницы. После вызова этой функции приложение может передавать данные спулеру функцией WritePrinter. Каждому вызову StartPagePrinter должен соответствовать парный вызов EndPagePrinter, после которого начинается новая страница или завершается задание печати. Если произошла ошибка, задание печати можно отменить функцией Abort- Printer. Функции спулера экспортируются клиентской библиотекой DLL спулера Win32 winspool.drv, чтобы программы Windows могли взаимодействовать со спулером. Однако настоящая реализация спулера находится в отдельном системном процессе (spoolsv.exe). Клиентская библиотека DLL общается со спулером через механизм RPC. Например, функция StartPagePrinter вызывает RpcStartPagePrinter, которая в свою очередь вызывает NdrC1ientCan2. Функциями спулера можно воспользоваться и для отправки низкоуровневых данных на принтер. Впрочем, полная поддержка спулинга позволяет сделать много больше. Выражаясь точнее, вы можете передавать любые данные при условии, что они поддерживаются процессором печати, который отвечает за обработку данных. Для проверки форматов данных, поддерживаемых процессором печати для конкретного драйвера принтера, откройте окно свойств принтера и перейдите на вкладку Advanced (Дополнительно). На рис. 17.1 показано, какие форматы поддерживаются для стандартного процессора печати Windows 2000. Advanced * AvaSabi© ftone Гг 11<МХ1ЛМ ,f 2Ш ЗййкОД * differ^ р№ ргшж? may result to d#f <srer& epttom hm$ r*™ ■■■,, -v sjwtabl»fardtimfctUttto/pm*Ifywrтнкшdmwfcq*dfy$utafcyp* . Drfvac J HP DeskJet ^*1ШтЫ1т*ЛЫитй^ * % * * ^'^; Sport ^Ашш д/ \Г;8ш primal 4 ■■■■"■■■■■^■-■^--•^-■-■•■-■* r ■ PrlfKfc ргвсшоп ;,; Шшк datafyptt SFMPSPRT RAW [FF appended] RAW [FF auto] NT EMF 1,003 NT EMF 1.006 NT EMF 1,007 NT EMF 1,008 TEXT К^УЫщтЫШ\ху ^'\v; */л\'< "«<"'/,,, -,/, , t 4<J'f/J;_* л 'y*>-'J, Ap* un&M&ev&mxi ' *''. *..'.*'*..'.'<. ..?"*' : / ' ' ' ;:'' * l?ft^Dtf«tftfc„'Г 'Шиши**» ' BtiwatorFwfcM J *•'' tmmmtmmmmttm» w n щит mmmmmtmrnmimm щтт. mmmnnu mmmt ii4Niri-iii-i-iriiiiinn 1 i f? Рис. 17.1. Типы данных, поддерживаемые стандартным процессором печати Windows
956 Глава 17. Печать Как видно из рисунка, стандартный процессор печати Windows 2000 поддерживает три категории типов данных: низкоуровневые, текстовые и NT EMF. Хотя в списке присутствуют четыре версии NT EMF, точные спецификации и различия между ними не документированы. Также следует обратить внимание на то, что имя WinPrint не соответствует физической библиотеке DLL. Стандартный процессор печати Windows 2000 является частью локального процессора печати (localspi.dll), о чем свидетельствуют имена экспортируемых функций — такие, как PrintDocumentOnPrintProcessor. Небольшой эксперимент убедит всех скептиков в том, что документированный интерфейс API спулера благополучно справляется с EMF-файлами. Следующая функция иллюстрирует использование функций спулера с EMF-файлами. void Demo_WritePrinter(void) { PRINTDLG pd; memset(&pd. 0, sizeof(PRINTDLG)); pd.lStructSize = sizeof(PRINTDLG); if ( PrintDlg(&pd)==ID0K ) { HANDLE hPrinter; DEVM0DE * pDevMode = (DEVM0DE *) Global Lock(pd.hDevMode); PRINTER_DEFAULTS prn; prn.pDatatype = "NT EMF 1.008"; prn.pDevMode = pDevMode; prn.DesiredAccess = PRINTER_ACCESS_USE; if ( 0penPrinter((char *) pDevMode->dmDeviceName, & hPrinter, & prn) ) { KFileDialog fd; if ( fd.GetOpenFileName(NULL, "spl", "Windows 2000 EMF Spool file") ) { D0CJNF0J docinfo: docinfo.pDocName = "Testing WritePrinter"; docinfo.pOutputFile = NULL; docinfo.pDatatype = "NT EMF 1.008"; \StartDocPrinter(hPrinter, 1, (BYTE *) & docinfo); StartPagePrinter(hPrinter); SendFile(hPrinter. fd.m_TitleName, true); EndPagePrinter(hPrinter); EndDocPrinter(hPrinter); }
Знакомство со спулером 957 ClosePrinter(hPrinter); } if ( pd.hDevMode ) GlobalFree(pd.hDevMode); if ( pd.hDevNames ) GlobalFree(pd.hDevNames); } } Функция Demo_WritePrinter создает стандартное диалоговое окно для выбора принтера, на котором будет производиться печать. Если в диалоговом окне был выполнен щелчок на кнопке ОК, в структуру PRINTDLG заносится глобальный манипулятор блока структуры DEVM0DE, содержащей все параметры печати. Манипулятор DEVM0DE преобразуется в указатель и используется для заполнения полей структуры PRINTER_DEFAULTS, передаваемой функции OpenPrinter. Если вызов OpenPrinter прошел успешно, программа предлагает пользователю выбрать EMF- файл спулера Windows NT/2000. Затем функция вызывает StartDocPrinter, указывает тип данных «NT EMF 1.008» и пересылает содержимое EMF-файла функцией SendFile (см. выше). Если все прошло нормально, EMF-файл передается драйверу заданного принтера и выводится на печать. Ключом к успешной работе Demo_WritePrinter является правильный формат EMF-файла спулера. В главе 16, посвященной EMF, упоминались способы получения EMF-файла спулера — при помощи утилиты EMFScope в Windows 95/98 или команды сохранения файла спулера в Windows NT/2000. Также были представлены инструменты декодирования EMF-файлов вообще и EMF-файлов спулера в частности. Этот прием позволяет приложениям передавать низкоуровневые данные печати или данные спулинга в формате EMF на принтер без прямого участия GDI. В сочетании с возможностью получения EMF-файлов от спулера появляется возможность заново использовать файл спулера без запуска исходного приложения, создавшего этот файл. Это может пригодиться при построении универсальных средств обработки документов, обеспечивающих единый механизм обработки выходных данных разных приложений. Учтите, что EMF-файл спулера должен быть совместим с принтером, на котором вы печатаете, поскольку при построении EMF-файла спулера в качестве эталона выбирается контекст драйвера принтера. Также обратите внимание на то, что в функции Demo_WritePrinter используется структура DEVM0DE с текущими настройками принтера. Правильнее было бы извлечь структуру DEVM0DE из SHD-файла, сгенерированного вместе с EMF-фай- лом. Дело в том, что некоторые поля структуры DEVM0DE могут измениться с момента последнего построения EMF-файла. Рассмотрим маленькую схему, которая помогает лучше представить, как реализован спулинг EMF-файлов. Если при создании нового задания GDI решает, что спулинг.ЕМР разрешен, то новое задание спулера в формате EMF создается аналогичным способом. Для каждого вызова функции GDI данные записи EMF- файла передаются спулеру при помощи WritePrinter. Функция StartDoc GDI вызывает StartDocPrinter, функция StartPage вызывает StartPagePrinter и т. д., а вывод из очереди организуется процессором печати. Конечно, это всего лишь упрощенная концептуальная схема. Мы вернемся к диалоговым окнам принтера и структуре DEVM0DE при описании печати средствами GDI.
958 Глава 17. Печать Процессор печати EMF При передаче данных спулинга в формате EMF функцией WritePrinter возникает интересный вопрос: что же именно делает процессор печати EMF? В разделе «Архитектура системы печати» главы 2 этой книги приведено довольно подробное описание работы процессора печати в контексте архитектуры системы печати Windows. А сейчас мы в общих чертах познакомимся с тем, что же делает процессор печати Windows 2000. Процессор печати представляет собой настраиваемый компонент архитектуры системы печати Windows, который создавался с расчетом на будущее. До появления Windows 2000 процессор печати почти не использовался из-за ограниченности его возможностей. Когда процессор печати получал от спулера задание в формате EMF, он просто передавал его драйверу принтера функцией GdiPlayEMF (gdoPlaySpool Stream в Windows 95/98) GDI. Объявление функции GdiPlayEMF в Windows NT 4.0 выглядит следующим образом: B00L GdiPlayEMFCLPWSTR pwszPrinterName. LPDEVM0DEW pDevmode. ELPWSTR pwszDocName, EMFPLAYPR0C prnEMFPlayFn, HANDLE hPageQuery); Как видите, процессор печати получает только имя принтера, DEVM0DE и имя документа. Все, что он может, — вывести несколько экземпляров документа многократным вызовом GdiPlayEMF. Последние два параметра предназначены для избирательного воспроизведения отдельных страниц EMF, но в Windows NT 4.0 эта возможность не поддерживается. В Windows 2000 возможности процессора печати были расширены. Теперь он может управлять воспроизведением страниц EMF, объединять несколько логических страниц на одной физической странице, изменять порядок печати страниц в документе и даже применять преобразования к логическим страницам. С учетом этих усовершенствований процессор печати Windows 2000 позволяет выводить несколько страниц на одной физической странице, печатать страницы в обратном порядке, выводить несколько копий каждой страницы, печатать брошюры и контуры страниц. Все эти возможности реализуются централизованно и легко расширяются без модификации GDI и драйверов принтеров. Доступ к новым возможностям процессора печати открывают новые функции, экспортируемые GDI. Ниже приведены объявления трех важнейших функций. HANDLE GdiGetPageHandle(HANDLE SpoolFileHandle. DWORD Page. LPDW0RD pdwReserved); BOOL GdiPlayPageEMF(HANDLE SpoolFileHandle, HANDLE hEmf. RECT * prectDocument, RECT * prectBorder); HDC GdiGetDCCHANDLE SpoolFi1eHandle); Функция GdiGetPageHandle позволяет процессору печати получить манипулятор конкретной страницы в EMF-файле спулера. Напомню, что этот манипулятор не является ни манипулятором объекта GDI или ядра, ни указателем на графические данные EMF. Он всего лишь может использоваться функцией GdiPlayEMF для воспроизведения одной страницы через драйвер принтера. Параметр prectDocument позволяет масштабировать логическую страницу EMF в часть физической страницы для печати нескольких страниц на одном листе или вывода брошюры. Необязательный параметр prectBorder определяет прямоугольник внешнего
Знакомство со спулером 959 контура страницы и упрощает визуальную разметку нескольких логических страниц на одной физической странице. Функция GdiGetDC возвращает процессору печати нормальный манипулятор контекста устройства GDI, который может использоваться для применения мирового преобразования перед воспроизведением EMF. В результате применения мировых преобразований процессор печати может поворачивать или выполнять зеркальное отражение страниц. Пример исходного текста процессора печати EMF включен в Windows NT 4.0/ 2000 DDK (каталог src\print\genprint). Специальные функции GDI для работы с процессором печати EMF документированы в DDK. Возможно, у вас возник вопрос — как же работает функция GdiPlayPageEMF? Если несколько логических страниц могут печататься на одной физической странице, значит, отдельные страницы EMF не могут воспроизводиться в контексте устройства (особенно для драйверов принтеров, использующих поддержку GDI для разбиения на полосы), поскольку для этого понадобился бы дополнительный уровень построения и воспроизведения EMF. Функция GdiPlayPageEMF не воспроизводит EMF в контексте устройства принтера; вместо этого она всего лишь сохраняет новую логическую страницу во внутренней структуре данных GDI. Воспроизведение нескольких логических страниц начинается лишь с вызовом GdiPlayPageEMF. При выполнении GdiEndPageEMF в отладчике обнаруживается любопытная динамика печати в GDI. Чтобы проследить за работой GdiEndPageEMF, подключите отладчик к процессу спулера в диспетчере задач, для чего следует щелкнуть правой кнопкой мыши на имени процесса спулера и выбрать в контекстном меню команду Debug. После подключения к процессу спулера установите точку прерывания по адресу _GdiEndPageEMF@8 в модуле gdi32.dll. Теперь можно запустить задание печати. Выясняется, что GdiEndPageEMF вызывает функцию StartPage GDI и внутреннюю функцию StartBanding, после чего в цикле вызывает функции InternalGdiPlayPageEMF и NextBand. Функция InternalGdiPlayPageEMF настраивает мировое преобразование и вызывает интересную функцию PrintBand. Функция PrintBand вызывает системную функцию NtGdiGetPerBandlnfo, соответствующую документированной точке входа драйвера принтера и передающую GDI информацию об очередной полосе. Затем PrintBand вызывает функцию PlayEnhMetaFile, которая и воспроизводит EMF в контексте устройства драйвера принтера. Где-то в этой схеме должен присутствовать цикл перебора логических страниц на физической странице. Вероятно, теперь вы гораздо лучше понимаете, почему EMF отводится центральное место в системе печати, особенно в Windows 2000. Перечисление принтеров В спулерный интерфейс Win32 API входит функция EnumPrinter, при помощи которой приложение может получить список принтеров и запросить информацию о принтере. Функция EnumPrinter весьма сложна, она имеет большое количество параметров и возвращает разные типы структур. С ее помощью можно получить списки локальных принтеров, провайдеров печати, имен доменов, а также всех принтеров и серверов печати в домене. Функция EnumPrinter заполняет
960 Глава 17. Печать массивы структур от PRINTERINF01 до PRINTERINF05. Наиболее полная информация о принтере хранится в структуре PRINTERINF02, состоящей из 21 поля, в том числе из полей имени сервера, имени принтера, имени драйвера, DEVMODE, процессора печати, типа данных спулинга и т. д. За подробной информацией о EnumPrinter обращайтесь к MSDN. Ниже приведен пример функции, которая перечисляет все локальные принтеры и подключения к удаленным принтерам. void * EnumeratePrinters(DWORD flag, LPTSTR name, DWORD level, DWORD & nPrinters) { DWORD cbNeeded; nPrinters = 0; EnumPrintersCflag. name, level, NULL. 0, & cbNeeded. & nPrinters); BYTE * pPrnlnfo = new BYTE[cbNeeded]; if ( pPrnlnfo ) EnumPrinters(flag. name, level, (BYTE*) pPrnlnfo. cbNeeded. & cbNeeded. & nPrinters); return pPrnlnfo; } void ListPrintersCHWND hWnd, int message) { DWORD nPrinters; PRINTER_INF0_5 * pInfo5 = (PRINTER_INF0_5 *) EnumeratePrinters(PRINTER_ENUM_LOCAL, NULL, 5, nPrinters); if ( pInfo5 ) { for (unsigned i=0; i<nPrinters; i++) SendMessage(hWnd. message. 0, (LPARAM) pInfo5[i].pPrinterName); delete [] (BYTE *) pInfo5; PRINTERJNFOJ * plnfol - (PRINTERJNFOJ *) EnumeratePrinters(PRINTER_ENUM_CONNECTIONS. NULL. 1, nPrinters); if ( plnfol ) { for (unsigned i=0; i<nPrinters; i++) SendMessage(hWnd, message, 0. (LPARAM) pInfol[i].pName); delete [] (BYTE *) plnfol; Функция EnumeratePrinters представляет собой простую оболочку для Enum- Printers. Она управляет получением данных о размерах блока и выделением памяти. Функция ListPrinters сначала вызывает EnumeratePrinters для перечисления
Знакомство со спулером 961 локальных принтеров, а затем — для удаленных принтеров. Имена принтеров заносятся в список, в котором пользователь может выбрать нужный принтер. Перечисление принтеров позволяет приложениям конструировать нестандартные диалоговые окна печати. Например, стандартные диалоговые окна Windows могут оказаться неподходящими для игры или учебной программы, написанной для DirectX. Некоторые приложения должны предоставлять пользователю быстрый доступ к принтеру по умолчанию. Имя текущего принтера по умолчанию можно получить функцией GetDefaultPrinter, которая поддерживается только в Windows 2000. BOOL GetDefaultPrinter(LPTSTR pszBuffer, LPDWORD pcchBuffer); Получение информации о принтере По манипулятору принтера функция GetPrinter возвращает разнообразные сведения о принтере. Она может возвращать различные структуры от PRINTERINF01 до PRINTER_INF0_9. Приложение также может воспользоваться функцией DeviceCapabitilies и получить от интерфейсного модуля драйвера устройства сведения о лотках для подачи бумаги, поддержке печати по копиям, поддержке двусторонней печати, размере приватной части DEVMODE, допустимых размерах бумаги, скорости печати и т. д. За подробной информацией о функциях GetPrinter и DeviceCapabilities обращайтесь к MSDN. Настройка драйвера принтера Всевозможные параметры печати хранятся в структуре DEVMODE. Точнее говоря, структура DEVMODE используется всеми графическими устройствами для обмена информацией о конфигурации устройства между приложением, GDI и драйвером устройства. В частности, указатель на структуру DEVMODE передается в последнем параметре функций CreateDC и CreatelC. Впрочем, для принтеров структура DEVMODE играет более важную роль, чем для экранных устройств. Ниже приведено определение структуры DEVMODE. typedef struct _devicemode { BYTE dmDeviceName[CCHDEVICENAME]; WORD dmSpecVersion; WORD dmDriverVersion; WORD dmSize; WORD dmDriverExtra: DWORD dmFields; union { struct { short dmOrientation; short dmPaperSize; short dmPaperLength: short dmPaperWidth; }: POINTL dmPosition; }:
962 Глава 17. Печать short dmScale: short dmCopies; short dmDefaultSource; short dmPrintQuality; short dmColor; short dmDuplex; short dmYResolution; short dmTTOption; short dmCollate; BYTE dmFormName[CCHFORMNAME]; WORD dmLogPixels; DWORD dmBitsPerPel; DWORD dmPelsWidth; DWORD dmPelsHeight; DWORD dmDisplayFlags; union { DWORD dmDisplayFlags; DWORD dmNup; } DWORD dmDisplayFrequency: DWORD dmICMMethod: DWORD dmICMIntent; DWORD dmMediaType; DWORD dmDitherType; DWORD dmReservedl: DWORD dmReserved2: DWORD dmPanningWidth: DWORD dmPanningHeight; } DEVMODE: Структура DEVMODE весьма сложна, что объясняется несколькими причинами. Она имеет переменный размер, зависящий от версии Windows, а интерпретация ее полей продолжает изменяться. После открытых полей структуры DEVMODE драйвер устройства может разместить дополнительные параметры, используемые во внутренней работе драйвера, поэтому приложение должно запросить размер структуры DEVMODE и выделить память из кучи (вместо того, чтобы предположить фиксированный размер структуры и создать ее в стеке). Поле dmDeviceName содержит «пользовательское» имя принтера, выводимое в приложении Printers (Принтеры) панели управления. Учтите, что заданное имя усекается до 32 символов. Поле dmSpecVersion определяет версию структуры DEVMODE. В файле wingdi.h определен макрос DM_SPECVERSION для обозначения текущей версии, в настоящее время равной 0x401. В поле dmDriverVersion хранится внутренняя версия драйвера, назначенная разработчиком драйвера. Например, для драйверов семейства UniDriver в Windows 2000 используется версия 0x500. Поле dmSize определяет размер открытой части структуры DEVMODE в байтах. При создании новой структуры DEVMODE ему следует присвоить значение sizeof(DEVMODE). Но если структура DEVMODE получена от внешнего источника, не следует предполагать, что значение dmSize совпадает с sizeof(DEVMODE), поскольку вы можете откомпилировать программу с новейшим заголовочным файлом Win32 и запустить ее на старом компьютере со старым драйвером, поддерживающим предыдущую версию DEVMODE (или наоборот). Поле dmDriverExtra задает размер блока, используемого драйвером устройства для хранения закрытых данных после от-
Знакомство со спулером 963 крытых полей DEVMODE. Если закрытые поля отсутствуют, полю присваивается 0. Общий объем памяти для хранения структуры DEVMODE равен dmSize +dmDriverExtra. Поле dmFields содержит информацию об инициализированных полях. Разным полям соответствуют разные флаги; например, флаг DM0RIENTATI0N относится к полю dmOrientation. В остальных полях DEVMODE хранятся в основном параметры устройства. Поле dmPrintQuality обычно определяет качество печати, которое существенно влияет на внешний вид напечатанных страниц, скорость, размер данных принтера и т. д. Качество печати обычно задается стандартными макросами DMRESHIGH, DMRES_ MEDIUM, DMRES_L0W и DMRESDRAFT. Драйвер принтера обычно сообщает GDI разные разрешения в зависимости от текущего значения поля dmPrintQuality, от чего зависит объем данных, получаемых им от GDI. Качество печати также влияет на воспроизведение графических команд драйвером принтера. Фактические значения этих макросов лежат в интервале от -4 до -1. Вместо этих значений драйвер принтера может присвоить полю dmPrintQuality фактически используемое разрешение (за подробностями обращайтесь к MSDN). Функция DocumentProperties заполняет структуру DEVMODE по имени и манипулятору принтера. LONG DocumentProperties(HDC hWND. HANDLE hPrinter, LPTSTR pDeviceName, PDEVMODE pDevModeOutput. PDEVMODE pDevModelnput. DWORD fMode); Последний параметр fMode определяет операцию, выполняемую функцией. Если fMode = 0, функция возвращает общий размер открытых и закрытых полей DEVMODE. Если fMode = DM_0UT_BUFFER, то структура, на которую указывает параметр pDevModeOutput, заполняется текущими параметрами DEVMODE заданного драйвера. Если fMode = DMINBUFFER, параметр pDevModelnput указывает на структуру DEVMODE с новыми значениями параметров. Если fMode = DM_IN_PR0MPT, функция выводит окно свойств принтера, в котором пользователь может изменить текущую конфигурацию. Следующая функция GetDEVMODE показывает, как использовать функцию DocumentProperties. DEVMODE * GetDEVMODE(TCHAR * PrinterName, int nPrompt) { HANDLE hPrinter; if ( !OpenPrinter(PrinterName. ShPrinter, NULL) ) return NULL; // Если последний параметр равен нулю. // функция возвращает необходимый размер буфера int nSize = DocumentProperties(NULL, hPrinter, PrinterName, NULL. NULL, 0); DEVMODE * pDevMode = (DEVMODE *) new char[nSize]; if ( pDevMode—NULL ) return NULL; // Обратиться к драйверу с запросом // на инициализацию структуры DEVMODE
964 Глава 17. Печать DocumentProperties(NULL, hPrinter. PrinterName, pDevMode. NULL, DM_OUT_BUFFER); // Вывести страницу свойств, чтобы пользователь // мог внести необходимые изменения BOOL rslt = TRUE; switch ( nPrompt ) { case 1: rslt = AdvancedDocumentProperties(NULL, hPrinter, PrinterName, pDevMode, pDevMode) == IDOK; break; case 2: rslt = ( DocumentProperties(NULL, hPrinter, PrinterName, pDevMode. pDevMode. DM_IN_PROMPT | DM_OUT_BUFFER | DM_IN_BUFFER ) — IDOK ): break; } ClosePrinter(hPrinter); if ( rslt ) return pDevMode; else { delete [] (BYTE *) pDevMode; return NULL; } } Работа функции GetDEVMODE начинается с вызова функции DocumentProperties, возвращающей реальный размер структуры DEVM0DE. После выделения памяти функция DocumentProperties вызывается снова для получения текущих настроек DEVM0DE, заданных пользователем в панели управления. Последний параметр GetDEVMODE указывает, следует ли предложить пользователю изменить настройки печати на странице свойств драйвера принтера или ограничиться окном дополнительных настроек (Advanced). Функция AdvancedDocumentProperties также поддерживается клиентской библиотекой DLL спулера Win32. Другая взаимосвязанная функция, PrinterProper- ties, выводит страницу свойств заданного принтера. На рис. 17.2 показаны примеры окон, вызванных функциями DocumentProperties, AdvancedDocumentProperties и PrinterProperties. Все окна, показанные на рисунке, реализуются интерфейсной библиотекой DLL драйвера принтера. В Windows 95/98 драйвер принтера представляет собой 16-разрядную библиотеку DLL, загружаемую 16-разрядным модулем gdi.exe GDI. Пользовательский интерфейс и базовый драйвер могут находиться в одной библиотеке DLL. В Windows NT 4.0 базовый драйвер принтера реализуется в виде DLL режима ядра, а пользовательский интерфейс сосредоточен в DLL пользовательского режима, поэтому они всегда находятся в разных библиотеках DLL. Хотя в Windows 2000 система допускает существование драйверов принте-
Базовая печать средствами GDI 965 ров пользовательского режима, интерфейсная библиотека DLL все равно должна существовать отдельно от базового драйвера. (ЖШвё*^:. Л^^йрРЙ РадазР^&кя*- [2 31 до HP DeskJet 895Cxi Advanced Documenl Settings "♦. L^ Paper/Output _+ jij Graphic C-] &$ Document Options A^W^F^^fafc»** [Enabled 3 Color Printing Mode tu .< i\i\u< ■; ^j Printer Features Print Mode <*: ч> > Print Quality \ст% «ход Nfr HP DeskJet 895Cxi Device Settings - ^5 Form To Tray Assignment Manual Paper Feed v-tr. Envelope, Manual Feed ч«»|- -33 Рис. 17.2. Окна настройки драйвера Реализация этих трех функций загружает интерфейсную библиотеку DLL в адресное пространство приложения для настройки параметров драйвера или свойств принтера. При этом загружаются и все используемые ей DLL. Загруженные библиотеки DLL обычно выгружаются перед выходом из этих функций. Похоже, в Windows 2000 предусмотрены какие-то меры оптимизации для простых запросов, но вывод страниц свойств по-прежнему сопровождается загрузкой десятка системных библиотек DLL. Базовая печать средствами GDI Функции спулера, описанные в предыдущем разделе, обеспечивают взаимодействие приложения со спулером и интерфейсными библиотеками DLL драйвера принтера. Обычно приложение производит настройку принтеров при помощи стандартных диалоговых окон, а для печати использует функции GDI. За кулисами стандартные диалоговые окна и GDI взаимодействуют со спулером и драйвером печати при помощи функций спулера. В этом разделе рассматривается базовая процедура создания заданий печати с использованием стандартных диалоговых окон и функций GDI. Стандартные диалоговые окна печати Стандартные диалоговые окна не относятся к числу основных средств Win32 API, поскольку все их возможности при желании можно имитировать другими
966 Глава 17. Печать функциями Win32. Однако эти окна определяют фактический стандарт пользовательского интерфейса для выполнения некоторых действий в операционной системе Windows. До настоящего момента мы рассмотрели стандартные окна для выбора цвета и шрифта, а также открытия/сохранения файла; все они были вполне удобными. Для печати в Windows предусмотрены два стандартных диалоговых окна — окно печати и окно параметров страницы. typedef struct tagPD { DWORD HWND HGLOBAL HGLOBAL HOC DWORD WORD WORD WORD WORD WORD HINSTANCE LPARAM LPPRINTHOOKPROC LPSETUPHOOKPROC LPCSTR LPCSTR HGLOBAL HGLOBAL PRINTDLG; IStructSize; hwndOwner; hDevMode; hDevNames; hDC; Flags; nFromPage; nToPage; nMinPage; nMaxPage; nCopies: hlnstance; ICustData; lpfnPrintHook; IpfnSetupHook; lpPrintTemplateName; IpSetupTemplateName; hPrintTemplate; hSetupTemplate; pedef struct tagPSD DWORD HWND HGLOBAL HGLOBAL DWORD POINT RECT RECT HINSTANCE LPARAM IStructSize; hwndOwner; hDevMode; hDevNames; Flags; ptPaperSize; rtMinMargin; rtMargin; hlnstance; ICustData; LPPAGESETUPHOOK 1pfnPageSetupHook; LPPAGEPAINTHOOK IpfnPagePaintHook; LPCSTR 1pPageSetupTemplateName; HGLOBAL hPageSetupTemplate; PAGESETUPDLG; BOOL PageSetupDlgCLPPAGESETUPDLG lppsd): Функция PrintDlg выводит стандартное диалоговое окно печати, в котором пользователь выбирает принтер, диапазон печатаемых страниц, количество копий, режим подбора по копиям, а также может вызвать страницы свойств принтера. Функция PageSetupDTg выводит стандартное окно параметров страницы, в котором пользователь выбирает размер бумаги, источник бумаги, ориентацию
Базовая печать средствами GDI 967 страницы и размеры полей. Функция PrintDlg также позволяет получить текущие настройки принтера по умолчанию без вывода диалогового окна; для этого при вызове ей передается флаг PDRETURNDEFAULT. На рис. 17.3 показано, как выглядят оба окна в Windows 2000. J№»^^-; мш «,„~ ~~. ,., -swwj&l & яь ^ \ Add Printer HP 8500 HPDJ970C HP Plotter №Ш PS t*0m~*- ~3 J №***«?«*&* f3 *§* ; p Cefet* ^ I рдоаддо, For: W* , $омщ ]Automatically Select -3 1 h Г Un&cup* ; Tqjsi [1 ft*** |1 Рис. 17.3. Стандартные диалоговые окна печати и параметров страницы в Windows 2000 Функция PrintDlg использует структуру PRINTDLG для получения параметров от приложения, возврата результатов приложению и настройки внешнего вида окна. В поле hDevMode хранится глобальный манипулятор структуры DEVM0DE, содержащей параметры печати. Поле hDevNames содержит глобальный манипулятор структуры DEVNAMES, содержащей имена драйвера принтера, устройства и порта вывода. Глобальные манипуляторы (а точнее, манипуляторы глобальных блоков памяти) были унаследованы от Win 16 API, где все задачи в системе работали в общем адресном пространстве. Глобальный манипулятор используется для ссылок на блок памяти, выделенный из глобальной кучи. В результате дефрагмен- тации и освобождения места для больших блоков такие блоки памяти могли перемещаться в куче, поэтому их приходилось фиксировать функцией Global Lock, возвращавшей дальний указатель на блок, и освобождать функцией Global Unlock. В Win32 API поддержка глобальных манипуляторов была сохранена только для того, чтобы упростить адаптацию 16-разрядных приложений. Впрочем, и в программах Win32 манипулятор глобального блока памяти и указатель на этот блок — совершенно разные вещи. Чтобы преобразовать манипулятор глобального блока в указатель на блок данных, следует вызвать функцию Global Lock. Впрочем, манипуляторы ресурсов имеют те же значения, что и соответствующие указатели. Поле hDC структуры PRINTDLG содержит манипулятор контекста устройства GDI или информационного контекста, созданный и возвращенный функций PrintDlg
968 Глава 17. Печать при передаче флага PDRETURNDC или PDRETURNIC. Поле Flags управляет интерпретацией входных данных структуры и построением выходных данных. Следующие четыре поля — nFromPage, nToPage, nMinPage и пМахРаде — управляют настройкой диапазона печатаемых страниц (см. левый нижний угол диалогового окна печати) — весьма удобная возможность для приложений, поддерживающих многостраничные документы. Поле nCopies задает количество копий. Остальные поля PRINTDLG предназначены для настройки диалогового окна печати: Программы с модифицированными окнами печати встречаются довольно часто. Например, программа бухгалтерского учета может поддерживать режим печати по интервалам дат вместо страниц. За подробным описанием структур PRINTDLG обращайтесь к MSDN. Функция PageSetupDlg использует аналогичную структуру PAGESETUP для получения параметров от приложения, возврата результатов приложению и настройки внешнего вида окна. Поля hDevMode и hDevNames структуры PAGESETUP имеют тот же смысл, что и в структуре PRINTDLG. Поле Flags также содержит десятки всевозможных флагов, управляющих работой PageSetupDlg. Главная информация PAGESETUP содержится в полях ptPageSize, rtMinMargin и rtMargin. Поле rtMinMargin задает минимальный размер полей при печати листа. Вследствие физических ограничений, обусловленных конструкцией принтера, на всех четырех сторонах листа имеются области, печать в которых невозможна. Поле rtMargin задает текущие поля, введенные пользователем в диалоговом окне, которые должны быть не меньше rtMinMargin. Ориентация листа сопровождается сменой горизонтальных и вертикальных метрик, а также отражается в структуре DEVM0DE. Данные, возвращаемые функциями PrintDlg и PageSetupDlg, чрезвычайно важны — по ним приложение форматирует документ для печати. Следовательно, они должны использовать одни и те же манипуляторы DEVM0DE и DEVNAMES, чтобы обеспечить согласованность настроек. Приложение также должно поддерживать настройку параметров печати на уровне документа, чтобы их можно было сохранить вместе с документом и восстановить при загрузке. Некоторые приложения предлагают пользователю выбрать принтер перед созданием документа. Многие приложения обращаются с запросом к драйверу принтера, чтобы синхронизировать параметры печати при загрузке документа. В листинге 17.1 приведено объявление и часть реализации класса KOutput- Setup, инкапсулирующего функции PrintDlg и PageSetupDlg. Листинг 17.1. Объявление и часть реализации класса KOutputSetup class KOutputSetup { PRINTDLG m_pd; PAGESETUPDLG m_psd; void Release(void); public: KOutputSetup(void); -KOutputSetup(void); void DeletePrinterDC(void); void SetDefaultCHWND hwndOwner, int minpage, int maxpage);
Базовая печать средствами GDI 969 int PrintDialogCDWORD flag): BOOL PageSetup(DWORD flag): void GetPaperSizeCPOINT & p) const p = m_psd.ptPaperSize: void GetMargin(RECT & rect) const rect = m_psd.rtMargin; void GetMinMargin(RECT & rect) const rect = m_psd.rtMinMargin; HDC GetPrinterDC(void) const return m_pd.hDC: DEVMODE * GetDevMode(void) return (DEVMODE *) GlobalLock(m_pd.hDevMode); const TCHAR * GetDriverName(void) const const TCHAR * GetDeviceName(void) const const TCHAR * GetOutputName(void) const HDC CreatePrinterDC(void): KOutputSetup::KOutputSetup(void) { memset (&m_pd. 0. sizeof(PRINTDLG)); m_pd.lStructSize = sizeof(PRINTDLG): memset(& m_psd, 0, sizeof(m_psd)); m_psd.lStructSize = sizeof(m_psd): } void KOutputSetup::SetDefault(HWND hwndOwner, int minpage, int maxpage) { m_pd.hwndOwner = hwndOwner: PrintDialog(PD_RETURNDEFAULT); m_pd.nFromPage = minpage; m_pd.nToPage = maxpage: m_pd.nMinPage = minpage: m_pd.nMaxPage = maxpage: m_psd.hwndOwner = hwndOwner; m_psd.rtMargin.left - 1250; // 1,25 дюйма m_psd.rtMargin.right = 1250; // 1,25 дюйма m_psd.rtMargin.top = 1000; // 1.25 дюйма m psd.rtMargin.bottom= 1000; // 1,25 дюйма Продолжение ^>
970 Глава 17. Печать Листинг 17.1. Продолжение HDC hDC = CreatePrinterDCO: int dpix = GetDeviceCaps(hDC, LOGPIXELSX); int dpiy = GetDeviceCaps(hDC. LOGPIXELSY); m_psd.ptPaperSize.x = GetDeviceCaps(hDC. PHYSICALWIDTH) * 1000 / dpix; m_psd.ptPaperSize.y = GetDeviceCaps(hDC. PHYSICALHEIGHT) * 1000 / dpiy; m_psd.rtMinMargin.left - GetDeviceCaps(hDC. PHYSICALOFFSETX) * 1000 / dpix; m_psd.rtMinMargin.top = GetDeviceCaps(hDC. PHYSICALOFFSETY) * 1000 / dpiy; m_psd.rtMinMargin.right = m_psd.ptPaperSize.x - m_psd.rtMinMargin.left - GetDeviceCaps(hDC. H0RZRES) * 1000 / dpix; m_psd.rtMinMargin.bottom= m_psd.ptPaperSize.y - m_psd.rtMinMargin.top - GetDeviceCaps(hDC. VERTRES) * 1000 / dpiy; DeleteObject(hDC); } int K0utputSetup:;PrintDialog(DW0RD flag) { m_pd.Flags = flag; return PrintDlg(&m_pd); } BOOL K0utputSetup;:PageSetup(DW0RD flag) { m_psd.hDevMode - m_pd.hDevMode; rn_psd. hDevNames = m_pd. hDevNames; m_psd.Flags = flag | PSD_INTHOUSANDTHSOFINCHES | PSD_MARGINS; return PageSetupDlg(& m_psd); } Класс KOutputSetup напоминает классы CPrintDialog и CPageSetupDialog MFC, слитые воедино. Конструктор просто инициализирует переменные m_pd (структура PRINTDLG) и m_psd (структура PAGESETUPDLG). Метод SetDefault присваивает значения нескольким важным полям без вывода диалогового окна. Для m_pd вызов PrintDlg с флагом PDRETURNDEFAULT возвращает манипуляторы DEVM0DE и DEVNAMES. Поля выбора страниц заполняются по значениям параметров SetDefault. По умолчанию размеры полей равны 1,25 дюйма для левого и правого поля и 1 дюйм для верхнего и нижнего поля. Минимальные размеры полей вычисляются с учетом физических размеров листа, печатных размеров листа и физических смещений; все эти данные возвращаются функцией GetDeviceCaps с соответствующим индексом. Размеры бумаги, возвращаемые при передаче индексов PHYSICALWIDTH и PHYSICALHEIGHT, определяют физические размеры; так, при разрешении 300 dpi размер листа формата Letter (8,5 х 11 дюймов) в пикселах равен 2250 х 3300. Размеры печатной части листа, возвращаемые при передаче индексов H0RZRES и VERTRES, определяют размеры печатной области в центре листа. Физические смещения (индексы PHYSICALOFFSETX и PHYSICALOFFSETY) определяют размеры верхних и нижних полей. На рис. 17.4 показан смысл шести метрик, возвращаемых функцией GetDeviceCaps.
Базовая печать средствами GDI 971 PHYSICAL-WIDTH PHYSICALHEIGHT PHYSICALOFFSETY <— PHYSICALOFFSETX HORZRES VERTRES Рис. 17.4. Размеры бумаги, возвращаемые функцией GetDeviceCaps Метод PrintDialog вызывает функцию PrintDlg, реализованную модулем стандартных диалоговых окон. Метод PageSetup вызывает функцию PageSetupDlg с теми же манипуляторами DEVMODE и DEVNAMES, которые используются при вызове PrintDlg; это обеспечивает синхронизацию их значений. Создание контекста устройства принтера Функция PrintDlg (а следовательно, и класс KOutputSetup) может вернуть контекст устройства принтера, позволяющий организовать вывод на печать средствами GDI. Для этого при вызове PrintDlg передается флаг PD_RETURNDC. Рассмотрим простой пример: void Demo_0utputSetup(boo1 bShowDialog) { KOutputSetup setup; DWORD flags - PD_RETURNDC; if ( ! bShowDialog ) flags |= PD_RETURNDEFAULT; if ( setup.PrintDialog(flags)==IDOK ) { HDC hDC - setup.GetPrinterDCO; // Использовать DC принтера
972 Глава 17. Печать Если флаг bShowDialog равен TRUE, на экран выводится диалоговое окно для настройки принтера; в противном случае используются текущие параметры. В обоих случаях возвращается манипулятор контекста устройства принтера, который может использоваться программой, и манипуляторы структур DEVM0DE и DEVNAMES. Все эти ресурсы освобождаются деструктором класса KOutputSetup. В общем, не происходит ничего особенного — контекст устройства принтера создается хорошо знакомой функцией CreateDC. Вспомните, что функция CreateDC получает четыре параметра: имя драйвера, имя устройства, имя порта/файла вывода и указатель на структуру DEVM0DE. Прототип CreateDC унаследован от Win 16 API, где графический драйвер представлял собой загружаемую 16-разрядную библиотеку DLL. В приложениях Win32 приложения уже не могут напрямую обращаться к драйверу графического устройства. Все параметры, необходимые для вызова CreateDC, хранятся в структуре PRINTDLG. Имена драйвера, устройства и порта вывода содержатся в структурах DEVM0DE и DEVNAMES. Приведенный ниже метод KOutputSetup: :CreatePrinterDC создает контекст устройства для принтера по манипуляторам DEVM0DE и DEVNAMES. HDC KOutputSetup::CreatePrinterDC(void) { return CreateDCCNULL. GetDeviceNameO. NULL, GetDevModeO); } const TCHAR * KOutputSetup::GetDeviceName(void) const { const DEVNAMES * pDevNames = (DEVNAMES *) Global Lock(m_pd.hDevNames): if ( pDevNames ) return (const TCHAR *) ( (const char *) pDevNames + pDevNames->wDeviceOffset ): else return NULL: } Методы GetDriverName, GetDeviceName и GetOutputName берут имена драйвера, устройства и порта вывода из структуры DEVNAMES. Типичными значениями являются «winspool», имя устройства и «lptl:». winspool.drv — клиентская библиотека DLL спулера Win32, предоставляющая все функции спулера приложениям Win32. Для создания контекста устройства принтера абсолютно необходим только один параметр — имя устройства. Имя драйвера подставляется автоматически; без имени порта вывода можно обойтись, поскольку оно передается GDI при вызове StartDoc; указатель на DEVM0DE тоже необязателен. Если вместо указателя на DEVM0DE передается NULL, драйвер принтера использует текущие настройки принтера, заданные в панели управления. Чтобы создать контекст устройства с нестандартными параметрами, необходимо передать правильно заполненную структуру DEVM0DE. Самый простой способ создания контекста устройства принтера в Windows 2000 без стандартных диалоговых окон печати заключается в использовании GetDefaultPrinter и CreateDC. В следующем фрагменте создается контекст устройства для текущего принтера по умолчанию со стандартными параметрами: TCHAR PrinterName[64]; DWORD Size - 64;
Базовая печать средствами GDI 973 GetDefaultPrinter(PrinterName, & Size); HDC hDC = CreateDC(NULL PrinterName, NULL. NULL); // Использовать DC принтера DeleteDC(hDC); При создании контекста устройства также можно запросить список принтеров, получить стандартную структуру DEVMODE, изменить ее и создать контекст устройства с именем принтера и настройками по вашему выбору. Получение информации о контексте устройства принтера Получив манипулятор контекста устройства «принтер», вы можете получить информацию о контексте функцией GetDeviceCaps. Вызов GetDeviceCaps с индексом TECHNOLOGY позволяет узнать тип принтера. Для плоттеров возвращается значение DT_PLOTTER, а для любых растровых принтеров и даже принтеров PostScript возвращается DTRASTPR INTER. Учтите, что некоторые плоттеры нового поколения тоже используют растровую технологию вместо традиционного набора из 8 перьев. При передаче индексов LOGPIXELSX и LOGPIXELSY функция возвращает разрешение принтера — важный показатель, используемый приложениями при настройке логического контекста устройства. В отличие от экранных устройств, которые при помощи логического разрешения увеличивают изображение на экране для удобства просмотра, на бумаге один дюйм всегда соответствует ровно одному дюйму. Впрочем, необходимо помнить о некоторых обстоятельствах, относящихся к разрешению принтера. О Разрешение принтера зависит от выбранного качества печати. Драйвер принтера может сообщать GDI разные значения — 1200 х 1200 dpi, 600 х 600 dpi или 300 х 300 dpi — в зависимости от качества печати и типа носителя в структуре DEVMODE. О Лист со всех четырех сторон ограничен полями (см. рис. 17.4). Вызовы GetDeviceCaps с индексами H0RZRES и VERTRES возвращают ширину и высоту не всего листа, а его печатной области. О Драйвер принтера или микропрограммное обеспечение принтера может перевести данные, полученные от GDI, в более высокое разрешение, чтобы улучшить качество печати. Например, драйвер принтера может сообщить GDI разрешение 1200 х 1200 dpi и масштабировать данные до разрешения 2400 х 1200 dpi. О Разрешение является важным, но не единственным фактором, влияющим на качество печати. Также приходится учитывать качество исходных данных, алгоритмы полутоновой обработки/смешения цветов, разрядность каждого цветового канала, механическую точность, химический состав чернил, регулировку цвета в зависимости от типа носителя и т. д. О В Windows 95/98 из-за ограничений 16-разрядной версии GDI увеличение разрешения приводит к уменьшению максимального размера бумаги. Например, если драйвер сообщает о поддержке разрешения 1200 dpi, максимальные
974 Глава 17. Печать размеры бумаги равны 32 767 пикселов (27,3 дюйма), а если разрешение увеличивается до 2400 dpi, максимальные размеры сокращаются до 13,65 дюйма. Индексы BITSPIXEL, PLANES, SIZEPALETTE и NUMC0L0RS позволяют определить формат палитры и пикселов устройства. Впрочем, вам вряд ли удастся найти драйвер принтера с поддержкой палитры. Индексы CLIPCAPS, RASTERCAPS, CURVECAPS, LINECAPS, P0LYG0NALCAPS и TEXTCAPS, относящиеся к поддержке примитивов DDI со стороны драйвера устройства, не играют особой роли для приложений в системах семейства NT. В этих системах графический механизм гораздо лучше справляется с поддержкой вывода в стандартном кадровом буфере формата DIB и разбиением команд GDI на примитивы. Скорее, эти атрибуты используются графическим механизмом для получения информации о драйвере принтера. Впрочем, если у вас возникнут проблемы с конкретным драйвером принтера — проверьте значения этих атрибутов. Возможно, это поможет вам в процессе диагностики. В Windows 98/2000 добавились новые индексы SHADEBLENDCAPS и C0L0RMGMCAPS для проверки возможностей устройства по поддержке градиентных заливок, альфа-наложения и управления цветом. При помощи функций GDI также можно перечислить шрифты, поддерживаемые принтером. Современные принтеры часто поддерживают шрифты, недоступные для драйвера экрана. На рис. 17.5 показаны атрибуты контекста устройства принтера, полученные при помощи функции GetDeviceCaps и взятые из структуры PRINTERI NF0_2, заполненной при вызове GetPrinter. Для сбора информации использовалась несколько измененная версия программы Device из главы 5. vVtf ъ/9 &•& ''' *4 ф '« / ' ' ' Oavbe Hm&< rm\mmjnmjt ГЙвй 1 Driver Name Printer Name Share Name Port Name Driver Name Comment Location Separator Page Print Processor JHPDJ970C *_—^ : Values HP DeskJet 970C Series HP DJ370C LPTV HP DeskJet 970C Series WmPrint ОС» w -ill TECHNOLOGY DRIVERVERSION HORZSIZE VERTSIZE HORZRES VERTRES LOGPIXELSX L0GPIXELSY BITSPIXEL PLANES NUMBRUSHES NUMPENS NUMMARKERS NUMF0NTS NUMC0L0RS PDEVICESIZE CURVECAPS LINECAPS ii::;::::;::::i; i Value 2 0x4005 203 mm 265 mm 2400 pixels 3141 pixels 300 dpi 300 dpi 24 bits 1 planes -1 5000 0 0 1000 0 0x1 ff Oxfe . * 1 , j 4 1 —J A J jt ж Рис. 17.5. Информация о контексте устройства принтера, возвращаемая функцией GetDeviceCaps
Базовая печать средствами GDI 975 Последовательность формирования заданий печати После настройки контекста устройства принтера можно переходить к построению задания печати средствами GDI. В GDI предусмотрены специальные функции для логической группировки команд GDI при построении заданий печати. typedef struct { int cbSize; LPCTSTR IpszDocName; LPCTSTR IpszOutput; LPCTSTR lpszDataType; DWORD fwType; } DOCINFO: int StartDoc(HDC hDC. CONST DOCINFO * Ipdi); int StartPage(HDC hDC); int EndPage(HDC hDC); int EndDoc(HDC hDC); HDC ResetDC(HDC hDC. const DEVMODE * IpInitData); int AbortDoc(HDC hDC); int SetAbortProc(HDC hdc. ABORTPROC IpAbortProc); Функция StartDoc сообщает GDI о начале нового задания печати. Структура DOCINFO, передаваемая при вызове StartDoc, содержит важнейшие сведения о задании, используемые GDI и спулером. В поле IpszDocName хранится имя документа, выводимое в диспетчере печати. Многие приложения включают в эту строку название приложения в формате <имя_приложения> - <имя_документа>. Поле 1 pszOutput содержит имя выходного устройства, которое получает данные, сгенерированные драйвером принтера. Изменяя значение этого поля, можно направить результаты вывода в файл вместо физического порта. Поле IpszDatatype содержит тип данных спулинга, рекомендуемый приложением; GDI и драйвер устройства могут игнорировать значение этого поля. Например, для применения особых возможностей Windows 2000 (скажем, печати нескольких страниц на листе) необходим спулинг в формате EMF, поэтому GDI может выбрать EMF-спулинг даже в том случае, если приложение запрашивает спулинг в низкоуровневом формате. Последнее поле содержит некоторые редкие флаги, используемые GDI и драйвером принтера. Флаг DI_APPBANDING сообщает, что разбиение страниц на полосы выполняется приложением; как упоминалось выше, GDI достаточно хорошо справляется с разбиением. Флаг DI_R0PS__READ_DESTINATI0N указывает, что приложение использует растровую операцию, читающую содержимое приемной поверхности. Растровые принтеры, у которых изображение строится на управляющем компьютере, обычно легко поддерживают любые растровые операции, но у принтеров PostScript поддержка операций, связанных с чтением с приемной поверхности, может вызвать затруднения. В Windows 95 флаг DI_R0PS_READ_DESTINATI0N фактически исключает спулинг в формате EMF. Функция StartPage начинает новую страницу задания печати, а функция EndPage завершает ее. Механизм работы спулера требует, чтобы весь вывод GDI четко делился на страницы. Графические команды не должны вызываться ни перед первым вызовом StartPage, ни между вызовами EndPage и StartPage. Из-за нетриви-
976 Глава 17. Печать альных возможностей вывода документов, реализованных в процессоре печати и драйвере принтера, StartPage и EndPage определяют только логические страницы, которые при выводе могут переставляться, разбиваться на листы или наоборот, выводиться по несколько страниц на одном физическом листе. Обычно страница выводится лишь после вызова EndPage. Это объясняется природой механизма спулинга и тем фактом, что команды GDI могут осуществлять вывод в любом месте страницы, поэтому драйвер принтера сначала получает все команды вывода для страницы и лишь потом выбирает, как действовать дальше. На этот процесс может влиять настройка спулера и особых возможностей вывода. Например, у спулера есть режим, при выборе которого печать начинается лишь после постановки в очередь последней страницы. Последнюю страницу приходится ожидать и при печати в обратном порядке, в режиме печати брошюр или двусторонней печати. При печати нескольких страниц на одном листе приходится ждать, пока в очередь будут поставлены все выводимые страницы. Функция EndDoc завершает задание печати, созданное функцией StartDoc. Вспомните, что при создании контекста устройства принтера обычно передается структура DEVM0DE со всеми необходимыми параметрами. Эти параметры можно изменять между страницами функцией ResetDC. Функции ResetDC передается манипулятор контекста устройства и указатель на новую, вероятно, измененную структуру DEVM0DE. При помощи этой функции приложение может изменить размер бумаги, ее ориентацию или другие параметры. Например, Microsoft Word позволяет выводить каждую страницу с новым размером, ориентацией и т. д. Функция AbortDoc предназначена для аварийного завершения задания печати и отмены вывода тех частей, которые еще не напечатаны. Функция SetAbortProc назначает функцию косвенного вызова, которая периодически вызывается GDI для проверки того, не следует ли отменить задание печати. Обычно эта функция используется средствами отмены печати в приложениях. В листинге 17.2 приведен несложный, но вполне реальный пример, демонстрирующий процесс формирования заданий печати в GDI. Листинг 17.2. Формирование заданий печати средствами GDI int nCall_AbortProc; BOOL CALLBACK SimpleAbortProc(HDC hDC, int iError) { nCall_AbortProc ++; return TRUE; } void SimplePrint(int nPages) { TCHAR temp[MAX_PATH]; DWORD size - MAX_PATH; GetDefaultPrinter(temp. & size); // Имя принтера по умолчанию HDC hDC = CreateDC(NULL. temp, NULL, NULL); // DC с параметрами по умолчанию
Базовая печать средствами GDI 977 if ( hDC ) { nCall_AbortProc - 0; SetAbortProc(hDC, SimpleAbortProc); DOCINFO docinfo; docinfo.cbSize = sizeof(docinfo); docinfo.IpszDocName = _T("SimplePrint"); docinfo.IpszOutput = NULL; docinfo.IpszDatatype = _T("EMF"); docinfo.fwType = 0; if ( StartDoc(hDC. & docinfo) > 0 ) { for (int p=0; p<nPages; p++) // По одной странице if ( StartPage(hDC) <« 0 ) break; else { int width - GetDeviceCaps(hDC. HORZRES); int height - GetDeviceCaps(hDC, VERTRES): int dpix - GetDeviceCaps(hDC, LOGPIXELSX); int dpiy - GetDeviceCaps(hDC, LOGPIXELSY); wsprintf(temp, _T("Page %6 of %6"). p+1. nPages); SetTextAlign(hDC. TAJOP | TA_RIGHT ); TextOut(hDC, width. 0, temp. _tcslen(temp)); Rectangle(hDC. 0. 0. dpix. dpiy); Rectangle(hDC. width, height, width-dpix. height-dpiy); if ( EndPage(hDC)<0 ) break; } EndDoc(hDC); } DeleteDC(hDC); } wsprintf(temp, "AbortProc called %6 times". nCall_AbortProc); MessageBoxCNULL. temp. "SimlePrint". MB_OK); } Функция SimplePrint организует простой цикл постраничной печати. Сначала она запрашивает имя принтера по умолчанию, создает контекст устройства, назначает функцию отмены печати и начинает вывод функцией StartDoc. Если все идет нормально, SimplePrint в цикле последовательно печатает все страницы. Все команды вывода находятся между вызовами StartPage и EndPage. Для каждой страницы функция запрашивает размер печатной области и разрешение, рисует квадрат со стороной 1 дюйм в левом верхнем и правом нижнем углах страницы и выводит номер страницы в правом верхнем углу. Обратите внимание: для контекста устройства принтера точка (0,0) в системе координат устройства (совпадает с точкой (0,0) в логической системе координат в режиме отображения ММТЕХТ) совмещается с первым печатным пиксе-
978 Глава 17. Печать лом страницы, а не просто с первым пикселом. Следовательно, ее расстояние от левого верхнего угла листа определяется величиной полей, возвращаемых функциями GetDeviceCaps(hDC, PHYSICALOFFSETX) и GetDeviceCapsChDC, PHYSICALOFFSETY). Иначе говоря, точное расположение вывода листинга 17.2 зависит от параметров устройства и шрифта, используемого при выводе текста, поскольку функция не выбирает собственный логический шрифт. По результатам, выведенным функцией SimplePrint, можно оценить размер печатной области и посмотреть, соответствует ли логический дюйм физическому. При выводе нескольких страниц их фактический порядок также учитывает другие параметры печати (например, печать в обратном порядке). В завершение своей работы SimplePrint выводит диалоговое окно, в котором приводится количество вызовов функции отмены печати. Возможно, вас удивит тот факт, что иногда эта функция не вызывается вообще, а иногда вызывается только раз на страницу. Впрочем, функция отмены печати постепенно утрачивает свое значение в новых версиях операционных систем из-за спулинга EMF и в приложениях Win32 из-за улучшенной поддержки многозадачности и многопо- точности. Поддержка печати в программах Используя функции GDI и спулера, описанные в двух предыдущих разделах, вы сможете найти принтер, настроить его, начать и завершить задание печати. Весь непосредственный вывод зависит только от ваших навыков обращения с базовыми графическими примитивами GDI. Тема вроде бы исчерпана, и мы можем двигаться дальше. Однако в реальных приложениях Windows с печатью возникает немало проблем, поскольку нигде толком не описано, как же реализуются сколько-нибудь нетривиальные возможности печати. Возможно, лучшим источником информации является система поддержки печати MFG, реализованная в архитектуре «документ/представление». Задачи, возникающие при поддержке печати в программе, делятся на несколько категорий. Нередко они оказывают влияние и на общую архитектуру программы. В этом разделе мы разработаем несколько классов, реализующих общие возможности печати в программе, в том числе поддержку единой логической системы • координат, изменения масштаба, разметки печатной страницы на экране, установки полей, вывода многостраничных документов, а также печати нескольких страниц на одном листе. В следующих двух разделах приводятся более завершенные примеры программ, предназначенных для вывода листингов с выделением синтаксических конструкций и печати фотоизображений. Единая логическая система координат В приведенных выше программах использовался режим отображения MMJTEXT, в котором преобразование из логической системы координат в координаты устройства является почти тождественным (не считая возможного смещения). Команды вывода в режиме ММ_ТЕХТ не удается легко использовать для вывода как
Поддержка печати в программах 979 на экран, так и на принтер, поскольку высокое разрешение принтера зависит от устройства и даже от текущих настроек печати. Более правильное решение заключается в выборе логической системы координат с физическими единицами (например, дюймами или миллиметрами). В GDI существует несколько стандартных режимов отображения — MM_LOENGLISH, MM_LOMETRIC, MMTWIPS и т. д. Во многих профессиональных приложениях пользователь выбирает масштаб вывода на экран. Например, Microsoft Word позволяет изменять масштаб от 500 до 10 % от логического разрешения, не говоря уже об изменении ширины, выводе всей страницы вместе с полями и режиме размещения двух страниц. Наконец, в режиме предварительного просмотра (Print Preview) функция вывода масштабирует изображение по размерам окна. Следовательно, стандартные режимы отображения исключаются. Остается единственный вариант — определить собственный режим отображения, используя наиболее общий режим MM_ANIS0TR0PIC. Ниже перечислены основные требования к такому режиму. О Единая логическая система координат. Количество единиц для представления физической единицы измерения в логической системе координат должно быть фиксированной величиной. Допустим, вы решаете, что один дюйм в логической системе координат всегда представляется 300 единицами независимо от масштаба вывода и устройства. При этом вы получаете один фрагмент графического кода, не содержащий внешних ссылок вида GetDevice- Caps(hDC, L0GPIXELS). О Поддержка масштабирования от 500 до 10 %. О Поддержка распространенных размеров носителей даже при работе в Windows 95/98. Точнее говоря, максимальный размер носителя должен составлять 43 см (17 дюймов). О Поддержка основных логических разрешений вывода без ошибок округления. При таких ограничениях нетрудно вычислить, что же мы можем сделать. Самые распространенные логические разрешения составляют 96 dpi (мелкий шрифт), 120 dpi (крупный шрифт), 360 и 600 dpi для принтеров. Наименьшее общее кратное 96, 120, 360 и 600 равно 7200. Умножая 7200 на 17 дюймов, мы получаем 122 400, что значительно больше максимальных размеров поверхности устройства в Windows 95/98 (32 767). В нашем распоряжении только числа, меньшие 1927 (32 767/17). Наименьшее общее кратное 96 и 120 равно 480; это число укладывается в отведенные границы. Наименьшее общее кратное 96,120 и 360 равно 1440, что тоже не превышает максимума. Следовательно, разумнее всего выбрать 1440 dpi — такое же логическое разрешение используется в режиме отображения MMJTWIPS. Максимальное разрешение экрана равно 120 dpi для режима крупного шрифта. Умножая 120 dpi на 500 %, мы получаем 600 dpi, что составляет примерно треть от 1927. Итак, если выбрать для нашей логической системы координат разрешение 1440 dpi, это позволит нам представить область размером до 22,75 х 22,75 дюйма, которую можно вывести в масштабе 1500 % на экране с разрешением 120 dpi, не нарушая ограничений 16-разрядной версии GDI в Windows 95/98. Главное преимущество разрешения 1440 dpi заключается в том, что оно позволяет приложению точно адресовать любые пикселы координатного пространства
980 Глава 17. Печать для графических устройств с разрешениями 96, 120 и 360 dpi без погрешностей округления. Например, один пиксел экрана с разрешением 96 dpi соответствует 15 единицам логического пространства, а на экране с разрешением 120 dpi один пиксел соответствует 12 единицам логического пространства. На принтере с разрешением 600 dpi один пиксел поверхности устройства соответствует 2,4 логической единицы, то есть 12 логических единиц соответствуют 5 пикселам. Впрочем, этим достоинства разрешения 1440 dpi не исчерпаны — 1440 делится на 72, поэтому один пункт (единица измерения кегля шрифта) соответствует 20 единицам. Если вы принадлежите к числу счастливчиков, которые пишут приложения только для систем семейства NT, подумайте об использовании логического пространства с разрешением 7200 dpi; это позволит точно адресовать все пикселы графических устройств с основными разрешениями 96, 120, 300, 360, 600, 720, 1200, 1440 и 2400 dpi. Как наглядно показывает приведенная ниже функция SetupULCS, создать единую логическую систему координат совсем несложно. #ifdef NT_0NLY #define BASE_DPI 9600 #else #define BASE_DPI 1440 #endif int gcddnt m. int n) { if ( m==0 ) return n; else return gcd(n % m, m): } void SetupULCS(HDC hDC. int zoom) { SetMapModeChDC. MM_ANIS0TR0PIC); int mul - BASE_DPI * 100; int div = GetDeviceCaps(hDC. LOGPIXELSX) * zoom: int fac = gcd(mul. div); mul /= fac; div /= fac; SetWindowExtEx(hDC, mul. mul, NULL): SetViewportExtEx(hDC, div, div, NULL); } Макрос BASE_DPI определяет количество единиц логической системы координат, соответствующих одному дюйму. Обычно оно равно 1440, если только вы не определите макрос NT_0NLY, сообщая тем самым компилятору, что программа предназначена только для систем семейства NT. Функция SetupULCS получает манипулятор контекста устройства и масштаб. Манипулятор может относиться к контексту любого графического устройства.
Поддержка печати в программах 981 Масштаб задается в процентах и изменяется в интервале от 400 до 10. На основании масштаба и логического разрешения контекста устройства функция вычисляет две внутренние переменные, из которых затем исключается наибольший общий делитель. Затем вызываются функции SetWindowExtEx и SetViewportExtEx, определяющие отображение из логической системы координат в систему координат устройства. Обратите внимание: при вызове SetWindowExtEx и горизонтальные, и вертикальные габариты определяются значением BASEDPI; это гарантирует, что BASEDPI единиц в логической системе координат всегда соответствуют одному дюйму. В табл. 17.1 приведены примеры отображений из логических координат в координаты устройства, определяемых функцией SetupULCS. Таблица 17.1. Поддержка разных устройств при разных масштабах Разрешение устройства Масштаб, % Габариты окна Габариты области просмотра 96 dpi (экран) 96 dpi (экран) 96 dpi (экран) 120 dpi (экран) 120 dpi (экран) 360 dpi (принтер) 600 dpi (принтер) 1200 dpi (принтер) 500 100 10 50 10 100 100 100 (3,3) (15,15) (150,150) (24,24) (120,120) (4,4) (12,12) (6,6) Имитация внешнего вида страницы Вывод границ листа на экране относится к числу стандартных приемов, используемых в профессиональных графических пакетах и текстовых редакторах. Границы листа помогают пользователю лучше представить, как же будет выглядеть напечатанный документ. В Microsoft Word этот режим называется разметкой страницы (page layout). Разметка страницы часто требуется и в режиме предварительного просмотра перед печатью. В некоторых приложениях весь пользовательский интерфейс строится именно на разметке страницы. Разметка печатной страницы на экране состоит из нескольких элементов. Во-первых, клиентская область окна обычно закрашивается слегка затемненным цветом. Страницы рисуются белыми, с черной рамкой и простейшей имитацией тени. Небольшие промежутки отделяют страницы друг от друга и от границ клиентской области. В режиме предварительного просмотра перед печатью непечатаемые области обычно обозначаются пунктирной линией, чтобы любые нарушения границ были хорошо видны на экране. Некоторые приложения также тем или иным способом обозначают границы полей, установленных пользователем в диалоговом окне параметров страниц.
982 Глава 17. Печать Ниже приведен пример разметки страницы на экране. Метод DrawPaper относится к классу KSurface, который будет рассматриваться ниже. Переменная т_Рарег класса KSurface определяет размеры листа, в переменной m_MinMargin хранятся минимальные размеры полей, а в переменной m_Margin — текущие размеры полей. Значения этих переменных берутся из диалогового окна параметров страниц. Вспомогательная функция DrawFrame вызывается в DrawPaper трижды. В первый раз DrawFrame рисует границу листа с тенью, во второй обозначает минимальные поля, а в третий — текущие поля. Функции рх и ру обеспечивают масштабирование координат. void KSurface::DrawPaper(HDC hDC. const RECT * rcPaint, int col, int row) { // Граница листа DrawFrame(hDC. px(0. col). py(0. row), px(m_Paper.cx, col). py(m_Paper.cy. row), RGB(0. 0. 0), RGB(0xE0. OxEO. OxEO). true); // Минимальные поля: граница печатной области DrawFrame(hDC, px(m_MinMargin.1 eft. col), py(m_MinMargin.top, row), px(m_Paper.cx - m_MinMargin.right, col), py(m_Paper.cy - m_MinMargin.bottom, row). RGB(0xF0, OxFO, OxFO), RGB(0xF0, OxFO, OxFO), false): // Поля DrawFrame(hDC. px(m_Margin.left. col). py(m_Margin.top. row). px(m_Paper.cx - m_Margin.right, col). py(m_Paper.cy - m_Margin.bottom, row), RGB(0xFF. OxFF. OxFF), RGB(0xFF. OxFF. OxFF). false): } Одновременный вывод страниц В общем случае документ состоит из нескольких страниц. Иногда при достаточно малом масштабе все страницы удается одновременно разместить на экране, что помогает пользователю получить представление о документе в целом. В таких приложениях основное внимание следует уделять логике размещения уменьшенных страниц на экране, чтобы ее не приходилось реализовывать снова и снова. Ниже приведена функция UponDraw, управляющая одновременным выводом для класса KSurface. // Вывод страниц в несколько столбцов с обозначением границ листов. // масштабированием и прокруткой void KSurface::UponDraw(HDC hDC. const RECT * rcPaint) { int nPage = GetPageCountO: int nCol = GetColumnCountO: int nRow - (nPage + nCol - 1) / nCol: for (int p=0: p<nPage: p++) { SaveDC(hDC): int col - p % nCol: int row - p / nCol:
Поддержка печати в программах 983 DrawPaper(hDC. rcPaint, col. row); SetupULCS(hDC, mjiZoom): OffsetViewportOrgExdiDC, px(m_Margin.left. col), py(m_Margin.top. row), NULL): UponDrawPage(hDC, rcPaint. GetDrawableWidthO, GetDrawableHeightO, p); RestoreDC(hDC. -1); } } Метод UponDraw (имя было выбрано для предотвращения конфликта с OnDraw) получает общее количество страниц в документе (виртуальная функция Get- PageCount) и количество столбцов в таблице (функция GetColumnCount). Затем он в цикле перебирает все страницы, находящиеся в разных строках и столбцах. Для каждой страницы UponDraw сохраняет состояние контекста устройства, вызывает функцию разметки и настраивает единую логическую систему координат. Перед виртуальным методом UponDrawPage, выводящим содержимое текущей страницы, вызывается метод OffsetViewportOrgEx, смещающий начало логической системы координат в позицию, определяемую полями текущей страницы. Функция UponDrawPage получает ширину и высоту печатной области (без учета полей) и номер страницы. Таким образом, ей не приходится беспокоиться о положении страницы на экране. После вывода страницы контекст устройства восстанавливается в прежнем состоянии для вывода следующей страницы. Печать нескольких страниц на одном листе Проблема одновременного вывода нескольких логических страниц уже рассматривалась в предыдущем разделе. Основная трудность заключается в том, чтобы использовать при печати тот же виртуальный метод UponDrawPage. В сущности, задача сводится к правильной настройке логической системы координат. Ниже приведен пример реализации для класса KSurface. // Одновременная печать, масштаб 100 % bool KSurface::UponPrint(HDC hDC, const TCHAR * pDocName) { int scale = GetDeviceCaps(hDC. LOGPIXELSX): SetMapMode(hDC. MM_ANISOTROPIC): SetWindowExtEx(hDC. BASE_DPI, BASEJPI. NULL): SetViewportExtEx(hDC. scale, scale. NULL): OffsetViewportOrgExdiDC. (m_Margin.left - m_MinMargin.left) * scale / BASE_DPI, (m_Margin.top - m_MinMargin.top ) * scale / BASE_DPI. NULL); DOCINFO docinfo: docinfo.cbSize = sizeof(docinfo): docinfo.IpszDocName = pDocName: docinfo.IpszOutput - NULL: docinfo. IpszDatatype = JVEMF");
984 Глава 17. Печать docinfo.fwType = 0; if ( StartDoc(hDC, & docinfo) <= 0) return false; int nFrom = 0; int nTo = GetPageCountO; for (int pageno=nFrom; pageno<nTo; pageno++) { if ( StartPage(hDC) < 0 ) { AbortDoc(hDC); return FALSE; } UponDrawPage(hDC. NULL. GetDrawableWidthO, GetDrawableHeightO, pageno); EndPage(hDC); } EndDoc(hDC); return true; } Логическая система координат для печати настраивается проще, поскольку вывод всегда осуществляется в масштабе 100 %. На принтере не нужно имитировать разметку страницы, но для использования той же функции UponDrawPage нам придется переместить начало логической системы координат в точку, определяемую размерами левого и верхнего полей страницы. Задача решается вызовом OffsetViewportOrgExt. Обратите внимание: смещение определяется только разностью между размерами полей и непечатаемой области, поскольку в системе координат устройства начало отсчета устанавливается в первую точку печатаемой области. Родовой класс печати Пора представить родовой класс KSurface, предназначенный для решения общих задач печати средствами GDI. В листинге 17.3 приведено объявление класса и те части реализации, которые не приводились выше. Листинг 17.3. Класс KSurface: одновременный вывод и печать нескольких страниц // 1440 = Н0К(72, 96, 120. 360) Удобно при ограничениях // в 22,75 дюйма в Win95/98 // 7200 = Н0К(72. 96. 120. 360. 600) Идеально подходит для большинства // устройств вывода #ifdef NT_0NLY typedef enum { ONE INCH - 7200 }; #else typedef enum {.ONEINCH - 1440 }: #endif
Поддержка печати в программах 985 class KSurface { public: typedef enum { BASE_DPI = ONEINCH. MARGINJ = 16. MARGIN Y =16 KOutputSetup m_OutputSetup; int mjiSurfaceWidth; // Ширина поверхности в пикселах int mjnSurfaceHeight; // Высота поверхности в пикселах SIZE m_Paper; // в BASEJPI RECT m_Margin; // в BASE_DPI RECT m_MinMargin; // в BASE_DPI int m_nZoom; // 100 для масштаба 1:1 int mjiScale; // GetDeviceCaps(hDC, LOGPIXELSX) * zoom / 100 int px(int x. int col) // Из base_dpi в экранное разрешение { return ( x + m_Paper.cx * col ) * mjiScale / BASE_DPI + MARGIN X * (col+1): int py(int y. int row) // Из base_dpi в экранное разрешение { return ( у + m_Paper.cy * row ) * mjiScale / BASE_DPI + MARGIN_Y * (row+1): } public: int GetDrawableWidth(void) { return m_Paper.cx - m_Margin.left - m_Margin.right: } int GetDrawableHeight(void) { return m_Paper.cy - m_Margin.top - m_Margin.bottom: } virtual int GetColumnCount(void) { return 1; } virtual int GetPageCount(void) { return 1: // Одна страница } virtual const TCHAR * GetDocumentName(void) { Продолжение &
986 Глава 17. Печать Листинг 17.3. Продолжение return _T("«Surface - Document"); } virtual void DrawPaper(HDC hDC. const RECT * rcPaint. int col. int row); virtual void CalculateSize(void); virtual void SetPaper(void); virtual void RefreshPaper(void); virtual void UponDrawPage(HDC hDC. const RECT * rcPaint. int width, int height, int pageno=l); virtual bool UponSetZoom(int zoom); virtual void Uponlnitialize(HWND hWnd); virtual void UponDraw(HDC hDC. const RECT * rcPaint); virtual bool UponPrint(HDC hDC. const TCHAR * pDocName); virtual bool UponFilePrint(void); virtual bool UponFilePageSetup(void); }: // Перейти от 1/1000 дюйма к BASE_DPI void «Surface::SetPaper(void) { POINT paper; RECT margin; RECT minmargin; m_OutputSetup.GetPaperSize(paper); mJMputSetup.GetMargin(margin); m_OutputSetup.GetMi nMargi n(mi nmargi n); m_Paper.cx - paper.x * BASE_DPI / 1000; m_Paper.cy - paper.у * BASEJPI / 1000; m_Margin.left - margin.left * BASE_DPI / 1000; m_Margin.right - margin.right * BASE_DPI / 1000; m_Margin.top - margin.top * BASE_DPI / 1000: m_Margin.bottom» margin.bottom * BASE_DPI / 1000; m_MinMargin.left - minmargin.left * BASE_DPI / 1000; m_MinMargin.right - minmargin.right * BASE_DPI / 1000; m_MinMargin.top - minmargin.top * BASE_DPI / 1000; m_MinMargin.bottom= minmargin.bottom * BASE_DPI / 1000; } // Вычислить общий размер поверхности для вывода nPage страниц в nCol столбцов void «Surface::CalculateSizeCvoid) { int nPage = GetPageCountO; int nCol = GetColumnCountO; int nRow = (nPage + nCol - 1) / nCol; m_nSurfaceWidth - px(m_Paper.cx. 0) * nCol + MARGINJ; m_nSurfaceHeight - py(m_Paper.cy. 0) * nRow + MARGINJ;
Поддержка печати в программах 987 bool KSurface::UponSetZoom(int zoom) { if ( zoom == m_nZoom ) return false; mjiZoom = zoom; HDC hDC = GetDC(NULL); m_nScale = zoom * GetDeviceCaps(hDC, LOGPIXELSX) / 100; DeleteDC(hDC); CalculateSizeO; return true; void KSurface;;RefreshPaper(void) { int zoom = mjiZoom; mjiZoom - 0; SetPaperO; UponSetZoom(zoom); } void KSurface::UponInitializeCHWND hWnd) { m_OutputSetup.SetDefault(hWnd. 1. GetPageCountO); m_nZoom = 100; RefreshPaperO; } void KSurface::UponDrawPage(HDC hDC. const RECT * rcPaint, int width, int height, int pageno) { for (int h*0; h<»(height-BASE_DPI); h +- BASE_DPI) for (int w=0; w<=(width-BASEJDPI); w +- BASE_DPI) Rectang1e(hDC. w. h. w+BASEJDPI. h+BASE_DPI); } bool KSurface::UponFilePrint(void) { int rslt - m_OutputSetup.PrintDialog(PD_RETURNDC | PD_SELECTION); if ( rslt==ID0K ) UponPrint(m_OutputSetup.GetPrinterDC(). GetDocumentName()); m__OutputSetup. Del etePri nterDC (); return false; bool KSurface::UponFilePageSetup(void) { if ( m_OutputSetup.PageSetup(PSD_MARGINS) )" { RefreshPaperO; return true; } return false; }
988 Глава 17. Печать Класс KSurface решает общую задачу одновременного вывода и печати нескольких страниц с поддержкой разных масштабов. Он настолько универсален, что даже не ассоциируется ни с каким окном — для работы с ним достаточно передать манипулятор контекста устройства. Следовательно, этот класс может использоваться для окон SDI и MDI, для диалоговых окон и страниц свойств и даже в элементах ActiveX, EMF или совместимых контекстах устройств. Переменная m_OutputSetup является экземпляром класса KOutputSetup, управляющего настройкой печати и параметров страниц. Переменные mjiSurfaceWidth и mjiSurfaceHeight определяют ширину и высоту поверхности вывода в пикселах и могут использоваться для организации прокрутки. Код вывода полностью поддерживает возможность прокрутки. Значения следующих трех переменных берутся из диалогового окна параметров страниц и преобразуются в единую логическую систему координат методом SetPaper. Переменные m_nZoom и m_nScale определяют масштаб вывода на экран и используются в подставляемых (in-line) функциях преобразования рх и ру. Смысл виртуальных и обычных методов класса вполне очевиден. Переопределение метода UponDrawPage играет ключевую роль при выводе. По умолчанию этот метод рисует на странице квадраты со стороной в один дюйм. Метод UponSetZoom связывается с командой меню или кнопкой панели инструментов и управляет масштабом вывода. Метод Uponlnitialize инициализирует переменные класса. Метод UponDraw связывается с обработчиком сообщения WMPAINT, если класс используется для вывода в окне. Метод UponFilePrint связывается с командой печати в меню. Наконец, метод UponFilePageSetup связывается с командой меню, обеспечивающей настройку параметров страниц. Рис. 17.6. Пример использования классов KSurface и KPageCanvas
Вывод в контексте устройства принтера 989 На прилагаемом компакт-диске приведен класс KPageCanvas, производный от классов KScroll Canvas (поддержка прокрутки в дочерних окнах MDI) и KSurface (одновременный вывод и печать нескольких страниц). На рис. 17.6 показан результат вызова стандартной функции UponDrawPage. Функция рисует квадраты со стороной 1 дюйм на листе размером 4x6 дюймов в альбомной ориентации, с полями размером 0,5 дюйма и в масштабе 75 %. Вывод в контексте устройства принтера Интерфейс GDI проектировался как аппаратно-независимый интерфейс, поэтому вывод в экранном контексте устройства не должен сильно отличаться от вывода в контексте принтера. И все же при работе с контекстом устройства принтера приходится учитывать ряд обстоятельств, особенно если результаты печати отличаются от желаемых. Некоторые проблемы связаны не столько с контекстом устройства принтера, сколько с методикой разработки графических алгоритмов, сохраняющих все пропорции при разных настройках логической системы координат. Единицы измерения Если в вашей программе вывод на экран и на принтер должен выполняться одним фрагментом кода, то от режима отображения ММ_ТЕХТ и системы координат устройства вам придется перейти на логическую систему координат. Однозначное соответствие между единицами логической системы координат и системы координат устройства при этом теряется. Например, класс KSurface из предыдущего раздела использует логическую систему координат с разрешением 1440 dpi как для печати, так и для вывода на экран. У большинства графических устройств разрешение не достигает 1440 dpi, поэтому один пиксел поверхности устройства обычно соответствует нескольким логическим единицам. Впрочем, в ближайшем будущем появятся принтеры с разрешением 2400 dpi. На таких устройствах одна логическая единица будет соответствовать нескольким пикселам поверхности устройства. Итак, при программировании аппаратно-независимого графического вывода следует обратить внимание на следующие обстоятельства, относящиеся к логической системе координат. О Если толщина пера превышает один пиксел, она задается в логической системе координат. При написании универсального графического кода толщину пера приходится задавать в реальных единицах, а не в пикселах. Например, при работе с классом KSurface, представляющим один дюйм 1440 единицами, при определении пера толщиной один пункт будет указываться толщина 20. О При преобразовании координат из логической системы в систему координат устройства следует использовать функции LPtoDP и DPtoLP GDI, поскольку рассчитывать на постоянную связь между этими системами координат уже не приходится.
990 Глава 17. Печать О Некоторые графические алгоритмы при работе с большим изображением используют приращение для перехода к следующей координате. Например, закраска области может осуществляться выводом серии линий, расположенных вплотную друг к другу. Проанализируйте такие алгоритмы и проверьте, не возникают ли при выводе пропуски, перекрытия или иные нарушения. О Функция BitBlt, часто используемая при выводе растров, хорошо работает лишь при выводе на экран или в режиме отображения ММ_ТЕХТ. В универсальном графическом коде вызовы BitBlt следует заменить более общей функцией StretchBU. О Размеры узоров в штриховых кистях GDI зависят от устройства. При использовании штриховых кистей в коде графического вывода с переменным масштабом и при печати окончательный размер этих узоров непредсказуем. Реализуйте собственные аппаратно-независимые штриховые кисти (см. главу 9). О Растры в узорных кистях определяются в системе координат устройства без масштабирования. Таким образом, при рисовании узорной кистью в контексте принтера высокого разрешения исходный (не масштабированный) узор повторяется до заполнения указанной области. Избегайте узорных кистей или масштабируйте растр узора до нужных размеров перед созданием кисти. Текст Текстовые метрики и функции GDI не обеспечивают в достаточной степени вывод текста с точным масштабированием. Главная проблема связана с тем, что GDI работает с целочисленными текстовыми метриками, масштабируемыми до размера шрифта. При выводе нескольких десятков символов в одной строке погрешности ширины и высоты символов накапливаются и начинают влиять на форматирование текста. Поэкспериментируйте с функциями GDI (например, TextOut) и USER (такими, как DrawText), с классами KSurface и KPageCanvas при разном разрешении экрана и масштабе вывода — вы заметите, как быстро накапливаются ошибки. Эта проблема подробно исследовалась в главе 15, посвященной работе с текстом. Одно из возможных решений — использовать эталонный шрифт, размер которого совпадает с размером em-квадрата, описывающего шрифт TrueType. Для решения этой проблемы был создан класс KTextFormator. К этой главе прилагается программа CodePrint, предназначенная для экспериментов с аппаратно-независимым форматированием и многостраничной печатью. В программе CodePrint реализованы простые средства просмотра и печати исходных текстов программ с цветовым выделением синтаксических элементов. Применение класса KTextFormator при форматировании текста гарантирует, что количество строк на странице и позиция символа в строке всегда остаются постоянными независимо от масштаба и разрешения устройства. В работе программы используется простейший лексический анализатор C/C++, который умеет распознавать ключевые слова C/C++, числа, символьные литералы, строковые литералы и комментарии. По результатам работы лексического анализатора каждому символу в строке присваивается цвет. Последовательность одноцветных символов выводится одной функций, перед вызовом которой нужный цвет выбирается для текста.
Вывод в контексте устройства принтера 991 Логика вывода исходных текстов реализована в классе KProgramPageCanvas, производном от класса KPageCanvas, описанного в предыдущем разделе. Ниже приведены два важных метода этого класса. void KProgramPageCanvas::SyntaxHighlight(HDC hDC, int x. int y. const TCHAR * mess) { BYTE f1ag[MAX_PATH]; int len = Jxslen(mess); assertden < MAX_PATH-50): memset(flag. 0. MAX_PATH): ColorText(mess, flag): float width = 0.0: for (int k=0; k<len: ) { int next = 1: while ( (k+next<len) && (flag[k]==flag[k+next]) ) next ++; SetTextColor(hDC. crText[flag[k]]): m_formator.TextOut(hDC. (int) (x + width + 0.5), y. mess+k. next): float w, h; m_formator.GetTextExtent(hDC, mess+k, next, w, h): width += w: k += next: void KProgramPageCanvas::UponDrawPage(HDC hDC. const RECT * rcPaint. int width, int height, int pageno) { if ( rcPaint )// Отказаться от вывода, если текущая страница { // не пересекается с rcPaint RECT rect - { 0. 0. width, height }: LPtoDP(hDC. (POINT *) & rect. 2); if ( ! IntersectRect(& rect. rcPaint. & rect) ) return: } HGDIOBJ hOld = SelectObject(hDC. mJiFont): SetBkMode(hDC. TRANSPARENT): SetTextAlign(hDC. TAJ.EFT | TAJTOP): KGetline parser(mj)Buffer, mjiSize); int skip = pageno * mjlinePerPage: for (int i=0: i<skip: i++) parser.NextlineO; for (i-0; i<m_nLinePerPage: i++) if ( parser.NextlineO ) {
992 Глава 17. Печать SyntaxHighlight(hDC. 0. (int)(m_formator.GetLinespace() * i + 0.5), parser.mjine); } else break; SelectObjectChDC. hOld); } Класс KProgramPageCanvas переопределяет метод KSurface::GetPageCount и реализует более точный способ подсчета страниц, основанный на подсчете строк исходного текста и точной информации о высоте строки. Метод UponDrawPage выводит одну страницу в таблице. Сначала он преобразует прямоугольник страницы из логической системы координат в координаты устройства, чтобы узнать, пересекается ли он с текущим перерисовываемым прямоугольником. Страницы, которые не видны на экране, пропускаются. Затем UponDrawPage при помощи класса KGetline последовательно читает все строки исходного текста, пропускает строки, относящиеся к предыдущим страницам, и выводит только текущую страницу. Вероятно, для повышения быстродействия следовало бы построить индекс. Функция SyntaxHighlight выводит одну строку программы. Она назначает цвета каждому символу в соответствии с лексическими правилами C/C++, обращаясь к лексическому анализатору ColorText, и использует методы класса KTextFormat для точного форматирования выводимого текста. На рис. 17.7 показано окно программы CodePrint, причем в качестве примера выбран ее собственный исходный текст. Рис. 17.7. CodePrint: форматирование текста, одновременный просмотр и печать нескольких страниц
Вывод в контексте устройства принтера 993 Растры Аппаратно-зависимые растры и DIB-секции всегда ассоциируются с совместимым контекстом устройства. Если совместимый контекст устройства ориентируется на конкретный целевой контекст, аппаратно-зависимые растры, созданные для совместимости с экраном, наверняка окажутся несовместимыми с контекстом устройства принтера. Например, если воспользоваться функцией LoadBItmap для загрузки растрового ресурса в формате DDB и создать совместимый контекст устройства для контекста принтера, в общем случае вам не удастся выбрать растр в совместимом контексте, поскольку он может оказаться несовместимым. Вместо этого совместимый контекст устройства следует создать для экранного контекста. И вообще, использовать при печати аппаратно-зависимые растры не рекомендуется — особенно в видеорежимах с 256 цветами, поскольку принтеры не поддерживают аппаратную палитру. Некоторые приложения разделяют большие растры на маленькие фрагменты, чтобы оптимизировать вывод растра на экран или обойти старое ограничение размеров растра в 64 Кбайт. Это особенно важно в Windows 95/98, поскольку до завершения графического вызова GDI все обращения к GDI от других программных потоков блокируются (во избежание проблем с реентерабельностью 16-разрядного кода GDI). С другой стороны, стратегия деления может вызвать большие проблемы с печатью. Во-первых, как было показано выше, при построении EMF у GDI не хватает сообразительности для исключения из EMF неиспользуемых частей исходного растра, в результате чего сгенерированные EMF-файлы могут иметь громадные размеры. Во-вторых, передача большого количества мелких растров драйверу принтера усложняет их обработку драйвером. В-третьих, при делении растра необходимо позаботиться о том, чтобы между частями растра не оставалось пробелов. Если у вас возникли проблемы с печатью растров, вы можете получить важную диагностическую информацию при помощи утилит просмотра и расшифровки EMF-файлов из предыдущей главы. Печать графики в формате JPEG С широким распространением цифровых устройств, работающих с графикой — цифровых фотоаппаратов, сканеров и цветных принтеров, — у нас все чаще возникает необходимость в получении качественных исходных изображений и профессиональном выводе с фотографическим качеством. Печать высококачественных фотографий на компьютере еще никогда не была такой простой и доступной. К сожалению, растровые форматы Win32 не поддерживают сжатия (не говоря уже о качественном сжатии) графики в форматах High Color и True Color. Как правило, никто не хранит свои фотографии в «родном» для Windows формате BMP. В настоящее время для хранения фотографических изображений чаще всего применяется формат JPEG, разработанный группой Joint Photographic Experts Group. GDI до сих пор не поддерживает формат JPEG, хотя в GDI и предусмотрены ограниченные возможности для передачи драйверу принтера изображений
994 Глава 17. Печать JPEG, внедренных в BMP-файлы. Чтобы передать драйверу принтера изображение JPEG или PNG в Windows 98/2000, приложение вызывает функцию ExtEscape с параметрами QUERYSCSUPPORT и CHECKJPEGFORMAT. Если драйвер принтера отвечает положительно, значит, приложение может передать ему изображение JPEG или PNG вызовом SetDIBitsToDevice или StretchDIBits. Однако никто не гарантирует, что ваш принтер поддерживает расшифровку сжатых данных JPEG/PNG, поэтому эту возможность все равно придется реализовывать в приложении. Если вам повезло и драйвер принтера поддерживает расшифровку, это повысит скорость печати. Следующая функция иллюстрирует передачу изображения в формате JPEG на принтер. BOOL StretchJPEGCHDC hDC, int x, int y, int w. int h, void * pJPEGImage. unsigned nJPEGSize, int width, int height) { DWORD esc = CHECKJPEGFORMAT; if ( ExtEscape(hDC, QUERYESCSUPPORT. sizeof(esc), (char *) &esc, 0, 0) <=0 ) return FALSE; DWORD rslt = 0; if ( ExtEscape(hDC, CHECKJPEGFORMAT. nJPEGSize. (char *) pJPEGImage. sizeof(rslt). (char *) &rslt) <=0 ) return FALSE; if ( rslt!=l ) return FALSE; BITMAPINFO bmi ; memset(&bmi , 0. sizeof(bmi)); bmi.bmiHeader.biSize - sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = width; bmi.bmiHeader.biHeight = - height; // Перевернутое изображение bmi.bmiHeader.biPlanes = 1; bmi.bmi Header.bi Bi tCount = 0; bmi .bmiHeader.biCompression = BI_JPEG; bmi.bmi Header.bi Si zelmage = nJPEGSi ze; return GDIJRROR != StretchDIBits(hDC. x. y. w. h, 0. 0. width, height, pJPEGImage. &bmi. DIB_RGB_C0L0RS. SRCCOPY); } Функция StretchJPEG вызывает ExtEscape дважды. В первый раз она проверяет, поддерживается ли команда CHECKJPEGFORMAT, а во второй — приемлем ли формат JPEG, который мы собираемся передать. Если обе проверки завершаются успешно, изображение JPEG упаковывается в DIB и передается функции StretchDIBits. Если вызов StretchJPEG завершается неудачей, вызывающая сторона должна самостоятельно расшифровать JPEG и передать расшифрованные данные устройству. Расшифровка JPEG считается очень непростой задачей из-за сложности алгоритма сжатия JPEG. С другой стороны, группа IJG (Independent JPEG Group)
Вывод в контексте устройства принтера 995 реализовала сжатие и восстановление JPEG на разных платформах, причем все исходные тексты распространяются бесплатно. Библиотеку IJG для работы с JPEG можно загрузить с сайта www.ijg.org. Хотя библиотека IJG написана на С, а не на C++, в ней использована превосходная объектно-ориентированная архитектура, реализующая скрытие данных и наследование средствами языка С. На прилагаемом компакт-диске библиотека IJG была слегка изменена, чтобы она больше походила на код C++. Переход на C++ упрощает настройку библиотеки без использования указателей на функции. Например, исходный вариант кода IJG поддерживал расшифровку данных только из файлового потока. Благодаря модификации мы можем легко расширить библиотеку и обеспечить расшифровку из буфера, находящегося в памяти. class const_source_mgr : public jpeg_source_mgr { public : void Reset(const unsigned char * buffer, int len ) { bytes_in_buffer -len; next_inputjbyte = buffer; } void init_source(j_decompress_ptr cinfo) { } virtual void term_source(j_decompress_ptr cinfo) { if (cinfo->src) { delete (const_source_mgr *) cinfo->src; cinfo->src = NULL; } } }: GLOBAL(void) jpeg_const_src (j_decompress_ptr cinfo. const unsigned char * buffer, int len) { const_source_mgr * src; if (cinfo->src == NULL) // Впервые для этого объекта JPEG? cinfo->src = new const_source_mgr; src = (const_source_mgr *) cinfo->src; src->Reset(buffer. len); } Класс, приведенный в листинге 17.4, расшифровывает изображения JPEG в формат DIB Windows и генерирует файлы JPEG по изображениям в формате DIB.
996 Глава 17. Печать Листинг 17.4. Класс KPicture: шифрование/расшифровка изображений в формате JPEG class KPicture { void Release(void); int Allocatednt width, int height, int channels, bool bBits=true); public: BITMAPINFO * m_pBMI: BYTE *m_pBits; BYTE * m_pJPEG: int mjnJPEGSize: KPictureO: -KPictureO; int GetWidth(void) const { return m_pBMI->bmiHeader.biWidth; } int GetHeight(void) const { return m_pBMI->bmiHeader.biHeight; } BOOL DecodeJPEGCconst void * jpegimage. int jpegsize); BOOL QueryJPEG(const void * jpegimage. int jpegsize): BOOL LoadJPEGFile(const TCHAR * filename): BOOL SaveJPEGFile(const TCHAR * fileName. int quality): BOOL KPicture::DecodeJPEGCconst void * jpegimage. int jpegsize) { try { struct jpeg_decompress_struct dinfo: jpeg_error_mgr jerr: dinfo.err = & jerr: di nfo.jpeg_create_decompress(): jpeg_const_src(&dinfo. (const BYTE *) jpegimage. jpegsize): di nfo.jpeg_read_header(TRUE); di nfo.jpeg_start_decompress(): intbps = Allocate(dinfo.image_width. dinfo.image_height. di nfo.out_color_components. true): if ( m_pBits==NULL ) return FALSE: for (int h=dinfo.image_height-l; h>=0: h--) // Перевернутое { // изображение
Вывод в контексте устройства принтера 997 BYTE * addr = m_pBits + bps * h; dinfo.jpeg_read_scanlines(& addr, 1); } di nfo.jpeg_fi ni sh_decompress(); dinfo.jpeg_destroy_decompress(); m_pJPEG = (BYTE *) jpegimage; m_nJPEGSize = jpegsize; } catch (...) { return FALSE; } return TRUE; } В листинге 17.4 приведено лишь объявление класса KPicture и функция расшифровки DecodeJPEG. Метод DecodeJPEG преобразует данные изображения JPEG, хранящегося в буфере памяти, прямо в перевернутый формат DIB системы Windows. Метод Allocate управляет выделением памяти и заполнением структуры BITMAPINFO. Поддерживаются как 24-разрядный цветной формат JPEG, так и 8-разрядные изображения в оттенках серого. После расшифровки указатель и размер исходного изображения JPEG сохраняются в классе KPicture на случай, если драйвер принтера согласится их принять. Обратите внимание: библиотека JPEG была модифицирована так, чтобы в расшифрованном изображении составляющие RGB следовали в порядке «синий — зеленый — красный», совместимом с 24-разрядным форматом DIB. На компакт-диске также имеется программа ImagePrint, предназначенная для экспериментов с расшифровкой, выводом на экран и печатью изображений в формате JPEG. В программе ImagePrint изображение JPEG поддерживается классом KImageCanvas, производным от класса KPageCanvas. Основная функция вывода KImageCanvas обеспечивает вывод и печать расшифрованных изображений, а также печать исходного изображения в формате JPEG. Метод UponDrawPage приведен ниже. void KImageCanvas;;UponDrawPage(HDC hDC, const RECT * rcPaint. int width, int height, int pageno) { if ( (m_pPicture==NULL) && (m_pPicture->m_pBMI==NULL) ) int int int int int int int return; sw sh = dpix = dpiy = dpi = dispwi m_pPicture->GetWidth(); m_pPicture->GetHeight(); sw * ONEINCH / width; sh * ONEINCH / height; max(dpix, dpiy); dth = sw * ONEINCH / dpi; dispheight = sh * ONEINCH / dpi; SetStretchBltMode(hDC. STRETCH DELETESCANS);
998 Глава 17. Печать int х - ( width- dispwidth)/2; int у = (height-dispheight)/2; if ( StretchJPEG(hDC, x, y. dispwidth, dispheight. m_pPicture->m_pJPEG. m_pPicture->m_nJPEGSize, sw, sh ) ) return; StretchDIBits(hDC, x, y. dispwidth. dispheight. 0. 0. sw. sh. m_pPicture->m_pBits. m_pPicture->m_pBMI. DIB_RGB_COLORS. SRCCOPY); } Программа ImagePrint была задумана как простейшее средство для печати фотографий, поэтому метод KImageCanvas: :UponDrawPage пытается с максимальной эффективностью использовать дорогую поверхность носителя. Он вычисляет максимальный размер изображения, помещающегося без искажения пропорций на бумаге при текущих размерах полей. Руководствуясь границами листа и обозначениями полей на экране, вы можете легко отрегулировать размеры полей или переключиться на другую ориентацию листа. Сначала метод вызывает функцию StretchJPEG и пытается вывести небольшое изображение в формате JPEG. Если попытка окажется неудачной, приходится передавать большое изображение в формате BMP. Кстати, существует как минимум один драйвер принтера, принимающий изображения в формате JPEG — это драйвер HP 8500 Color PostScript. При выводе в файл вы увидите, что изображение JPEG кодируется в файле PostScript в двоичные данные; это приводит к значительному уменьшению размеров файла. Итоги Настоящая глава объединяет многие темы, рассматривавшиеся в книге (контексты устройств, линии и кривые, заливки, растры, шрифты, текст и EMF) применительно к печати. Мы рассмотрели архитектуру спулера, общую последовательность действий при печати, функции API спулера, предназначенные для получения информации и настройки принтеров, функции поддержки печати в GDI, а также — что самое важное — реализацию профессиональных возможностей печати в приложениях средствами Win32 API. В разделе «Поддержка печати в программах» представлен родовой класс KSurf асе, предназначенный как для вывода на экран, так и для печати. Этот класс обеспечивает полноценное WYSIWYG-представление графических данных в единой логической системе координат. В разделе «Вывод в контексте устройства принтера» приведены два примера нетривиальных программ, использующих классы KSurface и KPageCanvas для решения вполне реальных задач — печати исходных текстов программ и графики в формате JPEG. На этом завершается наше знакомство с традиционным графическим программированием для Windows. Впрочем, GDI, как и любая технология, продолжает развиваться. Благодаря аппаратному ускорению в будущем программы начнут работать с еще большим количеством цветов, а их быстродействие ста-
Итоги 999 нет еще выше. Следующая глава посвящена одному из направлений развития GDI — программированию для DirectDraw. Дополнительная информация Если вы захотите еще больше узнать о печати и спулере, обратитесь к Microsoft DDK. Вы найдете очень подробное описание интерфейса DDI, архитектуры спулера и приемов написания мини-драйверов в архитектуре UniDriver. К DDK также прилагаются исходные тексты драйвера и мини-драйвера, процессора печати EMF и монитора порта. В старые версии Windows NT DDK даже входил полный исходный текст драйвера принтера PostScript. Некоторые проблемы с печатью решаются анализом EMF-файлов спулинга. За информацией и утилитами для работы с метафайлами обращайтесь к главе 16. Примеры программ К главе 17 прилагается несколько примеров программ (табл. 17.2). Таблица 17.2. Программы главы 17 Каталог проекта Описание Samples\Chapt_17\PrinterDevice Samples\Chapt_17\Printer Samples\Chapt_17\CodePrint Samples\Chapt_17\ImagePrint Программа для получения информации о работе спулера и атрибутов контекста устройства принтера. Создана па основе аналогичной программы для работы с экранными устройствами (см. главу 5) Тестовая программа для передачи спулеру низкоуровневых данных и EMF. Иллюстрирует работу с диалоговыми окнами печати и параметрами страниц, простейший цикл печати, применение классов KSurface и KPageCanvas, вывод линий и кривых, заливку замкнутых фигур и аппаратно-пезависимую работу с растрами и текстом Вывод на экран и печать исходных текстов программ с выделением синтаксических элементов в режиме WYSIWYG Просмотр и печать графики в формате JPEG. Программа позволяет передавать на принтер изображения JPEG. Использует библиотеку JPEG из каталога Samples\include\jlib
Глава 18 DirectDraw и непосредственный режим Direct 3D Интерфейс GDI в течение долгого времени считался основным интерфейсом API графического программирования для Windows. Впрочем, в мире персональных компьютеров произошло так много изменений, что и в GDI пришло время фундаментальных усовершенствований (хотя мы знаем, что GDI постепенно усовершенствуется в каждой новой версии Windows). Будущей версии GDI присвоено кодовое название GDI+; это будет GDI нового поколения от Microsoft. Согласно опубликованной документации Microsoft (www.microsoft.com/hwdev/vJdeo/~GDInext.htm), GDI+ создаст инфраструктуру для нововведений в области пользовательского интерфейса. В частности, GDI+ обеспечит простую интеграцию двумерной и трехмерной графики, упростит обработку оцифрованных изображений и установит новые стандарты в области качества графики и быстродействия настольных систем. GDI+ будет поддерживать нетривиальные графические возможности — альфа-наложение, размытие, прозрачные окна, второй буфер, гамма-коррекцию, трехмерный пользовательский интерфейс и т. д. Возникает впечатление, что интерфейс GDI+ в первую очередь направлен на интеграцию традиционного интерфейса GDI с новыми интерфейсами API от Microsoft, предназначенными для программирования игр (DirectDraw и Direct3D). Интеграция плоской и трехмерной графики начнется на уровне API и будет распространяться до уровня DDI (интерфейс драйверов устройств). На уровне DDI GDI+ полное аппаратное ускорение будет обеспечиваться комбинациями двумерных и трехмерных команд. В GDI+ будут определены новые команды для примитивов, не поддерживаемых существующими командами DirectDraw и Direct3D. Говорят, что DirectDraw и Direct3D уже не будут ограничиваться программированием игровых и учебных приложений, а войдут в число базовых компо-
Технология COM 1001 нентов GDI. Другими словами GDI+ это будет GDI + DirectDraw + Direct3D + что-нибудь еще. Как видите, у нас есть все причины, чтобы поскорее заняться DirectDraw и Direct3D. DirectDraw — относительно сложный интерфейс API двумерной графики, для сколько-нибудь приличного описания которого понадобится не менее 200 страниц. Впрочем, непосредственный режим (Immediate Mode) Direct3D настолько сложен, что вам придется прочитать немало книг по компьютерной графике хотя бы для того, чтобы в нем разобраться, не говоря уже об эффективном применении. Эту короткую главу можно рассматривать лишь как краткое введение в DirectDraw и Direct3D. Основное внимание уделяется следующим темам: О знакомство с базовыми концепциями, интерфейсами и методами для программистов GDI; О разработка классов C++, упрощающих программирование для DirectDraw и Direct3D; О возможности применения DirectDraw и Direct3D в «традиционном» программировании для Windows. Технология СОМ Подсистема GDI в Win32 API содержит примерно 500 функций, образующих довольно сложную иерархию без четкой группировки. При проектировании DirectX компания Microsoft позаимствовала модель интерфейса между приложениями и операционной системой из технологии СОМ. Понимание базовых принципов СОМ абсолютно необходимо для написания правильно работающих программ DirectX. СОМ-интерфейсы В технологии СОМ семантически связанные абстрактные методы группируются в абстрактных базовых классах, которые в терминологии СОМ называются интерфейсами. СОМ-интерфейс, как и абстрактный базовый класс, только определяет прототипы всех методов интерфейса на синтаксическом уровне и задает порядок этих методов. Для определения семантики этих методов вместо формального языка, подходящего для машинной проверки, используется запись, более или менее напоминающая естественный язык — просто потому, что на роль такого формального языка не нашлось подходящего кандидата. Все СОМ-интерфейсы являются производными от общего корневого интерфейса I Unknown, который определяется следующим образом: class IUnknown { public: virtual HRESULT stdcall QueryInterface(REFIID riid. void ** ppvObject) - 0;
1002 Глава 18. DirectDraw и непосредственный режим Direct3D virtual ULONG stdcall AddRef(void) - 0; virtual ULONG stdcall Release(void) = 0; }: С каждым СОМ-интерфейсом связывается 128-разрядный идентификатор, который обычно содержит гораздо больше информации, чем ISBN книги, номер машины или водительского удостоверения. Идентификаторы интерфейсов должны быть глобально-уникальными, поэтому они обычно называются GUID (Global Unique ID, глобально-уникальный идентификатор). Например, GUID интерфейса IUknown называется IID_IUnknown и определяется следующим образом: DEFINE_GUID(IID_IUnknown. 0x00000000. 0x0000. 0x0000. ОхСО. 0x00. 0x00. 0x00. 0x00. 0x00. 0x00. 0x46); СОМ-классы СОМ-интерфейс — всего лишь абстрактная спецификация. Чтобы интерфейс приносил практическую пользу, он должен быть воплощен в СОМ-классе. СОМ- класс, реализующий некоторый СОМ-интерфейс, должен определяться как производный от него и реализовывать все методы этого интерфейса. Ниже приведен пример реализации интерфейса I Unknown: class KUnknown : public IUnknown { ULONG m_nRef; // Счетчик ссылок public: KUnknown() { m nRef =0: // В начальном состоянии счетчик ссылок равен 0 ULONG AddRef(void) { return ++ mjnRef; // AddRef увеличивает счетчик ссылок } ULONG Release(void) // Release уменьшает счетчик ссылок { if ( -- m_nRef==0) // Если счетчик ссылок достиг 0 { delete this; return 0; } return mjnRef; } HRESULT QuerylnterfaceCREFIID id. void * * ppvObj) { if ( iid == IIDJUnknown) // Поддерживается только IUnknown { * ppvObj = this; // Вернуть указатель на текущий объект AddRefO; // Увеличить счетчик ссылок return S OK;
Технология COM 1003 return EJOINTERFACE; } }: Создание, применение и удаление СОМ-объектов обычно зависит от счетчика ссылок. Единственным исключением является единичный СОМ-объект, который создается в виде глобальной переменной и поэтому не нуждается в удалении. Следовательно, СОМ-объект обычно содержит хотя бы одну переменную — счетчик ссылок. При создании СОМ-объекта счетчик ссылок инициализируется нулевым значением. Метод AddRef увеличивает счетчик ссылок; этот метод должен вызываться при создании нового указателя на СОМ-объект. Метод Release уменьшает счетчик ссылок и вызывается в том случае, когда указатель на СОМ-объект перестает использоваться. Если счетчик ссылок упал до 0, соответствующий СОМ-объект (кроме единичных объектов) можно удалить. Класс KUnknown предполагает, что его экземпляры создаются в куче оператором new, поэтому они должны удаляться оператором delete. Соответствие между вызовами AddRef и Release чрезвычайно важно для работы программ СОМ. Лишний вызов AddRef не позволит удалить неиспользуемый СОМ-объект, что вызовет утечку памяти/ресурсов. Лишний вызов Release приведет к преждевременному удалению СОМ-объекта, и вероятнее всего — к ошибкам защиты. СОМ-объект должен предоставить реализацию для всех СОМ-интерфейсов, от которых он является производным. Следовательно, он должен реализовывать как минимум интерфейс I Unknown; вероятно, наряду с I Unknown будут реализованы и другие интерфейсы. Метод Query Interface позволяет клиенту СОМ-класса узнать, поддерживается ли тот или иной интерфейс. Query Interface получает ссылку на GUID, возвращает указатель на СОМ-объект, преобразованный к типу конкретного СОМ-интерфейса, а также признак успеха или неудачи. В классе KUnknown, реализующем единственный интерфейс IUnknown, метод Query Interface проверяет, равен ли переданный идентификатор GUID идентификатору IID_ IUnknown. Если идентификаторы совпадают, метод возвращает указатель this, увеличивает счетчик ссылок и возвращает код S0K; в противном случае возвращается код ошибки ENOINTERFACE. Указатель, возвращаемый функцией Querylnterface, называется указателем на объект интерфейса, или просто интерфейсным указателем. Строго говоря, интерфейсный указатель не является указателем на СОМ-интерфейс, поскольку интерфейс — всего лишь спецификация, «условность», а не реально существующий объект. Интерфейсный указатель указывает на адрес СОМ-объекта; первое двойное слово по этому адресу содержит указатель на таблицу указателей на реализации виртуальных методов, определенных в СОМ-интерфейсе. Проще говоря, интерфейсный указатель ссылается на другой указатель, который, в свою очередь, ссылается на массив реализаций виртуальных методов. В нашем простом примере с одним интерфейсом интерфейсный указатель совпадает с указателем на объект. С каждым СОМ-классом также связывается однозначно идентифицирующий его идентификатор GUID. Идентификаторы GUID СОМ-классов обычно относятся к отдельному типу CLSID, формат которого в точности совпадает с форматом GUID.
1004 Глава 18. DirectDraw и непосредственный режим Direct3D Создание СОМ-объекта Итак, у нас имеется СОМ-интерфейс и СОМ-класс. Как воспользоваться ими в других компонентах? Преимущества технологии СОМ главным образом обусловлены четким отделением интерфейсов от реализации, из чего следует, что клиентские компоненты СОМ-класса не видят объявления этого класса. Если объявление класса недоступно, вы не сможете создать новый экземпляр класса оператором new, удалить объект оператором delete или создать СОМ-объект в стеке. Чтобы клиентские компоненты могли создавать объекты, в СОМ определяется обобщенный СОМ-интерфейс ICIassFactory, отвечающий за создание СОМ- объектов. Createlnstance, главный метод ICIassFactory, получает GUID интерфейса, создает новый СОМ-объект и возвращает интерфейсный указатель. К СОМ- классам обычно прилагается специальный класс (называемый фабрикой класса), предназначенный исключительно для создания экземпляров формального класса. СОМ-классы обычно реализуются в виде библиотеки DLL, главная экспортируемая функция которой DllGetClassObject определяется следующим образом: STDAPI DllGetClassObject(REFCLSID rclsid. REFIID riid. LPVOID * ppv); Функция DllGetClassObject DLL COM проверяет GUID всех классов текущей библиотеки DLL. Обнаружив совпадение, она находит нужную фабрику класса и возвращает указатель на объект фабрики класса, который может использоваться для создания одного или нескольких экземпляров СОМ-класса. Операционная система должна регистрировать новые библиотеки DLL COM, чтобы точно знать, где они находятся и какие СОМ-классы реализуют. Общий способ создания СОМ-объектов заключается в использовании функции CoCreate- Instance Win32 API. Функция CoCreatelnstance получает CLSID СОМ-класса и IID СОМ-интер- фейса, ищет в реестре нужный компонент СОМ, загружает его в адресное пространство приложения, находит функцию DllGetClassObject и вызывает ее для создания СОМ-объекта. HRESULT Большинство методов СОМ-интерфейсов возвращают 32-разрядную знаковую величину типа HRESULT. Исключение составляют методы AddRef и Release. Тип HRESULT состоит из трех полей, в которых кодируется признак успешного вызова метода, описание подсистемы, в которой произошел сбой, и код статуса. Старший бит (31) HRESULT содержит 0, если вызов был успешным, или 1 в случае ошибки. Биты с 25 по 16 образуют 11-разрядный код подсистемы. Биты с 15 по 0 образуют 16-разрядный код статуса. Самая важная информация хранится в старшем бите HRESULT. Признак успешного вызова проверяется макросом SUCCEEDED. Этот макрос определяет, является ли HRESULT неотрицательной величиной. У макроса SUCCEEDED имеется парный макрос FAILED, который проверяет, что HRESULT соответствует отрицательной величине. Методы СОМ обычно возвращают S0K (0) в случае успешного завер-
Технология COM 1005 шения, однако сравнивать HRESULT с S_0K не рекомендуется. Методы DirectDraw обычно возвращают признак успешного завершения DD_0K (0). Код подсистемы, как правило, заносится в HRESULT лишь в случае неудачного вызова, чтобы программа могла в какой-то степени локализовать ошибку. В DirectX используются коды подсистемы 0x876 и 0x878. Ниже показано, как формируется значение HRESULT для ошибок DirectDraw/Direct3D. #define _FACDD 0x876 #define MAKE_DDHRESULT(code) MAKE_HRESULT(1. JACDD. code) Например, при создании поверхности DirectDraw с недопустимым форматом пикселов (код DDERRJNVALIDPIXELFORMAT) HRESULT = MAKEDDHRESULT (145). В DirectX определено свыше 200 кодов ошибок HRESULT, поскольку очень важно, чтобы в случае ошибки приложение смогло обнаружить ее возможные причины. DirectX и СОМ В DirectX используются десятки интерфейсов и классов СОМ, однако каноны модели СОМ соблюдаются не в полной мере. Самое заметное нарушение заключается в том, что СОМ-объекты DirectX либо создаются специальной экспортируемой функций, либо строятся на базе существующих СОМ-объектов, не используя родовую функцию CoCreatelnstance. Центральное место в DirectDraw и в непосредственном режиме Direct3D занимает серия интерфейсов IDirectDraw. Опубликованный (то есть формально документированный, распространяемый и используемый) СОМ-интерфейс изменить уже нельзя. Чтобы включить в него новые функциональные возможности, приходится создавать новый интерфейс. Например, после исходного интерфейса IDirectDraw появились интерфейсы IDirectDraw2, IDirectDraw4 и последний интерфейс IDirectDraw7, используемый в DirectX 7.O. DirectDraw экспортирует специальную функцию для создания объекта DirectDraw с поддержкой интерфейса IDirectDraw7: HRESULT WINAPI DirectDrawCreateEx(GUID * lpGUID. LPV0ID * IplpDD. REFIID iid. IUnknown * pUnkOuter): В первом параметре передается указатель на GUID, определяющий графическое устройство с поддержкой DirectDraw/Direct3D на уровне аппаратной реализации, аппаратной реализации на втором мониторе или программной эмуляции. Если передается NULL, используется активное устройство вывода. Константа DDCREATEJMULATEONLY выбирает программную эмуляцию, a DDCREATEHARDWARE0NLY - реализацию с аппаратным ускорением. Данная возможность особенно удобна при тестировании программы и диагностике проблем, встретившихся в другой реализации. Перечисление текущих реализаций DirectDraw/Direct3D в системе осуществляется функцией DirectDrawEnumerateEx. При помощи этой функции ваша программа может найти реализацию, отвечающую ее требованиям. Во втором параметре передается указатель на переменную, в которой сохраняется интерфейсный указатель при успешном создании объекта DirectDraw функцией DirectDrawCreateEx. Третий параметр может быть равен только IID_ IDirectDraw7 — GUID интерфейса IDirectDraw7. Последний параметр зарезерви-
1006 Глава 18. DirectDraw и непосредственный режим Direct3D рован для обеспечения совместимости с механизмом агрегирования СОМ и в настоящее время должен быть равен NULL. Ниже приведен пример инициализации среды DirectDraw функцией Direct- DrawCreateEx. void Demo_DirectDrawCreateEx(KLogWindow * pLog) { IDirectDraw7 * pDD = NULL; HRESULT hr - DirectDrawCreateEx(NULL. (void **) & pDD. IID_IDirectDraw7. NULL): if ( FAILED(hr) ) { pLog->Log("DirectDrawCreateEx failed (%x)". hr); return; } ChecklnterfaceCpLog, pDD. IID_IDirectDraw7, "IDirectDraw7"); CheckInterface(pLog, pDD. IID_IDirectDraw4, "IDirectDraw4"); ChecklnterfaceCpLog. pDD, IID_IDirectDraw2. "IDirectDraw2"); CheckInterface(pLog. pDD, IID_IDirectDraw. "IDirectDraw" ); CheckInterface(pLog. pDD. IID_IUnknown, "IUnknown" ); CheckInterface(pLog, pDD. IID_IDDVideoPortContainer, "IDDVideoPortContainer" ); CheckInterface(pLog, pDD. IID_IDirectDrawKernel. "IIDJDirectDrawKernel" ); ChecklnterfaceCpLog. pDD. IID_IDirect3D7, "IDirect3D7"); CheckInterface(pLog, pDD, IID_IDirect3D3. "IDirect3D3"): pDD->Release(); } Функция Demo_Di rectDrawCreateEx обращается к системе с требованием создать объект DirectDraw и вернуть интерфейсный указатель IDirectDraw7. Если в системе установлена библиотека DirectX 7.0, функция проверяет поддержку других СОМ-интерфейсов при помощи функции Checklnterface. Функция Checklnterface использует Query Interface для получения нового интерфейсного указателя, выводит данные в служебном окне и освобождает указатель. Наконец, DemoDi rectDrawCreateEx освобождает объект DirectDraw методом Release. Эксперимент показывает, что объект DirectDraw, созданный функцией DirectDrawCreateEx, поддерживает все перечисленные интерфейсы, кроме IDirect3D3. На рис. 18.1 в традиционном формате диаграмм СОМ изображены СОМ-интерфей- сы, поддерживаемые объектом DirectDraw. СОМ-объект с поддержкой всех интерфейсов, показанных на рисунке, имеет очень сложную структуру — особенно при смешанной поддержке интерфейсов DirectDraw и Direct3D. По указателям, возвращаемым функцией Querylnterface, можно заметить, что объект DirectDraw не создается как единое целое. Система достаточно умна, чтобы создавать и инициализировать части объекта по мере необходимости.
Общие сведения о DirectDraw 1007 lunknown О IDirectDraw7 О— IDirectDraw4 о— IDirectDraw2 О—г I DirectDraw о— IDDVideoPortContanter о— IDirectDrawKernel О— IDirect3D7 О— ... О— Рис. 18.1. СОМ-интерфейсы, поддерживаемые объектом DirectDraw Как упоминалось выше, интерфейсный указатель ссылается на указатель на таблицу виртуальных функций. Если СОМ-объект создается компилятором C++, последний собирает таблицу виртуальных функций в области постоянных данных программы и генерирует код для занесения указателя на таблицу виртуальных функций во вновь созданный объект. Таблица виртуальных функций объекта DirectDraw собирается во время работы программы в глобальном сегменте данных, доступном для чтения/записи. Такой подход позволяет легко выбрать нужную реализацию в зависимости от текущей настройки системы и даже перехватывать вызовы методов DirectX в отладочных целях. Методика мониторинга СОМ-интерфейсов DirectX описана в разделе «Отслеживание СОМ-интерфей- сов DirectDraw» главы 4. Общие сведения о DirectDraw Хотя СОМ-интерфейсы основаны на абстрактных базовых классах C++, работать с СОМ-интерфейсами значительно сложнее, чем с классами C++. СОМ- интерфейсы разрабатываются прежде всего для того, чтобы компоненты легко работали друг с другом на двоичном уровне, а не для упрощения работы программиста на уровне исходных текстов. Как правило, для работы с СОМ-интерфейсами пишутся оболочки в виде классов C++. СОМ-интерфейсы DirectX ничуть не лучше других СОМ-интерфейсов. Они содержат ограниченное число методов со сложными параметрами, объединяемыми в громадных структурах, и десятками всевозможных флагов. Например, для вывода растра на поверхности в GDI можно воспользоваться такими функциями, как PatBlt, BitBlt, StretchBlt, PlgBlt, MaskBlt, TransparentBlt и AlphaBlend. В DirectDraw для той же цели предусмотрены всего два метода: BltFast и Bit. Учитывая сложность полного описания интерфейсов DirectDraw, мы не будем вдаваться в подробности использования каждого метода. Вместо этого мы Объект DirectDraw
1008 Глава 18. DirectDraw и непосредственный режим Direct3D рассмотрим методы в контексте классов C++ и примерах вывода. Полная информация о любом СОМ-интерфейсе и его методах приведена в MSDN. Интерфейс IDirectDraw7 В листинге 18.1 приведен класс KDirectDraw, который представляет собой несложную оболочку для работы с интерфейсом IDirectDraw7. Листинг 18.1. Класс для работы с интерфейсом IDirectDraw #define SAFE_RELEASE(inf) { if (inf) { inf->Release(); inf - NULL; }} // Оболочка для интерфейса IDirectDraw7 с поддержкой первичной поверхности class KDirectDraw { protected: IDirectDraw7 * m_pDD: RECT m_rcDest; // Приемный прямоугольник KDDSurface m_primary; virtual HRESULT DischargeCvoid); public; KDirectDraw(void); virtual -KDirectDraw(void) { Discharge(); } void SetClientRectCHWND hWnd): virtual HRESULT SetupDirectDraw(HWND hTop. HWND hWnd. int nBufferCount=0. bool bFullScreen = false, int width=0. int height=0. int bpp=0); }: KDirectDraw: :KDirectDraw(void) { m_pDD = NULL: } HRESULT KDirectDraw::Discharge(void) { m_primary.Di scharge(); SAFE_RELEASE(m_pDD); return S_OK: } void KDirectDraw::SetClientRect(HWND hWnd) {
Общие сведения о DirectDraw 1009 GetCli entRect(hWnd. & m_rcDest); ClientToScreen(hWnd. (POINT*)& m_rcDest.left); ClientToScreen(hWnd. (POINT*)& m_rcDest.right); HRESULT KDirectDraw::SetupDirectDraw(HWND hTop. HWND hWnd, int nBufferCount. bool bFullScreen, int width, int height, int bpp) { HRESULT hr - DirectDrawCreateEx(pDriverGUID. (void **) &m_pDD. IID_IDirectDraw7. NULL): if ( FAILEDC hr ) ) return hr; if ( bFullScreen ) hr - m_pDD->SetCooperativeLevel(hTop. DDSCLJULLSCREEN | DDSCLJXCLUSIVE); else hr - m_pDD->SetCooperativeLevel(hTop. DDSCL_NORMAL): if ( FAILED(hr) ) return hr; if ( bFullScreen ) { hr - m_pDD->SetDisplayMode(width, height, bpp. 0. 0); if ( FAILED(hr) ) return hr; SetRect(& m_rcDest. 0. 0. width, height): } else SetClientRect(hWnd): hr « m_primary.CreatePrimarySurface(m_pDD. nBufferCount); if ( FAILED(hr) ) return hr; if ( ! bFullScreen ) hr = m_primary.SetClipper(m_pDD. hWnd); return hr; } В классе KDirectDraw определяются три переменные: интерфейсный указатель m_pDD на IDirectDraw7, приемный прямоугольник mrcDest и первичная поверхность вывода m_primary. Поверхность представлена классом KDDSurface, о котором речь пойдет ниже. Конструктор присваивает mpDD указатель NULL, метод Discharge освобождает выделенные ресурсы (этот метод вызывается в деструкторе). Метод SetupDirectDraw создает объект DirectDraw и осуществляет подготовку к выводу средствами DirectDraw. Метод получает семь параметров: манипулятор окна верхнего уровня, манипулятор дочернего окна, использующего DirectDraw, количество резервных буферов, флаг полноэкранного режима и три целых
1010 Глава 18. DirectDraw и непосредственный режим Direct3D числа, определяющих формат экрана в полноэкранном режиме. Метод Setup- DirectDraw создает объект DirectDraw вызовом функции DirectDrawCreateEx, возвращающей интерфейсный указатель на IDirectDraw7 в переменной m_pDD. Если выполнение функции прошло успешно, вызывается метод IDirectDraw7: :Set- CooperativeLevel, который передает манипулятору главного окна информацию о том, какой нужен режим — полноэкранный или оконный. Полноэкранные программы DirectX обычно относятся к категории игровых или обучающих. Как правило, такие программы присваивают монопольные права на распоряжение всеми ресурсами DirectX. DirectX также поддерживает вывод в оконном режиме и даже одновременный вывод в нескольких окнах несколькими экземплярами DirectDraw. Полноэкранная программа DirectX обычно изменяет разрешение pi цветовой формат экрана в соответствии со своими потребностями. Например, программы, использующие анимацию на базе палитры, должны переключить экран в режим с 256 цветами; программы, стремящиеся добиться максимального быстродействия, могут переключиться в режим с пониженным разрешением, чтобы уменьшить затраты видеопамяти и объем пересылаемых данных. Метод SetupDi rectDraw переключает видеорежим при помощи метода IDirectDraw::SetDisplayMode. Полноэкранная программа должна произвести перечисление поддерживаемых видеорежимов методом IDi rectDraw: :Enumerate- DisplayModes, иначе попытка переключения может завершиться неудачей. Например, многие видеоадаптеры поддерживают видеорежимы с 32-разрядным цветом, но не поддерживают 24-разрядных цветов. Метод SetupDi rectDraw также вычисляет прямоугольник поверхности вывода и сохраняет его в переменной m_rcDest. В полноэкранном режиме приемный прямоугольник соответствует всему экрану; в оконных режимах — клиентской области окна, определяемого параметром hWnd. Обратите внимание: при вызове SetupDi rectDraw передаются два манипулятора, поэтому мы не ограничены использованием DirectDraw только в главном окне. В завершение метод SetupDi rectDraw создает первичную графическую поверхность и настраивает в ней отсечение. Для решения этих задач нам понадобится класс KDDSurface. Интерфейс IDirectDrawSurface7 Все операции вывода в DirectDraw осуществляются с поверхностями, отдаленно напоминающими контексты устройств GDI. Поверхности DirectDraw могут представлять текущий экран или внеэкранный буфер, находящийся в памяти. Для первого случая можно провести аналогию с экранным контекстом устройства, а для второго — с совместимым контекстом устройства, созданным на базе DIB-секции. В настоящее время для работы с поверхностями DirectDraw используется интерфейс IDirectDrawSurface7, содержащий около 50 методов. Если прикинуть, скольким функциям GDI передается манипулятор контекста устройства, возникает желание дополнить IDirectDrawSurface новыми методами, чтобы упростить программирование. Некоторые базовые методы IDirectDrawSurface будут описаны по мере их использования в классе KDDSurface. Класс KDDSurface не только является простой
Общие сведения о DirectDraw 1011 оболочкой для работы с интерфейсом, но и содержит немало методов, упрощающих работу с поверхностями DirectDraw. В листинге 18.2 приведено объявление класса KDDSurface, а фрагменты реализации будут приводиться по мере надобности. Листинг 18.2. Класс для работы с интерфейсом IDirectDrawSurface7 class KDDSurface { protected: IDirectDrawSurface7 * m_pSurface; DDSURFACEDESC2 m_ddsd; HDC m hDC: public: KDDSurfaceO: virtual void Discharge(void): virtual -KDDSurfaceO DischargeC); // Освобождение ресурсов // перед вызовом деструктора // Освобождение всех ресурсов operator IDirectDrawSurface7 * & О return m_pSurface: operator HDC О return m_hDC; nt GetWidth(void) const return m_ddsd.dwWidth; nt GetHeight(void) const return m_ddsd.dwHeight; HRESULT CreatePrimarySurface(IDirectDraw7 * pDD. int nBackBuffer): const DDSURFACEDESC2 * GetSurfaceDesc(void): virtual HRESULT RestoreSurface(void); // Восстановление // потерянных поверхностей // Блиттинг в DirectDraw HRESULT SetClipper(IDirectDraw7 * pDD. HWND hWnd); HRESULT BltCLPRECT prDest. IDirectDrawSurface7 * pSrc, Продолжение &
1012 Глава 18. DirectDraw и непосредственный режим Direct3D Листинг 18.2. Продолжение LPRECT prSrc. DWORD dwFlags. LPDDBLTFX pDDBltFx=NULL) { return m_pSurface->Blt(prDest, pSrc. prSrc. dwFlags. pDDBltFx); } DWORD ColorMatchCBYTE red, BYTE green. BYTE blue); HRESULT FillColor(int xO. int yO. int xl. int yl. DWORD fillcolor): HRESULT BitBltCint x. int y. int w. int h. IDirectDrawSurface7 * pSrc. DWORD flag-O): HRESULT BitBltCint x. int y. KDDSurface & src. DWORD flag-O) { return BitBltCx. y. src.GetWidthO. src.GetHeightO. src. flag); } HRESULT SetSourceColorKeyCDWORD color); // Вывод с использованием контекста устройства GDI HRESULT GetDC(void); // Получение манипулятора DC HRESULT ReleaseDC(void): HRESULT DrawBitmapCconst BITMAPINFO * pDIB. int dx. int dy. int dw. int dh); // Прямой доступ к пикселам BYTE * LockSurfaceCRECT * pRect=NULL); HRESULT Unlock(RECT * pRect=NULL); int GetPitch(void) const { return m ddsd.lPitch; Класс KDDSurface содержит три переменные: указатель на интерфейс IDirect- DrawSurface7, структуру с описанием поверхности и манипулятор контекста устройства GDI. Указатель на IDirectDrawSurface7 возвращается системой при создании поверхности DirectDraw, и все взаимодействие с поверхностью происходит через этот указатель. Структура DDSURFACEDESC2 описывает формат поверхности. В ней хранятся важнейшие атрибуты поверхности — тип, ширина, высота, смещение строк развертки, адрес, формат пикселов и т. д. С каждой поверхностью DirectDraw может быть связан манипулятор контекста устройству GDI, позволяющий осуществлять вывод на поверхности DirectDraw средствами GDI. Хотя поверхности DirectDraw создаются всего одним методом IDirectDraw7:: CreateSurface, существует несколько способов создания поверхности. В классе KDDSurface предусмотрены дополнительные методы, упрощающие создание поверхностей. Ниже приведен конструктор класса KDDSurface и метод создания первичной поверхности, используемой классом KDirectDraw. KDDSurface;;KDDSurface() {
Общие сведения о DirectDraw 1013 m_pSurface - NULL; mJiDC - NULL: mjiDCRef - 0; memset(& mjJdsd. 0. sizeof(m_ddsd)); m_ddsd.dwSize - sizeof(m_ddsd); } HRESULT KDDSurface::CreatePrimarySurface(IDirectDraw7 * pDD. int nBufferCount) { if ( nBufferCount==0 ) { m_ddsd.dwFlags - DDSD_CAPS; m_ddsd.ddsCaps.dwCaps - DDSCAPS_PRIMARYSURFACE; } else { m_ddsd.dwFlags - DDS0_CAPS | DDSDJACKBUFFERCOUNT: mJdsd.ddsCaps.dwCaps - DDSCAPS_PRIMARYSURFACE | DDSCAPSJLIP | DDSCAPS_COMPLEX | DDSCAPSJIDEOMEMORY: m_ddsd.dwBackBufferCount = nBufferCount; } return pDD->CreateSurface(& m_ddsd. & m_pSurface. NULL); } В полноэкранных программах DirectX видеоадаптер может поддерживать простые поверхности, а также поверхности с двумя или тремя буферами. Простая поверхность состоит из одного буфера, в котором производится весь вывод и по содержимому которого генерируется видеосигнал. Поверхность с двумя буферами содержит два буфера: один буфер отображается на экране, а во втором выполняются операции вывода. Переключение буферов в DirectX выполняется методом IDirectDrawSurface7::Flip. Также существуют поверхности с тремя буферами: один буфер отображается, другой ждет отображения, а в третьем выполняются операции вывода. Поверхности с двумя и тремя буферами играют важную роль для обеспечения плавного вывода без мерцания. Впрочем, они возможны только в полноэкранном монопольном режиме, поскольку аппаратное переключение буфера может выполняться только на всем экране, но не в отдельном окне. Чтобы организовать качественный вывод в оконной программе DirectDraw, вам придется использовать внеэкранную поверхность и самостоятельно копировать ее содержимое на первичную поверхность. Метод CreatePrimarySurface получает указатель на интерфейс IDirectDraw7 и количество вторичных буферов. Если количество вторичных буферов равно 0, метод устанавливает в структуре DDSURFACEDESC2 два флага создания простой первичной поверхности; в противном случае устанавливаются дополнительные флаги и присваивается значение полю количества вторичных буферов. Переменная mddsd, относящаяся к типу DDSURFACEDESC2, частично инициализируется в конструкторе класса.
1014 Глава 18. DirectDraw и непосредственный режим Direct3D Вывод на поверхности DirectDraw От создания поверхности DirectDraw можно переходить к графическому выводу. Существует три варианта вывода на поверхности DirectDraw: методами IDirect- DrawSurface7, использующими аппаратное ускорение, средствами GDI или прямыми операциями с пикселами кадрового буфера поверхности. Вывод с аппаратным ускорением Интерфейс IDirectDrawSurface содержит всего три метода вывода: Bit, BltFast и BltBatch (причем последний метод не реализован). Поскольку методы Bit и BltFast могут ускоряться на аппаратном уровне, рекомендуется использовать их всюду, где это возможно, чтобы добиться хорошего быстродействия. Ниже приведено объявление метода Bit. HRESULT BltCLPRECT IpDestRect. LPDIRECTDRAWSURFACE7 IpDDSrcSurface, LPRECT IpSrcRect. DWORD dwFlags. LPDDBLTFX IpDDBltFx); Метод Bit напоминает функцию StretchBlt GDI — он тоже копирует прямоугольный участок поверхности-источника в прямоугольный участок приемной поверхности. Приемная поверхность определяется текущим указателем на IDirectDrawSurface/, а приемный прямоугольник задается параметром IpDestRect. Источник определяется параметром IpDDSrcSurface, а исходный прямоугольник — параметром IpSrcRect. В параметре dwFlags передаются флаги, управляющие процессом блиттинга, а последний параметр содержит указатель на структуру DDBLTFX с дополнительными управляющими полями. Простейшим применением функции Bit является заполнение приемного прямоугольника однородным цветом (по аналогии с функцией PatBlt). Ниже приведен метод KDDSurface: -.Fill Col or, инкапсулирующий однородную заливку. HRESULT KDDSurface::FillColor(int xO. int yO, int xl. int yl. DWORD fillcolor) { DDBLTFX fx; fx.dwSize = sizeof(fx); fx.dwFillColor = fillcolor; RECT re - { xO. yO. xl. yl }; return m_pSurface->Blt(& re. NULL. NULL. DDBLT_COLORFILL. & fx); } Метод Fill Col or заполняет структуру RECT четырьмя переданными параметрами. Поверхность и прямоугольник источника в данном случае не нужны. Параметр dwFlags равен DDBLTCOLORFILL, а структура DDBLTFX в основном определяет цвет заливки. Вывод средствами GDI Интерфейс DirectDraw разрабатывался для того, чтобы программисты могли отойти от GDI. Впрочем, уходить слишком далеко все равно не удастся — время от времени вам понадобится помощь со стороны GDI. Хотя технология DirectDraw обеспечивает вывод с аппаратным ускорением, функции вывода в ней очень ограничены. В DirectX GDI по-прежнему занимает важное место при вы-
Общие сведения о DirectDraw 1015 воде текста и инициализации поверхности растрами. Чтобы использовать GDI для работы с поверхностью DirectDraw, следует вызвать метод IDirectDrawSur- face::GetDC для получения манипулятора контекста устройства GDI. Полученный манипулятор позднее можно освободить методом ReleaseDC. Ниже приведены методы для вызовов GetDC и ReleaseDC, а также метод для вывода DIB на поверхности DirectDraw средствами GDI. HRESULT KDDSurface::GetDC(void) { return m_pSurface->GetDC(&m_hDC); } HRESULT KDDSurface::ReleaseDC(void) { if ( m_hDC==NULL ) return S_0K; HRESULT hr = m_pSurface->ReleaseDC(m_hDC); m_hDC « NULL: return hr; } HRESULT KDDSurface::DrawBitmap(const BITMAPINFO * pDIB. int x. int y. int w, int h) { if ( SUCCEEDED(GetDCO) ) { StretchDIBits(m_hDC. x. y. w. h. 0. 0. pDIB->bmiHeader.biWidth. pDIB->bmiHeader.biHeight. & pDIB->bmiColors[GetDIBColorCount(pDIB)], pDIB. DIB_RGB_C0L0RS. SRCCOPY); return ReleaseDCO: } else return E_FAIL; } Метод DrawBitmap выводит упакованный аппаратно-независимый растр на поверхности DirectDraw. При этом используется функция StretchDIBits, идеально подходящая для загрузки растров на поверхность DirectDraw. Если быстродействие критично, функция DrawBitmap требуется только для загрузки растра на внеэкранную или текстурную поверхность, которая затем выводится на первичной поверхности аппаратно-ускоренным методом Bit. В большинстве книг по DirectX для загрузки растра применяются DDB и DIB-секции в сочетании с совместимыми контекстами устройств, для чего приходится создавать два объекта GDI. Я предпочитаю загрузку растра с использованием DIB, поскольку при этом не изменяются цвета (как при использовании DDB) и не расходуются дополнительные ресурсы GDI. Манипулятор DC, возвращаемый методом IDirectDrawSurface7::GetDC, интерпретируется как манипулятор совместимого контекста устройства. Если вызвать для него функцию GetObjectType, GDI вернет 0BJMEMDC. Впрочем, его не стоит принимать за обычный манипулятор совместимого контекста устройства, поскольку он не был создан функцией CreateCompatibleDC или хотя бы CreateDC.
1016 Глава 18. DirectDraw и непосредственный режим Direct3D Этот манипулятор создается специальной системной функцией NtGdiDcGetDC. Зная манипулятор DC, можно воспользоваться вызовом GetCurrentObjectCmhDC, OBJBITMAP) для получения растра, выбранного в контексте; функция возвращает манипулятор DIB-секции. Если после этого запросить описание DIB-секции функцией GetObject, заполняется вполне нормальная структура DIBSection. Единственное отличие состоит в том, что указатель на графические данные ссылается на адресное пространство режима ядра, что не позволяет обратиться к нему в пользовательском режиме. Тем не менее эта DIB-секция отличается от обычных, поскольку поверхности DirectDraw могут иметь странные форматы пикселов, не относящиеся к стандартным форматам DIB. Например, некоторые драйверы экрана могут поддерживать 8-разрядные поверхности RGB в формате 2-3-2 или 16-разрядные поверхности RGB в формате 4-4-4-4. Прямой доступ к пикселам В некоторых ситуациях даже комбинация функций Bit, BltFast и функций GDI не решает всех проблем. Допустим, вы просто хотите изменить цвет одного пиксела на поверхности DirectDraw; вызывать для этого функцию Bit или функцию GDI было бы слишком долго. DirectDraw позволяет получить доступ к кадровому буферу поверхности посредством фиксации (locking). Метод IDirectDrawSur- face7: -.Lock отображает кадровый буфер поверхности в блок памяти, адресуемый в пользовательском режиме. Фиксация одинаково работает как для первичной поверхности, так и для внеэкранных поверхностей. Работа с зафиксированной поверхностью через указатель на кадровый буфер практически не отличается от работы с массивом пикселов DIB или DIB-секции, что позволяет использовать множество интересных алгоритмов. Фиксация поверхностей навевает воспоминания о старых игровых DOS-программах, которые напрямую работали с видеопамятью и добивались высокого быстродействия, недостижимого средствами GDI. Ниже приведены методы фиксации и освобождения поверхностей. BYTE * KDDSurface::LockSurface(RECT * pRect) { if ( FAILED(m_pSurface->Lock(pRect. & m_ddsd. DDLOCKJURFACEMEMORYPTR | DDLOCK_WAIT. NULL)) ) return NULL; else return (BYTE *) m_ddsd.lpSurface: } HRESULT KDDSurface::Unlock(RECT * pRect) { m_ddsd.lpSurface = NULL: // Содержимое поверхности // становится недоступным return m_pSurface->Unlock(pRect); } Метод LockSurface фиксирует прямоугольный участок поверхности, вызывая метод IDirectDrawSurface7::Lock, что приводит к заполнению структуры DDSURFACEDESC2. Самые важные поля заполненной структуры содержат информацию о формате пикселов поверхности, ширине, высоте, смещении строк развертки, а также указатель на кадровый буфер. Еойи при вызове метода Lock передается допустимый
Общие сведения о DirectDraw 1017 прямоугольник, указатель IpSurface ссылается на левый верхний пиксел этого прямоугольника; в противном случае он относится к первому пикселу поверхности. Метод Unl ock освобождает зафиксированную поверхность. Указатель, возвращаемый методом Lock, может использоваться для непосредственной работы с содержимым поверхности, однако необходима крайняя осторожность, поскольку прямой доступ не учитывает отсечения. Обращение к пикселам, находящимся за допустимыми границами, приведет к ошибкам защиты или порче содержимого других окон (если программа работает в оконном режиме). Приложение должно самостоятельно реализовать необходимое отсечение. Приведенная ниже функция уже не ограничивается простой закраской участка поверхности однородным цветом. BOOL PixelFillRect(KDDSurface & surface, int x. int y. int width, int height. DWORD dwColor[]. int nColor) { BYTE * pSurface - surface.LockSurface(NULL); const DDSURFACEDESC2 * pDesc - surface.GetSurfaceDescO; if (pSurface) { int pitch = surface.GetPitchO; int byt - pDesc->ddpfPixelFormat.dwRGBBitCount / 8; for (int j=0; j<height; j++) { BYTE * pS - pSurface + (y+j) * pitch + x * byt: DWORD color = dwColor[j % nColor]; int i; switch (byt) { case 1: memset(pS. color, width): break: case 2: for (i=0: i<width; i++) { * (unsigned short *) pS - (unsigned short) color; pS +- sizeof(unsigned short): } break: case 3: for (i»0; i<width: i++) { * (RGBTRIPLE *) pS - * (RGBTRIPLE *) & color: pS +- sizeof(RGBTRIPLE): } break: case 4: for (i=0: i<width; i++)
1018 Глава 18. DirectDraw и непосредственный режим Direct3D { * (unsigned *) pS = color; pS += sizeof(unsigned); } break; default: return FALSE; } } surface.UnlockO; return TRUE: } else return FALSE; } Функция PixelFillRect заполняет прямоугольную область разноцветными горизонтальными линиями. Она получает указатель на поверхность функцией LockSurface, вычисляет адрес графических данных в соответствии с форматом пикселов, а затем прямым копированием данных в кадровый буфер поверхности рисует линию за линией, пиксел за пикселом. Методы Blt/BltFast, вывод средствами GDI и прямой доступ к пикселам являются взаимоисключающими. При открытом манипуляторе контекста устройства GDI попытка зафиксировать поверхность завершается неудачей; вывод средствами GDI на зафиксированной поверхности тоже ни к чему не приводит. В Windows 95/98 фиксация поверхности обычно сопровождается установкой системного мьютекса, блокирующего другие программные потоки от работы с 16-разрядной реализацией GDI, из-за проблем реентерабельности. Следовательно, поверхности должны фиксироваться лишь в случае необходимости, а когда такая необходимость отпадает, поверхности следует освобождать. Подбор цветов Параметр KDDSurface:: Fill Color, определяющий цвет заливки, относится к типу DWORD вместо типа C0L0RREF, знакомого нам по GDI. Значение задается в физическом цветовом формате конкретной поверхности, а не в общем формате GDI. DirectDraw в действительности является тонкой прослойкой над аппаратным уровнем. Эта прослойка настолько тонка, что в DirectDraw не существует простого способа определения цветов с использованием цветовых каналов RGB. Физические цвета, приемлемые для DirectDraw, зависят от формата пикселов поверхности. Ниже приведен простой способ подбора цветов, реализованный в виде родового метода класса KDDSurface. const DDSURFACEDESC2 * KDDSurface::GetSurfaceDesc(void) { if ( SUCCEEDED(m_pSurface->GetSurfaceDesc(& m_ddsd)) ) return & m_ddsd; else return NULL; }
Общие сведения о DirectDraw 1019 DWORD KDDSurface::ColorMatch(BYTE red. BYTE green. BYTE blue) { if ( m_ddsd.ddpfPixel Format.dwSize==0 ) // Поверхность не инициализирована GetSurfaceDescO; // Получить описание поверхности const DDPIXELFORMAT & pf = mjdsd.ddpf Pixel Format; if ( pf.dwFlags & DDPF_RGB ) { // x-5-5-5 if ( (pf.dwRBitMask — 0x7C00) && (pf.dwGBitMask == ОхОЗЕО) && (pf.dwBBitMask==0x001F) ) return ((red»3)«10) | ((green»3)«5) | (blue»3); // 0-5-6-5 if ( (pf.dwRBitMask == OxF800) && (pf.dwGBitMask == 0x07E0) && (pf.dwBBitMask==0x001F) ) return ((red»3)«ll) | ((green»2)«5) | (blue»3): // x-8-8-8 if ( (pf.dwRBitMask == OxFFOOOO) && (pf.dwGBitMask == OxFFOO) && (pf.dwBBitMask==OxFF) ) return (red«16) | (green«8) | blue; } DWORD rslt = 0; if ( SUCCEEDED(GetDCO) ) // Получить GDI DC { COLORREF old = ::GetPixel(m_hDC. 0. 0); // Сохранить исходный пиксел SetPixeKmJiDC. 0. 0. RGB(red. green, blue)); // Присвоить ReleaseDCO: // пиксел RGB const DWORD * pSurface = (DWORD *) LockSurfaceO; // Зафиксировать if ( pSurface ) { rslt = * pSurface; // Прочитать первое двойное слово if ( pf.dwRGBBitCount < 32 ) rslt &= (1 « pf.dwRGBBitCount) - 1; // Усечение по bpp UnlockO; // Освободить поверхность } else assert(false); GetDCO: SetPixel(m_hDC. 0. 0. old); // Вернуть исходный пиксел ReleaseDCO; // Освободить GDI DC } else assert(false); return rslt;
1020 Глава 18. DirectDraw и непосредственный режим Direct3D Метод Col orMatch преобразует цвет, заданный красным, зеленым и синим каналами, в физический цвет — двойное слово (DWORD), готовое к занесению в кадровый буфер. Сначала он проверяет структуру DDPIXELFORMAT; если проверка оказывается неудачной, вызывается метод GetSurfaceDesc, возвращающий структуру с описанием текущей поверхности. Затем метод Col orMatch сравнением масок каналов пытается определить, относится ли поверхность к одному из стандартных 15-, 16-, 24 или 32-разрядных форматов RGB. Если маски совпадают, Col orMatch объединяет каналы RGB в правильный физический цвет. Если быстрый путь не приводит к успеху, приходится обращаться к GDI. Программа получает для поверхности манипулятор устройства GDI, сохраняет текущее состояние пиксела (0,0) при помощи функции GetPixel, присваивает пикселу (0,0) значение RGB функцией SetPixel, а затем читает физический цвет из зафиксированной поверхности. Перед возвратом из функции исходное состояние пиксела (0,0) восстанавливается еще одним вызовом SetPixel. Поскольку манипуляторы GDI и фиксация поверхности не могут использоваться одновременно, программа освобождает манипулятор DC перед фиксацией поверхности, а затем снова получает его для восстановления измененного пиксела. Как видите, для простого преобразования RGB-значения в физический цвет работы получается слишком много, поэтому результаты вызова Col orMatch следует по возможности использовать многократно. Метод KDDSurface:: Fill Col or получает физический цвет вместо логического, чтобы можно было организовать кэширование физических цветов. Интерфейс IDirectDrawClipper Первичная поверхность, создаваемая DirectDraw, всегда распространяется на весь экран. Она позволяет рисовать в любой точке экрана, как и контекст устройства, возвращаемый вызовом GetDC(NULL). Для ограничения области вывода в DirectDraw поддерживается механизм отсечения с объектами отсечения, абстрагированными в интерфейсе IDirectDrawClipper. Сначала объект отсечения создается, а затем присоединяется к поверхности DirectDraw. Область отсечения задается так называемым списком отсечения (clip list), который представляет собой не что иное, как структуру RGNDATA, используемую GDI при операциях с объектами регионов. Чтобы инициализировать объект отсечения правильным списком отсечения, проще всего ассоциировать его с окном. Операционная система автоматически управляет списком отсечения при перемещении или изменении размеров окна с учетом его видимости. Приведенный ниже метод KDDSurface: .-SetClipper создает объект отсечения, ассоциирует его с окном и присоединяет к поверхности. Этот метод вызывается методом KDirectDraw:: SetupDi rectDraw после создания первичной поверхности. HRESULT KDDSurface::SetClipper(IDirectDraw7 * pDD. HWND hWnd) { IDirectDrawClipper * pClipper: HRESULT hr - pDD->CreateClipper(0, & pClipper, NULL); if ( FAILEDC hr ) ) return hr;
Общие сведения о DirectDraw 1021 pClipper->SetHWnd(0. hWnd); m_pSurface->SetClipper(pClipper); return pClipper->Release(); } Обратите внимание: после вызова IDirectDrawSurface7::SetC1ipper поверхность получает указатель на объект отсечения, что приводит к увеличению счетчика ссылок объекта. Затем вызывается метод IDirectDrawClipper::Release, который освобождает ссылку на объект, хранящуюся в локальной переменной функции. Простое окно DirectDraw У нас имеются все классы и методы, необходимые для конструирования простого окна DirectDraw. В листинге 18.3 приведен несложный, но достаточно полный класс окна, с поддержкой DirectDraw. Листинг 18.3. Простой класс окна DirectDraw class KDDWin : public «Window, public KDirectDraw { void OnNCPaint(void) { RECT rect; GetWindowRect(m_hWnd. & rect); DWORD dwColor[18]; for (int i-0: i<18; i++) dwColor[i] - m_priтагу.ColorMatch(0. 0, 0x80 + abs(i-9)*12); PixelFillRect(m_primary, rect.left+24. rect.top+4, rect.right - 88 - rect.left. 18. dwColor. 18); BYTE * pSurface = m_primary.LockSurface(NULL); m_primary.Unlock(NULL); if ( SUCCEEDED(m_primary.GetDCO) ) { TCHAR temp[MAX_PATH]; const DDSURFACEDESC2 * pDesc = m_primary.GetSurfaceDesc(); if ( pDesc ) wsprintf(temp. "%6x%6 Ud-bpp. pitched. lpSurface=0xfcx", pDesc->dwWidth. pDesc->dwHeight. pDesc->ddpfPixelFormat.dwRGBBitCount. pDesc->lPitch. pSurface); else strcpy(temp. "LockSurface failed"); SetBkMode(m_primary. TRANSPARENT); SetTextColor(m_primary. RGB<0xFF. OxFF. 0)); Text0ut(m_primary. rect.left+24. rect.top+4. Продолжение^
1022 Глава 18. DirectDraw и непосредственный режим Direct3D Листинг 18.3. Продолжение temp. _tcslen(temp)); m_primary.ReleaseDC(); void OnDraw(void) { SetClientRect(m_hWnd); int n = min(m_rcDest.right-m_rcDest.left. m_rcDest.bottom-m_rcDest.top)/2; for (int i=0; i<n; i++) { DWORD color = m_priтагу.ColorMatch( OxFF*(n-l-i)/(n-l). OxFF*(n-l-i)/(n-l). OxFF*i/(n-l) ); m_priтагу.Fill Col or(m_rcDest.left+i. m_rcDest.top+i. m_rcDest.right-i. m_rcDest.bottom-i. color): LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam, LPARAM IParam) { switch( uMsg ) { case WM_CREATE: m_hWnd = hWnd; if ( FAILED(SetupDirectDraw(GetParent(hWnd). hWnd. false)) ) { MessageBox(NULL. _T("Unable to Initialize DirectDraw"). JCKDDWin"). MB_OK); CloseWindow(hWnd): } return 0; case WM_PAINT: OnDrawO; ValidateRect(hWnd. NULL): return 0; case WMJCPAINT: DefWindowProc(hWnd. uMsg. wParam. IParam): OnNCPaintO: return 0; case WM_DESTR0Y: PostQuitMessage(O): return 0: default: return DefWindowProcChWnd. uMsg. wParam. IParam); void GetWndClassExCWNDCLASSEX & wc)
Построение графической библиотеки DirectDraw 1023 { KWindow::GetWndClassEx(wc); wc.style |= (CSJREDRAW | CSJREDRAW): } } Класс KDDWin объявлен производным от классов KWindow и KDirectDraw. Поддержка DirectDraw инициализируется в обработчике сообщения WM_CREATE вызовом метода KDirectDraw: :SetupDirectDraw. Обработчик сообщения WMPAINT использует метод KDDSurface:: Fill Col or для заполнения клиентской области окна прямоугольниками, цвет которых постепенно изменяется от желтого к синему. Этот пример иллюстрирует вывод с аппаратным ускорением с использованием IDirectDraw7: :Blt. Обработчик WMNCPAINT рисует фон заголовка окна функцией Pixel Fill Rect и при помощи функции вывода текста GDI выводит в заголовке строку с описанием формата первичной поверхности (рис. 18.2). Этот простой класс показывает, как легко включить поддержку DirectDraw в обычной оконной программе при помощи классов KDirectDraw и KDDSurface, а также демонстрирует три способа вывода на поверхности DirectDraw. Рис. 18.2. Простой пример вывода DirectDraw в оконном режиме Построение графической библиотеки DirectDraw Как говорилось в предыдущем разделе, DirectDraw поддерживает всего два метода вывода с аппаратным ускорением, Bit и BltFast, позволяющие заполнять прямоугольные участки однородным цветом и копировать фрагменты изображений между поверхностями DirectDraw. Более сложные графические запросы приходится разбивать на серии Blt/BltFast, использовать прямой доступ к пикселам фиксированной поверхности или прибегать к помощи GDI. DirectDraw хорошо подходит для переноса в Windows игровых DOS-программ, которые обычно работают с обширными графическими библиотеками, ограничивающимися прямым доступом к пикселам. Если тексты графической библиотеки недоступны, вам придется строить свою собственную библиотеку или возвращаться к GDI.
1024 Глава 18. DirectDraw и непосредственный режим Direct3D В этом разделе мы рассмотрим пример построения простейшей библиотеки DirectDraw, поддерживающей операции с пикселами, заполнение замкнутых фигур, вывод линий, текста, простых и прозрачных растров. Вывод пикселов Когда поверхность DirectDraw фиксируется в памяти, программа получает указатель на ее кадровый буфер. Вывод пиксела сводится к простому определению его позиции в кадровом буфере и копированию нескольких байтов. Фиксация и освобождение поверхностей DirectDraw связаны с дорогостоящими вызовами системных функций; мы не можем себе позволить такие затраты для каждого пиксела поверхности. Следовательно, архитектура графической библиотеки должна позволять приложению один раз зафиксировать поверхность и вывести сразу несколько пикселов перед ее освобождением. Ниже приведен родовой класс, который обеспечивает фиксацию/освобождение поверхностей DirectDraw и организует прямой доступ к пикселам. class KLockedSurface { public: BYTE * pSurface; int pitch; i nt bpp; bool Initialize(KDDSurface & surface) { pSurface - surface.LockSurface(NULL); pitch = surface.GetPitchO; bpp = surface.GetSurfaceDesc()->ddpfPi xelFormat.dwRGBBi tCount; return pSurface!=NULL; BYTE & ByteAtCint x. int y) BYTE * pPixel - (BYTE *) (pSurface + pitch * y). return pPixel[x]; WORD & WordAt(int x. int y) WORD * pPixel = (WORD *) (pSurface + pitch * y); return pPixel[x]; RGBTRIPLE & RGBTripleAt(int x. int y) RGBTRIPLE * pPixel - (RGBTRIPLE *) (pSurface + pitch * y); return pPixel[x]:
Построение графической библиотеки DirectDraw 1025 DWORD & DWordAtdnt x. int у) { DWORD * pPixel - (DWORD *) (pSurface + pitch * y); return pPixel[x]; BOOL SetPixel(int x, int y, DWORD color) { switch ( bpp ) case 8 case 15 case 16 case 24 case 32 default } return TRUE; ByteAUx. y) - (BYTE) color WordAt(x, y) - (WORD) color RGBTripleAt(x, y) = * (RGBTRIPLE *) & color; break DWordAt(x. y) - (DWORD) color return FALSE; break; break; break; DWORD GetPixeKint x. int у); //Не приводится void Line(int xO. int yO. int xl. int yl. DWORD color); }: В классе KLockedSurface зафиксированная поверхность представлена тремя переменными: указателем на кадровый буфер, смещением соседних строк развертки и цветовой глубиной пикселов. Метод Initialize фиксирует поверхность DirectDraw и присваивает значения этим переменным. Четыре подставляемых (in-line) метода — ByteAt, WordAt, RGBTripleAt и DWordAt — превращают кадровый буфер в двумерный массив с произвольным доступом. Эти методы обеспечивают чтение и запись 8-, 16-, 24- и 32-разрядных пикселов поверхности. Метод KLockedSurface: : SetPixel обеспечивает обобщенный вывод пикселов поверхности, по аналогии с одноименной функцией GDI. Метод KLockedSurface: -.GetPixel выполняет обобщенное чтение пикселов. Ниже приведен пример использования класса KLockedSurface для реализации метода SetPixel в классе KDDSurface. BOOL KDDSurface-SetPixel(int x. int y. DWORD color) { KLockedSurface frame; if ( frame.Initialize(* this) ) { frame.SetPixel(x. y. color); UnlockO; return TRUE; } else return FALSE; } Чтобы класс обладал достаточно высоким быстродействием, вывод нескольких пикселов должен выполняться за одну фиксацию поверхности. Класс KLockedSurface позволяет использовать при выводе пикселов логические или другие растровые операции. Примеры:
1026 Глава 18. DirectDraw и непосредственный режим Direct3D ByteAtCx. у) |= (BYTE) color; // R2_MERGEPEN WordAtCx. у) "= (DWORD) color; // R2J0RPEN DwordAt(x. y) = 0; // R2_BLACK ByteAtCx. y) = ((ByteAt(x-l. y) + ByteAtCx. y-1). (ByteAt(x+l. y) + ByteAtCx. y+D) / 4; // Размытие Обратите внимание на отсутствие отсечения или проверки границ в классе KLockedSurface. Предполагается, что приложение передает предварительно отсеченные координаты. Приведенная ниже функция рисует на поверхности пикселы. void PixelDemo(void) { KLockedSurface frame; if ( ! frame.Initialize(m_primary) ) return; for (int i=0; i<4096: i++) frame.SetPixeK m_rcDest.left + randO % ( m_rcDest.right - m_rcDest.left). m_rcDest.top + rand О % ( m_rcDest.bottom - m_rcDest.top). m_primary.ColorMatch(rand(n256. rand()*256. rand()*256): m_primary.Unlock(); Вывод линий Любую кривую можно разбить на отрезки, вывод которых поддерживается примитивами любой графической библиотеки. При выводе кривых часто применяется алгоритм Брезенхэма (Bresenham), опубликованный в 1965 году. В алгоритме Брезенхэма линии аппроксимируются пикселами дискретной сетки с использованием итеративного процесса, работа которого зависит от накапливаемой погрешности. По погрешности алгоритм определяет, нужно ли при переходе к следующему пикселу обновлять координаты по обеим осям х и у или только по одной оси. При переходе к следующему пикселу погрешность обновляется в соответствии с отклонением аппроксимирующего пиксела от настоящей линии. Алгоритм Брезенхэма хорош тем, что он обходится без дорогостоящих операций умножения и деления (если не считать удвоение величины за умножение). Ниже приведена реализация алгоритма Брезенхэма в классе KLockedSurface. void KLockedSurface::Line(int xO, int yO. int xl. int yl. DWORD color) { int bps = (bpp+7) / 8: // Байт на пиксел BYTE * pPixel = pSurface + pitch * yO + bps * xO; // Адрес первого пиксела int error; // Погрешность int d_pixel_pos, d_error_pos; // Поправки для error>=0 int d_pixel_neg, d_error_neg; // Поправки для error<0 int dots: // Количество выводимых точек { int dx. dy. inc_x. inc_y; if ( xl > xO ) { dx = xl - xO; inc_x = bps; } else
Построение графической библиотеки DirectDraw 1027 { dx = хО - xl; inc_x = -bps; } if ( yl > уО ) { dy = yl - yO; inc_y = pitch; } else { dy = yO - yl; inc_y = -pitch; } d_pixel_pos = inc_x + inc_y; // Переместить х и у d_error_pos = (dy - dx) * 2; if ( d_error_pos < 0 ) // x dominant { dots = dx; error = dy*2 - dx; d_pixel_neg = inc_x; // Перемещение только по оси х d_error_neg = dy * 2; } else { dots = dy: error = dx*2 - dy; d_error_pos = - d_error_pos; d_pixel_neg = inc_y; // Перемещение только по оси у d_error_neg = dx * 2: switch ( bps ) { case 1: // Цикл для 8-разрядных пикселов. См. CD-ROM case 2: // Цикл для 16-разрядных пикселов. См. CD-ROM case 3: // Цикл для 24-разрядных пикселов. См. CD-ROM break; case 4: for (; dots>=0; dots--) // Цикл для 32-разрядных пикселов { * (DWORD *) pPixel = color; // Вывести 32-разрядный пиксел if ( error>=0 ) { pPixel += d_pixel_pos; error += d_error_pos; } else { pPixel += d_pixel_neg; error += d_error_neg; } } break; Метод KLockedSurface: :Line делится на две части: фазу начальной настройки и цикл вывода пикселов. В фазе начальной настройки задается адрес первого пиксела, количество выводимых пикселов, начальная погрешность, поправки адреса пиксела и погрешности. Цикл вывода поддерживает все стандартные форматы пикселов поверхностей. Для каждого формата программа в цикле устанавливает значение пиксела и переходит к следующему пикселу, выбранному в зависимости от погрешности. Для повышения быстродействия вычисление адреса пиксела оформлено «на месте». Практически все ранние графические библиотеки для игровых DOS-программ были написаны на ассемблере. Впрочем, и в наши дни встречается немало книг,
1028^ Глава 18. DirectDraw и непосредственный режим Direct3D рекомендующих программировать графические примитивы на ассемблере. Если вам доводилось просматривать ассемблерный код, сгенерированный современным компилятором, и вы уверены, что справитесь лучше — что ж, попробуйте... но учтите, что неоптимизированный ассемблерный код замедлит работу вашей программы. Если вы хотите посмотреть, на что способен компилятор, сгенерируйте листинги с командами C/C++ и ассемблерным кодом. Вот как выглядит цикл вывода 32-разрядных пикселов, обработанный компилятором VC 6.0: II II II II II II II _repeat: _elsepart _next: eax ebx есх edx esi edi ebp test jl mov inc test mov Jl add add jmp : add add dec jne : color : dots : error : pPixel : d_error_pos : d_error_neg : d_pixel_neg ebx. ebx _finish eax. color ebx ecx. ecx [edx], eax _elsepart edx, d_pixel_pos ecx. esi _next edx. ebp ecx. edi ebx _repeat if ( dots < 0 ) goto _finish; eax = color dots ++; * (DWORD *) pPixel = color; if ( error<0 ) goto _elsepart; pPixel +« d_pixel_pos error +=d_error_pos goto jnext pPixel +- d_pixel_neg error +- d_error_neg dots--; if ( dots!=0 ) goto_repeat На процессоре Intel, работающем в 32-разрядном режиме, имеется 7 регистров общего назначения, которые могут использоваться компилятором. В нашем цикле вывода, определяющем быстродействие вывода линий, компилятору хватило «ума» задействовать все 7 регистров. Места не осталось лишь для одного важного значения — dj)ixe1_pos. В цикле вывода используются всего две операции, операндами которых не являются регистры, — обращение к d_pixe1_pos и запись пиксела в кадровый буфер. Компилятор отделяет инструкцию проверки от последующего относительного перехода, чтобы «включились» оба конвейера обработки инструкций. Метод KLockedSurface::Line можно расширить для вывода стилевых линий, линий с растровыми операциями и даже с альфа-наложением. Впрочем, более толстые линии следует преобразовывать в заливки замкнутых фигур. Также предполагается, что координаты предварительно прошли отсечение. Ниже приведен пример использования метода Line. void LineDemo(KDDSurface & surface, int x, int y. int Radius) { const int N =19; const double theta - 3.1415926 * 2 / N; const COLORREF color[10] - { RGB(0. 0. 0). RGB(255,0,0). RGB(0.255.0). RGBC0.0. 255). RGB(255.255.0). RGB(0. 255. 255). RGBC255. 255. 0).
Построение графической библиотеки DirectDraw 1029 RGBC127. 255. 0). RGB(0. 127, 255). RGBC255. 0. 127) }: DWORD dwColor[10]: for (int i=0; i<10; i++) dwColor[i] = surface.ColorMatch(GetRValue(color[i]). GetGValue(color[i]) GetBValue(color[i])): KLockedSurface frame; if ( frame.Inistialize(m_priтагу) ) { for (int p=0; p<N; p++) for (int q=0; q<p; q++) frame.Line( (int)(x + Radius * sin(p * theta)). (int)(y + Radius * cos(p * theta)). (int)(x + Radius * sin(q * theta)). (int)(y + Radius * cos(q * theta)). dwColor[min(p-q. N-p+q)]); m_primary.Unlock(); Заливка замкнутых областей DirectDraw поддерживает заливку прямоугольных областей однородным цветом. Непрямоугольные области приходится разбивать на прямоугольные участки или преобразовывать в регионы отсечения. Ниже приведена реализация метода KDDSurface::FillRgn, закрашивающего произвольный регион однородным цветом. RGNDATA * GetCli pRegionData(HRGN hRgn) { DWORD dwSize - GetRegionData(hRgn. 0. NULL); RGNDATA * pRgnData - (RGNDATA *) new BYTE[dwSize]; if ( pRgnData ) GetRegionData(hRgn. dwSize. pRgnData); return pRgnData; } BOOL KDDSurface::Fi1IRgnCHRGN hRgn. DWORD color) { RGNDATA * pRegion = GetClipRegionData(hRgn); if ( pRegion==NULL ) return FALSE; const RECT * pRect - (const RECT *) pRegion->Buffer; for (unsigned i=0; i<pRegion->rdh.nCount; i++) { \ Fi11 Color(pRect->left. pRect->top. pRect->right. pRect->bottom. color); pRect ++; } delete [] (BYTE *) pRegion; return TRUE;
1030 Глава 18. DirectDraw и непосредственный режим Direct3D GDI содержит немало разнообразных функций регионов, позволяющих выводить простые геометрические фигуры и их комбинации, создавать замкнутые траектории и даже контуры текста. В программах DirectDraw рекомендуется опираться на поддержку регионов в GDI. Если быстродействие особенно важно, данные регионов можно обсчитывать заранее и кэшировать. Метод KDDSurface: :FillRgn получает манипулятор объекта региона GDI; он разбивает регион на серию прямоугольников (структура RGNDATA) функцией Get- RegionData GDI, после чего закрашивает каждый прямоугольник однородным цветом при помощи метода IDirectDrawSurface7: .-Bit с учетом состояния текущего объекта отсечения DirectDraw. Возможна и другая реализация — преобразовать список отсечения текущего объекта отсечения DirectDraw в регион GDI, получить его пересечение с выводимым регионом, создать новый список отсечения и вывести результат методом Bit. Недостаток подобного решения заключается в том, что вам придется создать второй объект отсечения DirectDraw и организовать переключение объекта отсечения и поверхности. В следующем примере на поверхности DirectDraw рисуется однородный эллипс: void RegionDemo(void) { HRGN hRgn = CreateEllipticRgnIndirect(& m_rcDest); if ( hRgn ) { m jdMтагу.Fi11Rgn(hRgn. m_primary.ColorMatch(OxFF, OxFF, 0)); DeleteObject(hRgn); } } На рис. 18.3 изображено дочернее окно MDI, в котором средствами DirectDraw нарисованы пикселы, линии и эллипс. ~С> Н^; f-i% ЛИ Рис. 18.3. Пикселы, линии и фигуры на поверхности DirectDraw
Построение графической библиотеки DirectDraw 1031 Отсечение Поверхности DirectDraw поддерживают отсечение с использованием объектов отсечения DirectDraw, создаваемых методом IDirectDraw::Createdipper. Объекты отсечения DirectDraw делятся на две категории: ассоциированные с окном и созданные на базе списка отсечения. Когда объект отсечения ассоциируется с окном методом IDirectDrawClipper:: SetHWnd, операционная система неким волшебным образом следит за тем, чтобы объект отсечения всегда синхронизировался с обновляемым регионом конкретного окна. Следовательно, вывод на поверхности DirectDraw с присоединенным объектом отсечения может ограничиваться видимой частью клиентской области. Мы уже видели, как объекты отсечения обеспечивают правильность работы метода Bit в оконном режиме. Приложение также может напрямую управлять объектом отсечения DirectDraw, изменяя содержимое его списка отсечения, который представляет собой обычную структуру RGNDATA GDI. В документации DirectX предполагается, что программисты DirectX достаточно хорошо разбираются в программировании GDI, поэтому в ней почти ничего не говорится о том, как правильно работать со списками отсечения. Приведенные ниже функция и класс связывают объект региона GDI с объектом отсечения DirectDraw. BOOL SetClipRegion(IDirectDrawClipper * pClipper. HRGN hRgn) { RGNDATA * pRgnData = GetClipRegionData(hRgn); if ( pRgnData==NULL ) return FALSE; HRESULT hr = pClipper->SetClipList(pRgnData. 0): delete (BYTE *) pRgnData; return SUCCEEDED(hr); } class KRgnClipper { IDirectDrawClipper * m_pNew; IDirectDrawClipper * m_p01d; IDirectDrawSurface7 * m_pSrf; public: KRgnClipper(IDirectDraw7 * pDD. IDirectDrawSurface7 * pSrf. HRGN hRgn) { pDD->CreateClipper(0. & rn_pNew. NULL); // Создать объект отсечения SetClipRegion(m_pNew. hRgn);// Получить список отсечения // по данным региону m_pSrf = pSrf; pSrf->GetClipper(& m_p01d); // Получить старый объект отсечения pSrf->SetClipper(m_pNew); // Заменить новым объектом отсечения
1032 Глава 18. DirectDraw и непосредственный режим Direct3D } -KRgnClipperО { m_pSrf->SetClipper(m_p01d); // Восстановить старый объект отсечения m_p01d->Release(); // Освободить старый объект отсечения m_pNew->Release(); // Освободить новый объект отсечения } }: Функция SetClipper заполняет список отсечения объекта отсечения DirectDraw данными объекта региона GDI. Для получения данных она вызывает функцию GetRegionData GDI по манипулятору региона. Как говорилось выше, поскольку данные региона имеют переменный размер, функция должна вызываться дважды — сначала вы получаете размер данных, выделяете память, а затем получаете сами данные. Класс KRgnClipper заменяет объект отсечения, связанный с поверхностью DirectDraw, новым объектом отсечения, созданным по данным объекта региона GDI. Конструктор создает новый объект отсечения, заполняет его список отсечения данными региона GDI и заменяет текущий объект отсечения, связанный с поверхностью. При всех последующих операциях вывода используется новый объект отсечения. Деструктор восстанавливает исходный объект отсечения и освобождает ресурс. В приведенной ниже функции класс KRgnClipper используется для заливки областей. void ClipDemo(void) { HRGN hUpdate = CreateRectRgn(0. 0. I, l); GetUpdateRgn(m_hWnd. hUpdate. FALSE); // Обновляемый регион OffsetRgn(hUpdate. m_rcDest.left. m_rcDest.top); // Экранные координаты HRGN hEl1 ipse = CreateEllipticRgn(m_rcDest.left-20. // Большой эллипс mjrDest.top-20. m_rcDest.nght+20. m_rcDest.bottom+20); CombineRgn(hEllipsef hEl1 ipse, hUpdate. RGN_AND); // Обновляемый регион AND эллипс DeleteObject(hUpdate); KRgnClipper clipper(m_pDD, m_primary. hEl1 ipse); DeleteObject(hEllipse); m_priтагу.Fi11 Color(m_rcDest.1eft-20. m_rcDest.top-20. m_rcDest.right+20, m_rcDest.bottom+20. m_primary.ColorMatch(0. 0. OxFF)); } Функция ClipDemo запрашивает обновляемый регион текущего окна и преобразует его из клиентских координат в экранные, как того требует DirectDraw. Затем функция создает эллиптический регион, размеры которого превышают размеры клиентской области, находит его пересечение с обновляемым регионом и определяет новую область отсечения. Метод KDDSurface:: Fill Color использует-
Построение графической библиотеки DirectDraw 1033 ся для заполнения области, большей клиентской части окна, но благодаря отсечению вывод ограничивается как границами эллипса, так и обновляемым регионом. Внеэкранные поверхности Как показывает метод KDDSurface: :DrawBitmap, для вывода растра на поверхности DirectDraw проще всего воспользоваться функциями GDI. Хотя данные растра можно самостоятельно скопировать на зафиксированную поверхность DirectDraw, для обработки сжатия, поддержки разных форматов растров, масштабирования и палитры, а также преобразования формата пикселов вам придется написать довольно большой объем кода, а это приведет к снижению быстродействия и потере всех преимуществ DirectDraw. Правильный подход к выводу растров в DirectDraw использует преимущества как GDI, так и DirectDraw. Сначала растр загружается на внеэкранную поверхность средствами GDI, а затем выводится на главную поверхность средствами DirectDraw. Создание внеэкранной поверхности и загрузка растра обеспечиваются классом KOffscreenSurfасе, производным от KDDSurface. typedef enum { mem_default. mem_system. memjnonloca1 video. memj oca 1 video }: class KOffScreenSurface : public KDDSurface { public: HRESULT Create0ffScreenSurface(IDirectDraw7 * pDD. int width. int height, int mem=mem_default); HRESULT Create0ffScreenSurfaceBpp(IDirectDraw7 * pDD. int width. int height, int bpp. int mem=mem_default): HRESULT CreateBitmapSurface(IDirectDraw7 * pDD. const BITMAPINFO * pDIB. int mem=mem_default); HRESULT CreateBitmapSurface(IDirectDraw7 * pDD. const TCHAR * pFileName. int mem=mem_default); }: const DWORD MEMFLAGS[] = { 0. DDSCAPSJYSTEMMEMORY. DDSCAPSJIONLOCALVIDMEM | DDSCAPSJ/IDEOMEMORY. DDSCAPS_LOCALVIDMEM | DDSCAPSJ/IDEOMEMORY }: HRESULT KOffScreenSurface::Create0ffScreenSurface(IDirectDraw7 * pDD. int width, int height, int mem)
1034 Глава 18. DirectDraw и непосредственный режим Direct3D { m_ddsd.dwFlags = DDSD_CAPS | DDSDJEIGHT | DDSD_WIDTH; m_ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_3DDEVICE | MEMFLAGS[mem]; m_ddsd.dwWidth = width; m_ddsd.dwHeight = height; return pDD->CreateSurface(& m_ddsd, & m_pSurface. NULL); } HRESULT K0ffScreenSurface::CreateBitmapSurface(IDirectDraw7 * pDD. const BITMAPINFO * pDIB. int mem) { if ( pDIB==NULL ) return E_FAIL; HRESULT hr = CreateOffScreenSurface(pDD. pDIB->bmiHeader.biWidth, abs(pDIB->bmiHeader.biHeight). mem); if ( FAILED(hr) ) return hr; return DrawBitmapCpDIB. 0, 0, m_ddsd.dwWidth. m_ddsd.dwHeight); } Метод CreateOffScreenSurface создает внеэкранную поверхность DirectDraw, то есть поверхность, хранящуюся в памяти, но не отображаемую на экране монитора. Память для внеэкранной поверхности может выделяться из системной памяти, локальной или нелокальной видеопамяти в зависимости от флагов поля ddsCaps.dwCaps. Системной памяти хватает в избытке, поскольку ее объем ограничивается только размером системного файла подкачки. К нелокальной видеопамяти относится память, находящаяся под управлением AGP (Advanced Graphics Port) — механизма, обеспечивающего ускоренное копирование данных в видеопамять. По сравнению с системной памятью нелокальная память является более ограниченным ресурсом, но вывод из нее происходит быстрее. Локальная видеопамять является самым дорогим и редким из всех видов памяти. Кстати, при прямом доступе к пикселам из приложения локальная видеопамять оказывается самой медленной, поскольку она расположена «дальше» от процессора. Последний параметр CreateOffScreenSurface показывает, откуда выделяется память поверхности. В отличие от первичной поверхности, при создании внеэкранных поверхностей необходимо указывать их точный размер. Метод CreateOffScreenSurfaceBpp, реализация которого здесь не приводится, позволяет создать внеэкранную поверхность с заданным форматом пикселов. Первый метод CreateBitmapSurface в качестве входных данных получает упакованный DIB-растр. Он создает внеэкранную поверхность по размерам растра, а затем копирует растр на поверхность средствами GDI. Второй метод CreateBitmapSurface, который здесь также не приводится, создает поверхность и загружает в нее растр из внешнего файла. Оба метода используют DIB, что обеспечивает экономию ресурсов по сравнению с DDB-растрами и DIB-секциями, активно рекомендуемыми в литературе по DirectX.
Построение графической библиотеки DirectDraw 1035 После того как растр загружен на внеэкранную поверхность, его можно скопировать на другую поверхность методом IDirectDrawSurface7: :Blt. Класс KDDSurface содержит два метода BitBlt, которые представляют собой простые оболочки для метода Bit, чтобы вызовы больше походили на вызовы функций GDI. Ниже приведена одна из этих оболочек. HRESULT KDDSurface::BitBlt(int x. int у. int w. int h, IDirectDrawSurface7 * pSrc. DWORD flag) { RECT re = { x. y. x+w. y+h }; return m_pSurface->Blt(& re, pSrc. NULL, flag. NULL); } Поддержка прозрачности посредством цветовых ключей Метод IDirectDrawSurface7: :Blt поддерживает вывод прозрачных растров с использованием цветовых ключей. Цветовой ключ является атрибутом поверхности DirectDraw и может задаваться как для исходной, так и для приемной поверхностей. Цветовой ключ, который может представлять собой как отдельный цвет, так и интервал цветов, определяется структурой DDC0L0RKEY. Ниже приведен метод SetSourceCol огКеу класса KDDSurface, задающий цветовой ключ источника с использованием одного физического цвета. HRESULT KDDSurface::SetSourceColогКеу(DWORD color) { DDCOLORKEY key: key.dwColorSpaceLowValue = color; key.dwColorSpaceHighValue = color; return m_pSurface->SetColorKey(DDCKEY_SRCBLT. & key); } Чтобы скопировать внеэкранную растровую поверхность с цветовым ключом источника, вызовите метод Bit с флагом DDBLTKEYSRC. При этом копируются только те пикселы, значения которых отличны от цветового ключа источника. В DirectDraw также поддерживаются цветовые ключи приемника. Шрифт и текст DirectX как простой низкоуровневый интерфейс API, оптимизированный для максимального быстродействия, не обладает встроенной поддержкой шрифтов или вывода текста. Даже реализация OpenGL для Windows работает со шрифтами при помощи специальных расширений. Если вы задействуете средства GDI для операций со шрифтами и вывода текста на поверхностях DirectDraw, можно подумать и об использовании контекста устройства GDI. Однако в играх и приложениях, требующих высокого быстродействия, применение медленных функций GDI неприемлемо. В играх часто встречается другой вариант — программа заранее строит растр с полным
1036 Глава 18. DirectDraw и непосредственный режим Direct3D набором требуемых глифов (назовем его шрифтовым растром). Вместо шрифта программа работает с растром, а вывод текста сводится к копированию фрагментов шрифтового растра. Слабой стороной такого решения является недостаточная гибкость, поскольку программа задействует ограниченный набор шрифтов заданного размера. Оптимальное решение, как и прежде, объединяет два подхода — GDI и шрифтовые растры. Идея заключается в том, чтобы динамически построить шрифтовые растры для заданной гарнитуры и кегля, а затем воспользоваться методами DirectDraw для вывода текста из шрифтовых растров. Шрифтовые растры строятся только при загрузке приложения, что расширяет выбор гарнитур и кеглей без особой потери быстродействия. Шрифтовые растры даже можно кэшировать в растровых файлах на диске и загружать на внеэкранные поверхности (вероятно, в локальную видеопамять для максимального быстродействия) методом Bit, поддерживающим аппаратное ускорение. В листинге 18.4 приведен класс KDDFont, поддерживающий работу с динамически сгенерированными шрифтовыми растрами на внеэкранных поверхностях. Листинг 18.4. Класс KDDFont: работа с динамическими шрифтовыми растрами и вывод текста template <int MaxChar> class KDDFont : public KOffScreenSurface { int m_offset [MaxChar]; // Метрика А int m_advance[MaxChar]; // A + В + С int m_pos [MaxChar]; // Горизонтальная позиция int m_width [MaxChar]; // - min(A. 0) + В - min(C.O) unsigned m_firstchar; int mjiChar; public: HRESULT CreateFont(IDirectDraw7 * pDD. const L0GF0NT & If. unsigned firstchar. unsigned lastchar, C0L0RREF crColor); int Text0ut(IDirectDrawSurface7 * pSurface, int x. int y, const TCHAR * mess, int nChar=0); }: template <int MaxChar> HRESULT KDDFont<MaxChar>;:CreateFont(IDirectDraw7 * pDD. const L0GF0NT & If. unsigned firstchar. unsigned lastchar. C0L0RREF crColor) { m_firstchar = firstchar; mjiChar = lastchar - firstchar + 1; if ( mjiChar > MaxChar ) return E INVALIDARG; HFONT hFont - CreateFontIndirect(&lf);
Построение графической библиотеки DirectDraw 1037 if ( hFont==NULL ) return EJNVALIDARG; HRESULT hr; ABC abc[MaxChar]: int height; { HDC hDC - ::GetDC(NULL); if ( hDC ) { HGDIOBJ hOld « SelectObjectChDC. hFont); TEXTMETRIC tm: GetTextMetrics(hDC. & tm); height = tm.tmHeight: if ( GetCharABCWidths(hDC. firstchar. lastchar. abc) ) hr - S_OK; else hr = E INVALIDARG; SelectObject(hDC. hOld); ::ReleaseDC(NULL. hDC); } if ( SUCCEEDED(hr) ) { int width = 0; for (int i=0; i<m_nChar; i++) { m_offset[i] = abc[i].abcA; m_width[i] » - min(abc[i].abcA. 0) + abc[i].abcB - min(abc[i].abcC. 0); m_advance[i] = abc[i].abcA + abc[i].abcB + abc[i].abcC: width += m width[i]; hr = CreateOffScreenSurface(pDD. width, height): if ( SUCCEEDED(hr) ) { GetDCO; int x = 0; PatBltCm hDC. 0. 0. GetWidthO. GetHeightO. BLACKNESS); SetBkMode(m_hDC. TRANSPARENT); SetTextColor(m_hDC. crColor); // Белый основной цвет Продолжение^
1038 Глава 18. DirectDraw и непосредственный режим Direct3D Листинг 18.4. Продолжение HGDIOBJ hOld = SelectObject(m_hDC. hFont); SetTextAlign(m_hDC. TA_TOP | TA_LEFT); for (int i=0; i<m_nChar; i++) { TCHAR ch = firstchar + i; m_pos[i] = x; ::TextOut(m_hDC. x-m_offset[i], 0. & ch. 1); x += m_width[i]; } SelectObject(m_hDC. hOld); ReleaseDCO; SetSourceColorKey(O); // Цветовой ключ источника - черный } } Del eteObjecK hFont); return hr; }: tempiate<int MaxChar> int KDDFont<MaxChar>::Text0ut(IDirectDrawSurface7 * pDest, int x, int y, const TCHAR * mess, int nChar) { if ( nChar<=0 ) nChar = Jxslen(mess); for (int i=0; i<nChar; i++) { int ch = mess[i] - m_firstchar; if ( (ch<0) || (ch>m_nChar) ) ch = 0; RECT dst - { x + m_offset[ch]. y. x + m_offset[ch] + m_width[ch]. у + GetHeightO }; RECT src = { m_pos[ch]. 0. m_pos[ch] + m_width[ch]. GetHeightO }; pDest->Blt(& dst. m_pSurface. & src. DDBLT_KEYSRC. NULL); x += m_advance[ch]; } return x; } Класс KDDFont рассчитан на поддержку обобщенных шрифтов, предоставляемых GDI. Выражаясь точнее, он поддерживает моноширинные и пропорциональные шрифты и текстовые метрики ABC. Класс KDDFont добавляет к классу KOffScreenSurface новые поля. В массиве m_offset хранятся метрики А всех гли-
Построение графической библиотеки DirectDraw 1039 фов; массив m_advance задает смещения следующих символов; массив m_pos содержит горизонтальную позицию каждого глифа на поверхности, а в массиве m_width хранятся значения ширины глифов. Класс преобразует интервал символов шрифта в шрифтовой растр (первый и последний символы интервала хранятся в отдельных переменных). Максимальное количество символов определяется параметром шаблона. Метод KDDFont: :CreateFont инициализирует шрифтовой растр по исходным данным — указателю на объект IDirectDraw7, структуре LOGFONT GDI, интервалу символов и цвету текста. Он создает логический шрифт GDI по структуре LOGFONT и запрашивает метрики ABC, на основе которых заполняются четыре массива. В результате вычислений определяется ширина и высота шрифтового растра, после чего создается внеэкранная поверхность DirectDraw. Очистка растра и вывод в нем всех глифов осуществляются средствами GDI. К сожалению, мы не можем преобразовать все символы интервала в строку и вывести их одним вызовом функции, поскольку в строке символы могут перекрываться по горизонтали. Каждый символ рисуется отдельно в заранее вычисленной позиции растра. Символы выводятся на черном фоне, и черный цвет назначается цветовым ключом шрифтовой поверхности. Метод KDDFont: :TextOut использует содержимое поверхности шрифтового растра для вывода строки символов на поверхности DirectDraw. Для поиска глифов и их выравнивания в строке задействуются четыре массива, хранящихся в переменных класса KDDFont. Каждый символ выводится в прозрачном режиме с цветовым ключом источника (шрифтовой поверхности) методом IDirectDrawSurface7: :Blt. Метод TextOut выводит текст с цветовым ключом, заданным при создании шрифтовой поверхности. Класс KDDFont можно дополнить методами, изменяющими цвет текста или выводящими текст с применением специальных эффектов. Шрифтовую поверхность можно сохранить в растре и в дальнейшем обойтись без повторных вызовов GDI. Возможны и другие усовершенствования — скажем, разделение глифов дополнительными интервалами. Спрайты Многие игровые программы основаны на использовании простых и прозрачных растров. Прозрачные растры, называемые в играх спрайтами, изображают различные перемещающиеся объекты, при соприкосновении которых в игре происходят те или иные события. Для управления перемещением спрайтов в играх задействуется клавиатура, мышь или другое устройство ввода. Ниже приведена псевдоигровая программа для DirectDraw, иллюстрирующая вывод растров, спрайтов и текста на поверхности DirectDraw. class KSpriteDemo : public KMDIChiId. public KDirectDraw { KOffScreenSurface m_background; POINT m_backpos; KOffScreenSurface m_sprite; POINT m_spntepos; KDDFont<128> m_font; HINSTANCE m hlnst;
1040 Глава 18. DirectDraw и непосредственный режим Direct3D HWND m_hTop; void OnDraw(void): void OnCreate(void): void MoveSprite(int dx. int dy) { m_spritepos.x +- dx * 5; m_spritepos.y += dy * 5; OnDraw(): LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam) { switch( uMsg ) case WM_PAINT: OnDraw(); ValidateRect(hWnd. return 0; case WM_KEYDOWN: switch ( wParam ) { case VKJOME case VK_UP case VK_PRIOR case VK_LEFT : case VK_RIGHT: case VKJND case VK_DOWN case VK NEXT NULL); MoveSprite(-l MoveSprite( 0 MoveSprite(+l MoveSprite(-l MoveSprite( 1 MoveSprite(-l MoveSprite( 0 MoveSpriteC 1 1): 1): 1); break; break; break; 0); break: 0); break; break break break } return 0; case WM_CREATE: m_hWnd = hWnd; OnCreateO: // Продолжить default: return KMDIChild::WndProc(hWnd. uMsg, wParam. IParam); public: KSpriteDemo(HMODULE hModule. HWND hTop) m_spritepos.x m_spritepos.y m_backpos.x m backpos.y = 0 = 0 = 0 = 0
Построение графической библиотеки DirectDraw 1041 mjilnst = hModule; mJYTop = hTop; } }: Класс KSpriteDemo объявлен производным от классов KMDIChild (поддержка дочерних окон MDI) и KDirectDraw (поддержка DirectDraw). Переменные mbackground и m_backpos предназначены для управления фоновым растром, загруженным на внеэкранную поверхность DirectDraw. Размеры фонового растра могут превышать размеры окна, в этом случае организуется прокрутка окна в фоновом растре. Переменные msprite и mspritepos используются при выводе самолета поверх фоновой сцены. В переменной m_font хранится экземпляр класса KDDFont. Метод OnCreate инициализирует фоновый растр, спрайт и логический шрифт. Метод OnDraw выводит изображение в окне, а метод MoveSprite обеспечивает управление полетом с клавиатуры. Ниже приведена реализация метода OnCreate. void KSpriteDemo::OnCreate(void) { if ( SUCCEEDED(SetupDirectDraw(m_hTop. m_hWnd. false)) ) { BITMAPINFO * pDIB = LoadBMP(m_hInst. MAKEINTRESOURCE(IDBJIGHT)); if ( pDIB ) m_background.CreateBitmapSurface(m_pDD. pDIB): pDIB - LoadBMP(m_hInst. MAKEINTRESOURCECIDB_PLANE)); if ( pDIB ) { m_sprite.CreateBitmapSurface(m_pDD, pDIB): m_sprite.SetSourceColorKey(0); // Черный } LOGFONT If; memsetC&lf. 0. sizeof(lf)); lf.lfHeight = - 36; If.IfWeight - FW_B0LD; lf.lfltalic - TRUE; lf.lfQuality - ANTIALIASED_QUALITY; _tcscpy(lf.lfFaceName. "Times New Roman"); m_font.CreateFont(m_pDD. If. ' '. 0x7F. RGB(0xFF. OxFF. 0)): } else { MessageBoxCNULL. _T("Unable to Initialize DirectDraw"). JC'KSpriteDemo"). MB_0K); CloseWindow(m hWnd); Метод OnCreate инициализирует среду DirectDraw в оконном режиме, вызывая метод KDirectDraw: iSetupDirectDraw. Затем он загружает фоновое изображение из ресурса в формате упакованного DIB-растра и инициализирует поверхность фо-
1042 Глава 18. DirectDraw и непосредственный режим Direct3D нового растра. Спрайт тоже загружается из ресурса, и в качестве цветового ключа источника ему назначается черный цвет. Поверхность шрифтового растра инициализируется по структуре LOGFONT, представляющей сглаженный курсивный шрифт кегля 36 пунктов. Ниже приведена реализация метода OnDraw. void KSpriteDemo::OnDraw(void) { SetClientRect(m_hWnd); int dy = (m_rcDest.bottom - m_rcDest.top - m_background.GetHeight())/2; // Выводимая область выходит за границы фонового изображения if ( dy>0 ) { DWORD color = m_primary.ColorMatch(0x80, 0x40. 0); m_primary.Fill Col or(m_rcDest.1 eft. m_rcDest.top, m_rcDest.right. m_rcDest.top + dy. color); // Верхняя полоса color = m_priтагу.ColorMatch(0. 0x40. 0x80); m_primary.FillColor(m_rcDest.left. m_rcDest.bottom - dy-1. m_rcDest.right. m_rcDest.bottom, color); // Нижняя полоса } else dy = 0; // Вывод фонового изображения // Если правый край спрайта выходит за границу окна. // сместить фон влево while ( (m_spritepos.x + m_sprite.GetWidth() + m_backpos.x) > (m_rcDest.right - m_rcDest.left) ) m_backpos.x -* 100; // Если левый край спрайта выходит за границу окна. // сместить фон вправо while ( (m_spritepos.x + m_backpos.x) < 0 ) m_backpos.x += 100; // Убедиться, что текущая позиция фона лежит в допустимом интервале m_backpos.x = max(m_backpos.x. m_rcDest.right - m_background.GetWidth() - m_rcDest.left); m_backpos.x = min(m_backpos.x. 0); m_primary.BitBlt(m_rcDest.left + m_backpos.x. m_rcDest.top + m_backpos.y + dy. m_background); // Вывести спрайт m_primary.BitBlt(m_rcDest.left + m_spritepos.x + m_backpos.x. m_rcDest.top + m_spritepos.y + m_backpos.y. m_sprite. DDBLT_KEYSRC); m_font.TextOut(m_primary. m_rcDest.left+5. m_rcDest.top+l. "Hello. DirectDraw!"); }
Непосредственный режим Direct3D 1043 Вывод «игровой» сцены состоит из четырех этапов. На первом этапе рисуется клиентская область, не закрываемая фоновым изображением. В нашей программе используется фон в виде длинной и узкой полосы. Программа выравнивает фон по центру окна вдоль вертикальной оси, после чего заполняет полосы в верхней и нижней части окна однородными заливками. Фоновое изображение выводится на втором этапе, однако большая часть кода предназначена для вычисления новой позиции фонового изображения, при которой самолет будет виден на экране. На третьем этапе выполняется вывод прозрачной поверхности спрайта с цветовым ключом. На последнем, четвертом этапе в левой верхней части окна выводится простой фиксированный текст. Запустите программу. При помощи клавиш управления курсором можно управлять перемещением самолета по фоновому изображению с автоматической прокруткой. На рис. 18.4 показано окно программы DEMODD, использующей класс KSpriteDemo. Рис. 18.4. Вывод текста, растров и спрайтов в DirectDraw Если вы увлекаетесь программированием игр, попробуйте в виде спрайтов создать объекты вражеских самолетов, реализуйте проверку соприкосновений и стрельбу из оружия. Непосредственный режим Direct3D Хотя технология DirectDraw обеспечивает аппаратное ускорение, возможности вывода в ней весьма ограничены. При работе с DirectDraw все время кажется, что вы пишете драйвер устройства, а не прикладную программу, поскольку вам приходится принимать во внимание множество мелочей. С другой стороны, непосредственный режим Direct3D как API графического программирования обладает гораздо более широкими возможностями. В Direct3D
1044 Глава 18. DirectDraw и непосредственный режим Direct3D поддерживаются логические цвета, Z-буфер, отсечение, альфа-наложение, текстуры, области просмотра, мировые преобразования, матрицы вида, проекции, источники света, линии и треугольники, эффект тумана и т. д. Хотя непосредственный режим Direct3D проектировался как API трехмерной графики, ничто не мешает применять его pi при двумерном выводе, который просто расположен в одной плоскости трехмерного пространства. В этом разделе мы в общих чертах рассмотрим программирование для непосредственного режима Direct3D. Подготовка среды непосредственного режима Direct 3D Для работы в непосредственном режиме Direct3D вам понадобится нечто большее, чем объект DirectDraw и поверхности DirectDraw, инкапсулированные в классе KDirectDraw. Обычно для этого необходим объект Direct3D, объект Direct- 3DDevice, поверхность вывода фона и Z-буфер. Объект Direct3D управляет доступом к поддержке Direct3D. В фоновом буфере выполняется весь графический вывод, а Z-буфер управляет отсечением скрытых поверхностей. Объект Direct- 3DDevice играет роль графического устройства. В листинге 18.5 приведен класс KDirect3D, инкапсулирующий среду Direct3D. Листинг 18.5. KDIrect3D: класс среды непосредственного режима Direct3D class KDirect3D : public KDirectDraw { protected: IDirect3D7 * m_pD3D; IDirect3DDevice7 * m_pD3DDevice; KOffScreenSurface m_backsurface; KOffScreenSurface m_zbuffer; bool m_bReady; virtual HRESULT Discharge(void); virtual HRESULT OnRender(void) return S_0K; virtual HRESULT OnlnitCHINSTANCE hlnst) m_bReady = true; return S_0K: virtual HRESULT OnDischarge(void) m_bReady = false; return S OK;
Непосредственный режим Direct3D 1045 public: KDirect3D(void); ~KDirect3D(void) { Dischargee): } virtual HRESULT SetupDirectDraw(HWND hWnd. HWND hTop. int nBufferCount=0. bool bFullScreen=false, int width=0. int height=0. int bpp=0); virtual HRESULT ShowFrame(HWND hWnd): virtual HRESULT RestoreSurfaces(void); virtual HRESULT Render(HWND hWnd); virtual HRESULT ReCreate(HINSTANCE hinst virtual HRESULT OnResize(HINSTANCE hinst hWnd); HRESULT KDirect3D::SetupDirectDraw(HWND hTop. HWND hWnd. int nBufferCount. bool bFullScreen, int width, int height, int bpp) { HRESULT hr = KDirectDraw::SetupDirectDraw(hTop. hWnd. nBufferCount. bFullScreen, width, height, bpp); if ( FAILEDC hr ) ) return hr; // Устройство с 8-разрядным цветом отклоняется if ( GetDisplayBpp(m_pDD)<=8 ) return DDERRJNVALIDMODE; // Создать фоновую поверхность hr = m_backsurface.CreateOffScreenSurface(m_pDD. width, height); if ( FAILED(hr) ) return hr: // Запросить у DirectDraw доступ к Direct3D m_pDD->QueryInterface( IID_IDirect3D7. (void**) & m_pD3D ); if ( FAILED(hr) ) return hr; CLSID iidDevice - IID_IDirect3DHALDevice; // Создать Z-буфер hr - m_zbuffer.CreateZBuffer(m_pD3D, m_pDD. iidDevice. width, height); if ( FAILED(hr) ) { iidDevice - IID_IDirect3DRGBDevice; hr - m_zbuffer.CreateZBuffer(m_pD3D. m_pDD. iidDevice. width, height); } Продолжение ^> HWND hTop. HWND hWnd); int width, int height. HWND hTop. HWND
1046 Глава 18. DirectDraw и непосредственный режим Direct3D Листинг 18.5. Продолжение if ( FAILED(hr) ) return hr; // Присоединить Z-буфер к фоновой поверхности hr * m_backsurface.Attach(m_zbuffer): if ( FAILED(hr) ) return hr; hr * m_pD3D->CreateDevice( iidDevice. m_backsurface, & m_pD3DDevice ): if ( FAILED(hr) ) return hr; { D3DVIEWP0RT7 vp - { 0. 0, width, height. (float)O.O. (float)l.O }: return m_pD3DDevice->SetViewport( &vp ); } } Класс KDirect3D добавляет в KDirectDraw пять переменных: указатель на объект Direct3D7, указатель на объект Direct3DDevice7, фоновый буфер, Z-буфер и логический флаг. Процедура инициализации начинается с настройки среды DirectDraw вызовом KDirectDraw: .-SetupDirectDraw. После инициализации DirectDraw функция проверяет текущий видеорежим, и если в нем используется палитра — возвращает код ошибки. Программы Direct3D лучше всего работают в режимах High Color и True Color; режим с палитрой тоже поддерживается, но в нем действует слишком много ограничений. Вывод Direct3D чрезвычайно сложен, поэтому все операции следует выполнять на фоновой поверхности. В полноэкранном режиме можно использовать поверхности с двумя или тремя буферами, а в оконном режиме вывод осуществляется на отдельной фоновой поверхности. Функция создает внеэкранную поверхность, размеры которой совпадают с размерами клиентской области окна. Чтобы создать Z-буфер для поверхности вывода, сначала необходимо получить указатель на интерфейс IDirect3D7 объекта DirectDraw, созданного функцией DirectDrawCreateEx. Z-буфер тоже является внеэкранной поверхностью, если не считать того, что для получения информации о форматах Z-буфера, поддерживаемых текущим устройством, используется функция IDirect3D7::EnumZBufferFormats. Функция пытается создать Z-буфер для устройства с аппаратным ускорением, но в случае неудачи переключается на устройство с программной эмуляцией. Созданный Z-буфер необходимо присоединить к фоновой поверхности. Завершающая часть метода KDirect3D: .-SetupDirectDraw создает объект Direct- 3DDevice7 для фоновой поверхности и определяет область просмотра для устройства. Объект Direct3DDevice7 обеспечивает интерфейс к средствам построения трехмерных изображений, реализованных для поверхностей с включенной ЗО-поддержкой. Первые четыре поля области просмотра определяют прямоугольный участок поверхности, в котором осуществляется вывод; два последних поля определяют интервал значений в Z-буфере.
Непосредственный режим Direct3D 1047 Изменение размеров окна В оконном режиме фоновая поверхность и Z-буфер создаются по размерам клиентской области окна. Тем не менее, когда пользователь изменяет размеры окна, эти поверхности необходимо создать заново для новых размеров. Самое простое решение — уничтожить все объекты DirectDraw/Direct3D и создать их с самого начала. Ниже приведены методы удаления и повторного создания объектов среды Direct3D. HRESULT KDirect3D::Discharge(void) { SAFE_RELEASE(mj)D3DDevice); m_backsurface.Discharge(); m_zbuffer.Discharge(): SAFE_RELEASE(m_pD3D): return KDirectDraw::Discharge(); HRESULT KDirect3D::ReCreate(HINSTANCE hlnst, HWND hTop. HWND hWnd) { if ( FAILED(OnDischargeO) ) return E_FAIL: if ( FAILEDC DischargeO ) ) // Освободить все ресурсы return E_FAIL; SetClientRect(hWnd); HRESULT hr = SetupDirectDraw(hTop. hWnd. 0. false. m_rcDest.right - m_rcDest.left. m_rcDest.bottom - m_rcDest.top); if ( SUCCEEDED(hr) ) return Onlnit(hlnst); else return hr; HRESULT KDirect3D::0nResize(HINSTANCE hlnst. int width, int height. HWND hTop. HWND hWnd) { if ( ! m_bReady ) return S__0K; if ( width "(mj-cDest.right - mjxDest.left) ) if ( height—tmjrDest.bottom - m_rcDest.top) ) return S_0K; return Recreate(hlnst. hTop. hWnd):
1048 Глава 18. DirectDraw и непосредственный режим Direct3D Метод Discharge освобождает все ресурсы, связанные с объектом KDirect3D. Метод Recreate вызывает Discharge, чтобы освободить все ресурсы, а затем создает новую среду Direct3D вызовом SetupDirectDraw. Метод OnSize вызывает Recreate при изменении размеров окна. Двухэтапный вывод При использовании фоновой поверхности изображение строится в два этапа: сначала происходит вывод на фоновой поверхности, а потом результат копируется с фоновой поверхности на первичную. Аналогичная методика применяется и к объектам DirectDraw, чтобы подавить мерцание при выводе. Ниже приведены два метода, обеспечивающие двухэтапный вывод в классе KDirect3D. HRESULT KDirect3D::Render(HWND hWnd) { if ( ! m_bReady ) return S_0K; HRESULT hr - OnRenderO: if ( FAILED(hr) ) return hr; hr = ShowFrame(hWnd); if ( hr = DDERR_SURFACELOST ) return RestoreSurfacesO; else return hr; } HRESULT KDirect3D:;ShowFrame(HWND hWnd) { if ( m_bReady ) { SetClientRect(hWnd); return m_primary.Blt(& m_rcDest, m_backsurface. NULL. DDBLT_WAIT); } else return S_0K; } Метод KDirect3D:: Render сначала вызывает виртуальный метод OnRender, выполняющий фактический вывод, а затем метод ShowFrame, копирующий данные с фоновой поверхности на первичную. При запуске нескольких приложений DirectX память, выделенная для поверхности, может быть захвачена другими приложениями. Программа проверяет условие потери поверхности и восстанавливает все потерянные поверхности вызовами IDirectDrawSurface7: .-Restore.
Непосредственный режим Direct3D 1049 Использование Direct3D в окне Класс KDirect3D разрабатывался как родовой класс, который может использоваться где угодно. По этой причине обработку сообщений пришлось реализовать в отдельном классе. Ниже приведен простой класс окна, поддерживающего непосредственный режим Direct3D. class KD3DWin : public KWindow, public KDirect3DDemo { bool m_bActive; HINSTANCE m_hlnst; LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM lParam) { switch( uMsg ) { case WM_CREATE: m_hWnd = hWnd; m_bActive = false: if ( FAILED(ReCreate(m_hInst. hWnd. hWnd)) ) CloseWindow(hWnd); SetTimer(hWnd. 101. 1. NULL); return 0; case WM_PAINT: ShowFrame(hWnd); break; case WM_SIZE: m_bActive = (SIZE_MAXHIDE!=wParam) && (SIZE_MINIMIZED!=wParam); if ( m_bActive && FAILED(OnResize(m_hInst. L0W0RD(lParam). HIWORD(lParam). hWnd. hWnd)) ) CloseWindow(hWnd); break; case WMJIMER: if ( m_bActive ) Render(hWnd); return 0; case WM_DESTR0Y: KillTimer(hWnd, 101); DischargeO; PostQuitMessage(O); return 0L; return DefWindowProcC hWnd. uMsg. wParam. lParam ); } void GetWndClassEx(WNDCLASSEX & wc)
1050 Глава 18. DirectDraw и непосредственный режим Direct3D { KWindow::GetWndClassEx(wc): wc.style |= (CSJREDRAW | CSJREDRAW); wc.hlcon = LoadIcon(m_hInst. MAKEINTRESOURCE(IDI_GRAPH)): } public: KD3DWin(HINSTANCE hlnst) { mjilnst = hlnst; } }: Класс KD3DWin объявлен производным от классов KWindow (общая поддержка окна) и KDirect3D (поддержка Direct3D). Среда Direct3D инициализируется при обработке сообщения WM_CREATE, изменяется при получении сообщения WM_SIZE и уничтожается при обработке сообщения WM_DESTROY. Обработчик WM_PAINT выводит данные с фоновой поверхности простым вызовом KDirect3D: :ShowFrame. Класс KD3DWin создает таймер, управляющий анимацией в окне. Обработчик сообщения WMJTIMER выводит новый кадр методом KDirect3D::Render. Частота поступления сообщений таймера зависит от архитектуры операционной системы. В Windows 95/98 программа получает не более 18-19 сообщений таймера в секунду; в Windows NT/2000 в секунду может поступать до 100 сообщений. Программы DirectX обычно увеличивают частоту смены кадров за счет использования пассивных циклов при обработке сообщений. С другой стороны, изменение цикла обработки сообщений главного программного потока возможно не всегда. В альтернативном решении смена кадров выделяется в отдельный программный поток. Текстурные поверхности Основные объекты, выводимые средствами Direct3D — точки, линии и треугольники, — обеспечивают вывод простейших геометрических форм в одномерном, двумерном и трехмерном пространстве. Чтобы геометрические фигуры больше походили на объекты реального мира, Direct3D позволяет накладывать текстуры на выводимые треугольники. Текстурный растр должен быть предварительно загружен на текстурную поверхность, используемую Direct3D. Текстурная поверхность представляет собой внеэкранную поверхность с загруженным растром. Для повышения быстродействия и расширения возможностей устройства Direct3D поддерживает несколько разновидностей форматов текстурных растров. Приложению остается лишь выбрать правильный формат текстуры в списке доступных форматов. Приведенный ниже метод KOffScreenSurface: :CreateTextSurface обеспечивает простейшее создание текстурных поверхностей. Создание текстурной поверхности на базе растра требует нескольких дополнительных действий. HRESULT CALLBACK TextureCalIbackCDDPIXELFORMAT* pddpf. void * param) { // Найти простой формат текстуры >=16 бит/пиксел
Непосредственный режим Direct3D 1051 if ( (pddpf->dwFlags & (DDPF_LUMINANCE|DDPF_BUMPLUMINANCE|DDPF_BUMPDUDV|DDPF_ALPHAPIXELS))==0 ) if ( (pddpf->dwFourCC == 0) && (pddpf->dwRGBBitCount>=16) ) { memcpy(param. pddpf, sizeof(DDPIXELFORMAT) ): return DDENUMRET_CANCEL: // Прекратить поиск } return DDENUMRETJDK; // Продолжить HRESULT KOffScreenSurface::CreateTextureSurface(IDirect3DDevice7 * pD3DDevice, IDirectDraw7 * pDD, unsigned width, unsigned height) { // Запросить информацию о возможностях устройства D3DDEVICEDESC7 ddDesc; HRESULT hr = pD3DDevice->GetCaps(&ddDesc); if ( FAILED(hr) ) return hr; m_ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSDJIIDTH | DDSD_PIXELFORMAT | DDSDJEXTURESTAGE: m_ddsd.ddsCaps.dwCaps = DDSCAPSJEXTURE; mjjdsd.dwWidth - width; m_ddsd.dwHeight = height; // Включить управление текстурами для устройств с аппаратным ускорением if ( (ddDesc.deviceGUID =- IID_IDirect3DHALDevice) || (ddDesc.deviceGUID == IID_IDirect3DTnLHalDevice) ) m_ddsd.ddsCaps.dwCaps2 - DDSCAPS2JEXTUREMANAGE: else m_ddsd.ddsCaps.dwCaps |- DDSCAPS JYSTEMMEMORY; // Отрегулировать ширину и высоту, если этого требует драйвер if ( ddDesc.dpcTriCaps.dwTextureCaps & D3DPTEXTURECAPS_P0W2 ) { for ( mjJdsd.dwWidth=l; width > m_ddsd.dwWidth; mjJdsd.dwWidth«=l ); for ( mjJdsd.dwHeight=l; height > m_ddsd.dwHeight; m_ddsd.dwHeight«=l ): } if ( ddDesc.dpcTriCaps.dwTextureCaps & D3DPTEXTURECAPS_SQUARE0NLY ) { if ( mjjdsd.dwWidth > m_ddsd.dwHeight ) m^ddsd.dwHeight - mjjdsd.dwWidth; else m_ddsd.dwWidth - m_ddsd.dwHeight: } memset(& m^ddsd.ddpfPixel Format. 0. sizeof(m_ddsd.ddpfPixel Format)); pD3DDevi ce->EnumTextureFormats(TextureCal1 back. & m_ddsd.ddpfPixel Format);
1052 Глава 18. DirectDraw и непосредственный режим Direct3D if ( mjdsd.ddpf Pixel Format. dwRGBBitCount ) return pDD->CreateSurface( & m_ddsd. & m_pSurface. NULL ); else return E FAIL; Пример использования непосредственного режима Direct3D Итак, в нашем распоряжении имеется среда, подготовленная классами KDirect3D и KD3DWin, и поддержка текстурных растров. Чтобы реализовать окно Direct3D, достаточно создать класс, производный от KDirect3D, и переопределить в нем несколько методов. В листинге 18.6 приведен класс простого окна непосредственного режима Direct3D, в котором выводится вращающаяся пирамида. Пример иллюстрирует работу с текстурами и Z-буфером, а также создание анимации. Листинг 18.6. class KDirect3DDemo : public KDirect3D { KOffScreenSurface m_texture[4]; public: HRESULT OnRender(void); HRESULT OnlnltCHINSTANCE hlnst): HRESULT OnDischarge(void); }: HRESULT KDirect3DDemo::0nInit(HINSTANCE hlnst) { D3DMATERIAL7 mtrl; memset(&mtrl. 0. sizeof(mtrl)); mtrl.ambient.г = l.Of; mtrl.ambient.g = l.Of; mtrl.ambient.b = l.Of: m_pD3DDevice->SetMaterial( &mtrl ): m_pD3DDevice->SetRenderState( D3DRENDERSTATE_AMBIENT. RGBA_MAKE(255. 255. 255. 0) ); D3DMATRIX mat: memset(& mat, 0. sizeof(mat)): mat.Jl = mat._22 = mat._33 = mat._44 = l.Of; // Матрица вида. 10 единиц по оси z D3DMATRIX matView = mat: matView._43 = 10.Of: m_pD3DDevice->SetTransform( D3DTRANSF0RMSTATE_VIEW. &matView ); mat. mat. mat. mat. mat. 11 = 22 - 34 = 43 = 44 = 2.Of 2.Of l.Of -O.lf O.Of m_pD3DDevice->SetTransform( D3DTRANSF0RMSTATE_PR0JECTI0N. &mat):
Непосредственный режим Direct3D 1053 // Разрешить использование Z-буфера m_pD3DDevice->SetRenderState( D3DRENDERSTATE_ZENABLE. TRUE); for (int i=0; i<4; i++) { const int nResIDE] = { IDBJIGER. IDB_PANDA. IDB_WHALE, IDBJLEPHANT }; BITMAPINFO * pDIB - LoadBMP(hInst, MAKEINTRESOURCE(nResID[i])); if ( pDIB ) m_texture[i].CreateTextureSurface(m_pD3DDevice. m_pDD. pDIB); else return E FAIL; m_bReady = true; return S_OK; } HRESULT KDirect3DDemo;:OnDischarge(void) { m_bReady = false; for (int i=0; i<4; i++) m_texture[i].Discharge(); return S_OK; } Класс KDirect3DDemo объявлен производным от класса KDirect3D. Он содержит массив объектов KOf fScreenSurface для хранения четырех текстур и переопределяет три метода (инициализация, уничтожение и вывод). Метод Onlnit выбирает простой белый материал, рассеянный белый свет, фиксированные матрицы вида и проекции, а также включает использование Z-бу- фера. В завершающей части метода Onlnit четыре текстурные поверхности инициализируются растровыми ресурсами. Метод OnDischarge освобождает ресурсы, выделенные методом Onlnit. Ниже приведена реализация метода OnRender. HRESULT DrawTriangle(IDirect3DDevice7 * pDevice. int xO, int yO. int zO. int xl. int yl. int zl. int x2. int y2. int z2) { D3DVERTEX vertices[3]: D3DVECT0R pl( (float)xO. (float)yO, (float)zO ) D3DVECT0R p2( (float)xl. (float)yl. (float)zl ) D3DVECT0R p3( (float)x2. (float)y2, (float)z2 ) D3DVECT0R vNormal = Normalize(CrossProduct(pl-p2. p2-p3)); // Инициализировать З вершины фронтальной стороны треугольника
1054 Глава 18. DirectDraw и непосредственный режим Direct3D vertices[0] = D3DVERTEXC pl. vNormal. 0.5f. O.Of ) verticesd] = D3DVERTEX( p2. vNormal. l.Of. l.Of ) vertices[2] = D3DVERTEX( p3. vNormal. O.Of. l.Of ) return pDevice->DrawPrimitive(D3DPT_TRIANGLELIST. D3DFVF_VERTEX. vertices. 3. NULL): HRESULT KDirect3DDemo::OnRender(void) { double time = GetTickCountO / 2000.0; m_pD3DDevice->Clear(0. NULL. D3DCLEARJARGET | D3DCLEAR_ZBUFFER. RGBA_MAKE(0. 0. Oxff, 0). l.Of. 0); if ( FAILEDC m_pD3DDevice->BeginScene() ) ) return E_FAIL; D3DMATRIX matLocal; memset(& matLocal. 0. sizeof(matLocal)); matLocal._11 = matLocal._33 = (FLOAT) cos( time ): matLocal._13 = matLocal._31 = (FLOAT) sin( time ): matLocal._22 = matLocal._44 = l.Of; m_pD3DDevice->SetTransform( D3DTRANSF0RMSTATE_W0RLD. &matLocal ); m_pD3DDevice->SetTexture( 0. m_texture[0] ): DrawTriangle(m_pD3DDevice. 0. 3. 0. 3. -3. 0. 0. -3. 3); m_pD3DDevice->SetTexture( 0. m_texture[l] ): DrawTriangle(m_pD3DDevice. 0. 3. 0. 0. -3. -3. 3. -3. 0): m_pD3DDevice->SetTexture( 0. m_texture[2] ); DrawTriangle(m_pD3DDevice. 0. 3. 0. -3. -3. 0. 0. -3. -3); m_pD3DDevice->SetTexture( 0. m_texture[3] ); DrawTriangle(m_pD3DDevice. 0. 3. 0, 0. -3. 3. -3. -3. 0); m_pD3DDevice->EndScene(); return S_0K; } Метод OnRender заполняет поверхность устройства Direct3D однородным синим цветом и сбрасывает Z-буфер, используя для этого метод IDirect3DDevice7::Clear. Вывод начинается с вызова BeginScene и завершается вызовом EndScene. Метод запрашивает системное время и использует его для настройки матрицы поворота вдоль оси у. Период вращения составляет примерно 12 секунд (2 х pi x 2). Затем метод выводит четыре грани пирамиды, причем на каждую грань накладывается своя текстура. Грани пирамиды рисуются в виде треугольников в трехмерном пространстве. Вспомогательная функция DrawTriangle получает три точки пространства в целочисленных координатах. Для представления вершин, требующихся при выводе точек, линий и треугольников, в Direct3D используется структура D3DVERTEX.
Итоги 1055 Простая вершина содержит координаты точки, вектор нормали и координаты текстуры. Координаты точки определяют местонахождение вершины в пространстве; вектор нормали задает направление поверхности, на которой находится точка, а координаты текстуры определяют позицию соответствующего пиксела на текстурном растре. При наложении текстуры Direct3D автоматически интерполирует текстуру для каждого пиксела треугольника. Функция DrawTriangle преобразует целочисленные координаты к формату с плавающей точкой, вычисляет вектор нормали к поверхности и задает для каждой вершины фиксированные координаты текстуры. Данные сохраняются в массиве D3DVERTEX и выводятся одним вызовом IDirect3DDevice: :DrawPrimitive — основным методом, предназначенным для вывода на устройствах Direct3D. На рис. 18.5 показан один из кадров при вращении пирамиды. Рис. 18.5. Пример использования непосредственного режима Direct3D Из-за богатых возможностей непосредственного режима Direct3D программирование для него оказывается слишком сложным делом, чтобы его можно было подробно описать на страницах этой книги. Обращайтесь к документации DirectX от Microsoft — в ней вы найдете неплохой учебник и примеры программ. Итоги В этой главе были представлены азы программирования для DirectDraw и непосредственного режима Direct3D. Мы рассмотрели процесс создания классов C++
1056 Глава 18. DirectDraw и непосредственный режим Direct3D для обобщенной поддержки DirectDraw/Direct3D. Эти классы отделены от манипулятора окна, поэтому они могут интегрироваться с любыми окнами. В части, посвященной DirectDraw, мы довольно подробно рассмотрели, как использовать метод Bit DirectDraw для вывода с аппаратным ускорением, как напрямую работать с зафиксированной поверхностью и как обратиться за помощью к GDI. Попутно были разработаны классы и методы для работы с внеэкранными поверхностями, текстурными поверхностями, Z-буферами и шрифтовыми поверхностями, а также для вывода текста. Надеюсь, автору удалось показать, что программирование для DirectDraw/ Direct3D — не такая уж сложная задача, особенно при наличии хорошо спроектированных классов C++. Область применения DirectDraw/Direct3D не ограничивается игровыми и учебными программами. Обычные оконные приложения тоже могут воспользоваться аппаратной поддержкой DirectDraw/Direct3D, чтобы улучшить качество вывода и сделать пользовательский интерфейс более удобным. Microsoft предоставляет неплохую документацию, учебники и примеры программ для DirectDraw, непосредственного режима Direct3D и других компонентов DirectX. Документацию и учебники можно найти в MSDN, а примеры программ - в Platform SDK и DirectX SDK. Microsoft также предлагает библиотеку классов для построения приложений непосредственного режима Direct3D; центральное место в этой библиотеке занимает класс CD3DAppl ication. Программный код находится в подкаталогах Include и Src\D3DFrame каталога Samples\MultiMedia\D3DIM комплекта SDK. В этой главе классы Direct3D от Microsoft не использовались, поскольку они слишком жестко объединяют приложение, окно и поддержку DirectDraw/Direct3D. Эта библиотека позволяет создавать приложения с единственным окном, поддерживающим Direct3D. Даже цикл обработки сообщений использует глобальную переменную для передачи сообщений виртуальным обработчикам, определенным в классе CD3DApplication. В библиотеку входит очень удобный класс для работы с текстурами, но нет класса общей поддержки поверхностей DirectDraw. Также есть чрезвычайно полезный класс для загрузки файлов DirectX в формате .X (формат Microsoft для представления трехмерных моделей). В целом библиотека содержит немало полезного кода, но для того, чтобы включить поддержку Direct3D в готовую оконную программу C++, вам придется немало потрудиться над ее адаптацией. Технология DirectX7 появилась относительно недавно. На момент написания этой книги еще не было хороших учебников, которые можно было бы порекомендовать. Возможно, наряду с документацией, учебниками и примерами от Microsoft вам понадобится хорошая книга по OpenGL, поскольку непосредственный режим Direct3D имеет с OpenGL много общего. Примеры программ К главе 14 прилагаются три программы и несколько классов для работы с DirectDraw и непосредственного режима Direct3D (табл. 18.1).
Итоги 1057 Таблица 18.1. Программы главы 18 Каталог проекта Описание Samples\Chapt_18\ddbasic Демонстрация основных возможностей DirectDraw Samples\Chapt_18\DemoDD Применение DirectDraw для вывода в дочерних окнах MDI, рисования пикселов, линий и замкнутых фигур, вывода растров, спрайтов и текста Samples\Chapt_18\DemoD3D Использование непосредственного режима Direct3D в окне SDI, работа со вторичной поверхностью, Z-буферы и текстурные поверхности
Алфавитный указатель А AbortDoc, 976 AbortPrinter, 955 AddFontMemResourceEx, 795 AddFontResource, 794 Adobe Type Manager (ATM), 765 AdvancedDocumentProperties, 964 AlphaBlend, 1007 ALTERNATE, режим заполнения, 501, 505 AngleArc, 455, 466 ANSI_CHARSET, 745 ANTIALIASED_QUALITY, 857, 875 AppendMenu, 588 ARABIC_CHARSET, 746 Arc, 454 ArcTo, 454 В BALTIC__CHARSET, 746 BeginPaint, 312, 390-391 BeginPath, 461, 466, 504, 878, 898 BitBlt, 576, 616, 1007, 1035 BITMAP, структура, 387, 598 BITMAPCOREHEADER, структура, 538 BITMAPFILEHEADER, структура, 549, 596 BITMAPINFO, структура, 214, 544, 594, 858, 920 BITMAPINFOHEADER, структура, 545, 549, 598, 858 BITMAPV4HEADER, структура, 538, 598 BITMAPV5HEADER, структура, 538, 598 BITSPIXEL, 974 BLACKNESS/WHITENESS, 616 BltBatch, 1014 BltFast, 1007, 1014, 1023 BMP, формат заголовок, 536 маски, 536 массив пикселов, 545 цветовая таблица, 536 Borland C++, 52 BoundsChecker (NuMega), 58, 241 BreakChar, 759 BRUSH, структура, 212 С C/C++, 31,56 C++, имена классов, 36 CallNextHookEx, 246 CAPTUREBLT, флаг, 612 CGdiObject, 386 CHINESEBIG5_CHARSET, 746 Chord, 498, 927 ClientToScreen, 344 CLIPCAPS, 974 CLIPOBJ, структура, 218 CloseEnhMetaFile, 899 CloseFigure, 463 стар, таблица, 767 COLOR_GRADIENTINACTIVE- CAPTION, 488 COLOR_SCROLLBAR, 488 COLORREF, 246, 403, 664, 1018 COM (Component Object Model), 31, 1001 CombineRgn, 514 COMMCTRL.DLL, 589 COMPLEXREGION, 393 CopyEnhMetaFile, 908 CopyToClipBoard, 911 CreateBitmap, 565, 583 CreateBitmapIndirect, 566 CreateBitmapSurface, 1034 CreateBrushlndirect, 489 CreateCompatibleBitmap, 567
Алфавитный указатель 1059 CreateCompatibleDC, 343 CreateDC, 307, 392 CreateDIBitmap, 569 CreateDIBPalette, 722 CreateDIBPatternBrush, 484 CreateDIBPatternBrushPt, 484 CreateDIBSection, 595 CreateDiscardableBitmap, 569 CreateEllipticRegion, 508, 922 CreateEllipticRegionlndirect, 508 CreateEnhMetafile, 152, 898 CreateEvent, 79 CreateFile, 177 CreateFont, 806 CreateFontlndirect, 806 CreateFontlndirectEx, 806, 808 CreateHalftonePalette, 411, 706 CreateHatchBrush, 482 CreateIC, 343 CreatePatternBrush, 484 CreatePen, 430, 918 CreatePenlndirect, 433 CreatePolygonRgn, 509 CreatePrimary Surface, 1013 CreateRectRgn, 171, 393 CreateRoundRectRgn, 508 CreateScalableFontResource, 793 CreateService, 181 CreateWindow/CreateWindowEx, 40, 390 CS (Code Segment), 47 CURVECAPS, 974 D D3DVERTEX, структура, 1054 DD_SURFACE_INT, структура, 237 DD_SURFACE_LOCAL, структура, 237 DD_SURFACE_MORE, структура, 237 DDA, алгоритмы, 445 DDBLTFX, структура, 1014 DDCOLORKEY, структура, 1035 DDCREATE_EMULATEONLY, 1005 DDCREATE_HARDWAREONLY, 1005 DDI, интерфейс, 275 DDK (Device Driver Kit), 31 DDPIXELFORMAT, структура, 1020 ddraw.dll, 104 DDSURFACEDESC2, структура, 1016 DEFAULT_CHARSET, 755, 822 DEFAULT_PITCH, 754 DefWindowProc, 591 DeleteEnhMetaFile, 900 DeleteObject, 385, 428, 481 Delphi, 31, 52 DESIGNVECTOR, структура, 794 DEVLEVEL, структура, 222 DEVMODE, структура, 699, 957 DIBSECTION, структура, 387 DIB-секции CreateDIBSection, 595 GetDIBColorTable, 599 SetDIBColorTable, 599 общие сведения, 593, 666 Direct3D, 100, 1043 Direct Animation, 101 DirectDraw, 42, 100, 102, 1000 HAL, 105 HEL, 105 IDirectDraw, интерфейс, 101, 103 IDirectDrawClipper, интерфейс, 1020 IDirectDrawColorControl, интерфейс, 103 IDirectDrawGammaControl, интерфейс, 103 IDirectDrawPalette, интерфейс, 103 IDirectDrawSurface, интерфейс, 103 IDirectDrawSurface7, интерфейс, 103 IDirectDrawVideoport, интерфейс, 103 архитектура, 103 прямой доступ к пикселам, 1016 структуры данных, 232 Directlnput, 100 DirectMusic, 100 DirectPlay, 100 Direct Setup, 100 DirectShow, 101 DirectSound, 100 DirectX, 99, 383 DllGetClassObject, 1004 DocumentProperties, 963 DPtoLP, 492 DrawText, 866, 929
1060 Алфавитный указатель DRVENABLEDATA, 205 DS (Data Segment), 47 DSTINVERT, 620 dumpbin.exe, 54 E EASTEUROPE_CHARSET, 746 EDD_DIRECTDRAW_CLOBAL, структура, 234 EDD_DIRECTDRAW_LOCAL, структура, 233 EDD_SURFACE, структура, 237 Ellipse, 498, 927 EMF (расширенные метафайлы), 82, 604, 897 воспроизведение, 900 записи, 912 недокументированные типы записей, 940 палитры, 922 текст, 928 траектории, 922 EmptyClipBoard, 911 EMRBITBLT, структура, 919 EndDoc, 976 EndPage, 975 EndPath, 463, 504, 922 ENHMETAHEADER, структура, 913 EnumDisplayDevices, 294 EnumDisplaySettings, 295 EnumEnhMetaFile, 930 EnumeratePrinters, 960 EnumFontFamiliesEx, 755 ENUMLOGFONTEXW, структура, 225 EnumObjects, 428, 480 EnumSystemCodePages, 750 EPALOBJ, структура, 215 EqualRegion, 513 ETO_CLIPPED, 850 ETO_GLYPH_INDEX, 850 ETOJGNORELANGUAGE, 850 ETO_NUMERICSLATIN, 850 ETO_NUMERICSLOCAL, 850 ETO_OPAQUE, 850 ETO_PDY, 850 ETO_RTLREADING, 850 ExtCreatePen, 433 ExtCreateRegion, 171, 519 EXTLOGFONTW, структура, 930 EXTLOGPEN, структура, 214 ExtSelectClipRegion, 395 ExtTextOut, 818, 864, 928 F FD_GLYPHSET, структура, 226 FF_DECORATIVE, 754 FF_DONTCARE, 754 FF_MODERN, 754 FF_ROMAN, 754 FF_SCRIPT, 754 FF_SWISS, 754 FillPath, 471, 504, 888 FillRect, 493, 928 FillRgn, 522, 928 FindResource, 909 FirstChar, 759 FIXED, структура, 863 FIXED_PITCH, 754 FlattenPath, 467, 878 FLOATOBJ, структура, 175 FNT, расширение, 758 FON, расширение, 758 FONTEDIT, утилита, 758 FONTOBJ, структура, 228 G GCPJUSTIFY, флаг, 853 GCP_REORDER, флаг, 853 GCP_USEKERNING, флаг, 853 GDI, 93, 102 API, 273 OpenGL, 96 архитектура, 93 манипуляторы, 143 недокументированные функции, 96 объекты, 383 системные DLL, 95 экспортируемые функции, 94 GDI+, 1000 GDI32.DLL, 52, 157, 159 GetAspectRatioFilterEx, 881 GetBkColor, 483, 833 GetBkMode, 483 GetBoundsRect, 904 GetCharABCWidthFloat, 842 GetCharABCWidthI, 847
Алфавитный указатель 1061 GetCharABCWidths, 842 GetCharacterPlacement, 846, 851 GetCharWidth32, 842 GetCharWidthI, 847 GetClientRect, 343 GetClipboardData, 911 GetClipBox, 396 GetClipRgn, 393 GetCurrentObject, 393 GetCurrentProcessId, 163 GetDC, 310, 400 GetDCBrushColor, 480 GetDCOrgEx, 343 GetDCPenColor, 429 GetDefaultPrinter, 972 GetDeviceCaps, 205, 416, 970 GetEnhMetaFileBits, 916 GetEnhMetaFileHeader, 906 GetGlyphlndices, 846, 852 GetKerningPairs, 847 GetMetaRgn, 397 GetNearestColor, 411 GetNearestPalettelndex, 412 GetObject, 387 GetObjectType, 161, 168 GetPaletteEntries, 412 GetPath, 463, 922 GetPathData, 890 GetPixel, 417, 663, 1020 GetPolyFillMode, 501 Get Printer, 961, 974 GetRandomRgn, 398-399 GetRegionData, 516 GetROP2, 423 GetStockObject, 152 GetSurfaceDesc, 1020 GetSysColor, 488 GetSysColorBrush, 488 GetTabbedTextExtent, 865 GetTextABCWidths, 846 GetTextABCWidthsFloat, 846 GetTextCharacterExtra, 839 GetTextCharSet, 819 GetTextCharSetlnfo, 819 GetTextExtentPoint32, 839 GetTextFace, 827 GetUpdateRegion, 391 GetWindowDC, 310 GetWindowRect, 343 GGO_BEZIER, 857, 861 GGO_BITMAP, 860 GGO_GLYPH_INDEX, 857 GGO_GRAY2__BITMAP, 857 GGO_GRAY4_BITMAP, 857 GGO_GRAY8_BITMAP, 857 GGO_METRICS, 857 GGO_NATIVE, 861 GGO_UNHINTED, 861 GIF, формат, 546 glyf, таблица, 767, 773 GLYPHINFO, таблица, 760 GLYPHMETRICS, структура, 856 GRADIENT_RECT, структура, 524 GRADIENTJTRIANGLE, структура, 524 GradientFill, 523 GREEK_CHARSET, 746 GUID, 1002 H HAL, 105 HANDLETABLE, структура, 918 HANGUL_CHARSET, 746 HBITMAP, 210, 594 HBRUSH, 151, 480 HDC, 151 HEBREW_CHARSET, 746 HEL, 105 HENHMETAFILE, 898 HFONT, 151 HGDIOBJ, 151,262,480 HGLOBAL, 214 HINSTANCE, 149 HLS, цветовое пространство, 406, 419 HMENU, 151 HMODULE, 149 HORZRES, 906 HORZSIZE, 906 HPEN, 151, 428 HWND, 151 I IClassFactory, интерфейс, 1004 ICM (Image Color Management), 86 IDirect3D7, интерфейс, 1046 IDirectDraw, интерфейс, 103 IDirectDraw2, интерфейс, 1005 IDirectDraw7, интерфейс, 1005
1062 Алфавитный указатель IDirectDrawClipper, интерфейс, 103, 1020 IDirectDrawColorControl, интерфейс, 103 IDirectDrawGammaControl, интерфейс, 103 I Direct DrawPalette, интерфейс, 103 IDirectDrawSurface, интерфейс, 103 IDirectDrawVideoport, интерфейс, 103 IFIMETRICS, структура, 226 IMAGE_DOS_HEADER, 63 IMAGE_NT_HEADERS, 63 IMAGE_OPTIONAL__HEADER, 64 InflateRect, 491 InsertMenuItem, 588 IntersectRect, 491 InvertRect, 928 InvertRgn, 522, 928 IUnknown, интерфейс, 1001 з JOHAB_CHARSET, 746 JPEG, 607 К KERNEL32.DLL, 97, 159, 183 L LastChar 759 LAYOUT JBITMAPORIENTATION- PRESERVED, 837 LAYOUT_RTL, 838 LFONT, структура, 228 LINEATTRS, структура, 222 LINECAPS, 974 LineDDA, 476 LineDDAProc, 445 LineTo, 442 LoadBitmap, 716, 909 Loadlmage, 717, 909 LoadResource, 149, 909 loca, таблица, 772 LOGBRUSH, структура, 170, 214, 387 LOGFONT, структура, 387, 755 LOGFONTW, структура, 225 LOGPALETTE, структура, 214, 708 LOGPEN, структура, 387 LOGPIXELX, 899 LOGPIXELY, 899 LPDIRECTDRAW, 232 LPDIRECTDRAWSURFACE, 232 LPtoDP, 395, 492 M MAC_CHARSET, 746 MAKEROP4, макрос, 635 MaskBlt, 636 MAT2, структура, 857 MDI (Multiple Document Interface), 40 MERGECOPY, 617, 619 MERGEPAINT, 622 METAFONT, 744 MFC (Microsoft Foundation Classes), 31, 46 Microsoft Knowledge Base, 508 Microsoft Word, 897 MM_ANISOTROPIC, режим отображения, 352, 935 MM_HIENGLISH, режим отображения, 348 MM_HIMETRIC, режим отображения, 350 MM JSOTROPIC, режим отображения, 351 MM_LOENGLISH, режим отображения, 348 MM_LOMETRIC, режим отображения, 350 ММ_ТЕХТ, режим отображения, 348, 487, 842, 977 MM_TWIPS, режим отображения, 351 MoveToEx, 442, 929 MSDN (Microsoft Developer Network), 59 Multiple Master OpenType, шрифты, 794 N NextBand, 959 nmake.exe 54 NOMIRRORBITMAP, флаг, 612, 837 NOTSRCCOPY, 617 NOTSRCERASE, 622
Алфавитный указатель 1063 Novell Netware, провайдер печати, 109 NTFS (NT File System), 75 NTOSKRNL.EXE, 92 NULL, регион отсечения, 393 NULLREGION, 393 NUMCOLORS, 974 О OBJ_ENHMETAFILE, 898 OEM_CHARSET, 746 OffsetClipRgn, 396 OffsetRgn, 520 OffsetViewportOrgEx, 983 OLE, 31 OnDraw, 983 OPAQUE, режим заполнения фона, 427 OpenGL, 89, 96 OpenType, шрифты, 811 OUTLINETEXTMETRIC, структура, 796,811-812 Р PAGESETUP, структура, 968 PageSetupDlg, 968 PaintRgn, 522, 928 PAINTSTRUCT, структура, 312 PALETTE, 210 PALETTEENTRY, структура, 699 PALETTEINDEX, 412, 705 PALETTERGB, 412, 705 PALOBJ, структура, 216 PANOSE, система подстановки шрифтов, 812 PANOSE, структура, 757 PatBlt, 1007, 1014 PATCOPY, 639 PATH, структура, 221 PATHDATA, структура, 224 PATHDEF, структура, 221 PATHDT, структуры, 221 PATHOBJ, структура, 220 PathToRegion, 507, 895 PATINVERT, 635, 872 PATPAINT, 623 PCL, 107 PCX, 546 PDEV, структура, 200 PDEV_WIN32K, 201 РЕ, формат исполняемых файлов, 61, 149 PFE, структура, 227 PFF, структура, 227 PFT, структура, 228 Pie, 498 PLANES, 974 PlayEnhMetaFile, 606, 901, 923-924, 959 PlayEnhMetaFileRecord, 931 PlgBlt, 628, 663, 1007 PNG, формат, 536 POINT, структура, 443, 492, 856 PolyBezier, 449 PolyBezierTo, 442 Poly Draw, 451, 466, 862 POLYGONALCAPS, 974 PolyLineTo, 442 PolyPolygon, 504, 927 PolyPolyline, 462 PolyTextOut, 928 PostScript, 107, 603 PrintBand, 959 PrintDialog, 971 PrintDlg, 966, 968 PRINTDLG, структура, 967-968 PrinterProperties, 964 profile.exe, 54 PS_ALTERNATE, 435, 487 PS_COSMETIC, 433 PS_DASH, 431 PS_DASHDOT, 431 PS_DASHDOTDOT, 431 PS_DOT, 431 PS_ENDCAP_FLAT, 437 PS_ENDCAP_ROUND, 437 PS_ENDCAP_SQUARE, 437 PS_GEOMETRIC, 433 PSJNSIDEFRAME, 431 PS_JOIN_BEVEL, 433 PSJOIN_MITER, 433 PS JOIN_ROUND, 433 PS_NULL, 433 PS_SOLID, 457 PS_USERSTYLE, 435 PT_CLOSEFIGURE, 466 Q Query Interface, 1006 QuickDraw GX (Apple), 855
1064 Алфавитный указатель R R2_MASKPEN, 425, 487, 529 R2_MERGEPEN, 529 R2_NOP, 424 R2_NOT, 424 R2_NOTCOPYPEN, 425 R2_NOTXORPEN, 425 R2_WHITE, 425 R2_XORPEN, 425 RASTERCAPS, 974 RAW, формат спулинга, 109 RC_BITBLT, 577 RC_PALETTE, 698 RDTSC, инструкция процессора, 48 RealizePalette, 412 rebase.exe, 54 RECT, структура, 171, 490, 1014 Rectangle, 492 RectlnRegion, 513 REGION, структура, 217, 221 REGIONOBJ, 517 ReleaseDC, 1015 RemoveFontResource, 794 RemoveFontResourceEx, 794 RestoreDC, 471 RFONT, структура, 229 RGB, цветовое пространство, 286, 4( RGBQUAD, структура, 544 RGBTRIPLE, структура, 544, 667 RGNDATA, структура, 520, 926 RoundRect, 522 RUSSIAN_CHARSET, 746 s SaveDC, 471 SCAN, структура, 218, 517 SelectClipPath, 922 SelectClipRgn, 394, 929 SelectObject, 161, 385 SelectPalette, 385, 706 SelectRegion, 922 SetAbortProc, 976 SetBkColor, 483, 583, 833 SetBkMode, 483, 929 SetBoundsRect, 904 SetBrushOrgEx, 484 SetClipboardData, 911 SetClipPath, 895 Set Clipper, 1032 SetClipRgn, 398 SetDCPenColor, 429, 480 SetDIBColorTable, 599, 725 SetDIBitsToDevice, 561 SetEnhMetaFileBits, 909 SetMenuItemBitmaps, 584 SetMenuItemlnfo, 584, 588 SetMetaRgn, 397, 939 SetMiterLimit, 439 SetPixel, 416, 663, 927 Set Pixel V, 416 SetPolyFillMode, 501 SetRect, 491 SetRectRgn, 172 SetROP2, 423 SetSourceColorKey, 1035 SetStretchBltMode, 558 SetSysColor, 488 SetTextAlign, 929 SetTextCharacterExtra, 839 SetTextColor, 583 SetTextJustification, 840 SetViewportExtEx, 924 SetWindowExtEx, 942 SetWindowRgn, 310, 391, 507 SetWindowsHookEx, 244 SetWorldTransform, 924 SHIFTJIS_CHARSET, 746 SIMPLEREGION, 393 SoftlCE/W, 58 SPOOLSV.EXE, 955 SPRITESTATE, структура, 204, 236 Spy++, 54 SRCAND, 624 SRCCOPY, 556 SRCINVERT, 632 SRCPAINT, 645 SS (Stack Segment), 47 StartDoc, 975 StartPage, 977 STI (Still Image) API, 89 STM_SETIMAGE, сообщение, 909 STRETCH_ANDSCANS, 558 STRETCH_DELETESCAN, 559 STRETCH_HALFTONE, 559 STRETCH_ORSCANS, 558 StretchBlt, 577, 641, 1014 StretchDIBits, 556, 645 STRICT, макрос, 32, 55 StrokeAndFillPath, 471, 888
Алфавитный указатель 1065 StrokePath, 471, 888 SubtractRect, 491 SUCCEEDED, макрос, 1004 SURFACE, 210 SURFOBJ, 208 SYMBOL_CHARSET, 755 T TA_BASELINE, 835 TA_BOTTOM, 836 TA_CENTER, 835 TA_LEFT, 835 TA_NOUPDATECP, 834 TA_RIGHT, 836 TA_RTLREADING, 837 TA_TOP, 835 TA_UPDATECP, 835 TabbedTextOut, 865 TBBUTTON, 55 TEXT, формат спулинга, 110 TEXTCAPS, 974 TEXTMETRIC, структура, 812, 827 TextOut, 834, 860 THAI_CHARSET, 746 TIFF, формат, 546 TRANSPARENT, режим заполнения фона, 427 TransparentBlt, 627, 1007 TRIVERTEX, структура, 524 TrueType, шрифты, 765 инструкции, 782 таблица PostScript, 791 имен, 791 кернинга, 789 формат, 765 TT_PRIM_CSPLINE, 862 TT_PRIM_LINE, 862 TT_PRIM_QSPLINE, 862 TTPOLYCURVE, структура, 861 TTPOLYGONHEADER, структура, 862 TURKISH_CHARSET, 746 и Unicode, 750, 846, 928 UniDriver, 75 UNIDRVUI.DLL, 113 Uniscribe, 854 USER32.DLL, 52, 495, 872 V VARIABLE_PITCH, 754 VERTRES, 906 VERTSIZE, 906 VIETNAMESE_CHARSET, 746 Visual Basic, 31, 52 Visual C++, 52 VTune (Intel), 58 w WidenPath, 468, 502 WIN32K.SYS, 92, 383 WinDbg, 51 WINDING, режим заполнения, 501 Windows NT 4.0/2000, 51 WINSPOOL.DRV, 955 WM_CREATE, сообщение, 40, 1050 WM_DESTROY, сообщение, 1050 WM_DISPLAYCHANGE, сообщение, 699 WM_ERASEBKGND, сообщение, 589 WM_FONTCHANGE, сообщение, 794 WMJNITDIALOG, сообщение, 592 WM_MOUSEMOVE, сообщение, 425 WM_NCPAINT, сообщение, 1023 WM_PAINT, сообщение, 324, 329, 922 WM_PALETTECHANGED, сообщение, 700, 922 WM_PALETTEISCHANGING, сообщение, 711 WM_QUERYNEWP ALETTE, сообщение, 710 WM_SIZE, сообщение, 1050 WNDCLASSEX, структура, 37 WriteFile, 381 WritePrinter, 112,957 WS_EX_LAYOUTRTL, 837 WS_EX_RIGHT, 837 z Z-буфер, 291, 1047 Z-размывка, 292
1066 Алфавитный указатель А адресное пространство режима ядра, доступ, 153 алгоритмы цветовых преобразований растров, 672 альфа-канал, 651 альфа-наложение имитация, 659 общие сведения, 649 аппаратно-зависимые растры (DDB) CreateBitmap, 565 CreateBitmapIndirect, 566 CreateCompatibleBitmap, 567 CreateDIBitmap, 569 LoadBitmap, 570 массив пикселов, 596 общие сведения, 166, 564 аппаратно-независимые растры (DIB) SetDIBitsToDevice, 561 StretchDIBits, 556 вывод, 556 преобразование цветового формата, 559 растровые операции, 559 арабская письменность, 836 архитектура GDI, 93 Windows, 71 графической системы Windows, 84 системы печати, 106 ассемблер, 46 аффинные преобразования ассоциативность, 361 замкнутость, 361 кривые Безье, 360 линии, 360 обратные, 361 общие сведения, 358 параллельность, 360 растры, 667 свойства, 359 тождественность, 361 эллипсы,360 Б базовая линия, 752 Безье, 447 бинарные операции растровые, 422 с регионами, 394 блиттинг, 1014 Брезенхэм, алгоритм, 1026 В векторные шрифты, 762 видеоадаптер, 282 виртуальная память, 158 внешний зазор, 803 внеэкранная поверхность, 1033 внутренний зазор, 803 выключка, 839 выравнивание текста, 833 Г гамма-коррекция, 676 геометрические перья, 436 гистограмма, 686 глифы индекс в таблице, 760 кириллицы, 819 определение, 751 основных пикселов, 426 расшифровка контура, 862 фоновых пикселов, 426 градиентные заливки, 523 в пространстве HLS, 529 радиальные, 530 режимы, 523 д дамп, 275 декоративные шрифты, 755 драйверы ввода-вывода, 59 режима ядра, 93 устройств Microsoft Windows, 72 файловой системы, 59 экрана, 59 ДУГИ AngleArc, 455 Arc, 454 ArcTo, 454 общие сведения, 454
Алфавитный указатель 1067 дуги {продолжение) определение в градусах, 455 преобразование в кривые Безье, 457 3 замкнутые фигуры градиентные заливки, 523 закраска, 532 замкнутые траектории, 504 кисти, 479 многоугольники, 500 общие сведения, 479 прямоугольники, 490 регионы, 509 сегменты, 498 секторы, 498 текстурные заливки, 532 эллипсы, 498 И инкапсуляция, 144 инструкции глифов, 782 Интернет, 85 интерфейсный указатель, 1003 информационный контекст устройства, 315 исполнительная часть, 72 к квадратичные кривые Безье, 861 квантование по октантному дереву, 726 цветов, 726 кватернарные растровые операции, 635 кернинг, 847 кисти LOGBRUSH, структура, 489 базовая точка, 484 логические, объект, 479 общие сведения, 479 пользовательские, 481 системные цвета, 488 стандартные, 480 клиентская область, 391 Кнут, Дональд, 744 коллекции шрифтов, 792 компилятор, 52, 55 контекст устройства Windows 2000, 320 атрибуты, 304 информационный, 315 метафайловый, 316 общие сведения, 297 получение информации о возможностях, 299 родительский, 315 связь с окном, 307 совместимый, 316 создание, 298 контрольная сумма, 157, 159 косметические перья, 435 кривые Безье PolyBezier, 449 PolyBezierTo, 452 Poly Draw, 451 аффинная инвариантность, 447 делимость, 448 общие сведения, 447, 862 преобразование дуг, 457 Л лигатура, 751 линии, 442, 862 логические палитры, 705 палитра по умолчанию, 705 полутоновая палитра, 706 логические шрифты, 767 CreateFont, 806 CreateFontlndirect, 806 CreateFontlndirectEx, 806 LOGFONT, структура, 806 внешний зазор, 803 внутренний зазор, 803 имитация начертаний, 754 метрики А-В-С, 803 надстрочный интервал, 786 подстрочный интервал, 787 локальный провайдер печати, 109 м Мандельброта, множество, 418 манипуляторы, 143, 149
1068 Алфавитный указатель массив пикселов, 545 метарегион, 319, 391 метафайловый контекст устройства, 316 метафайлы, 897 EMF воспроизведение, 900 записи, 912 недокументированные типы записей, 940 палитры, 922 текст, 928 траектории, 922 WMF (метафайлы Windows), 897 микроядро, 73 мини-драйверы, 75 многоугольники, 500, 920 морфологические фильтры, 693 н надстрочный интервал, 786 О обновляемый регион, 391 объектно-ориентированное программирование, 144 классы, 144 манипуляторы, 149 объекты ядра, 149 однородные кисти, 481 отсечение бинарные операции с регионами, 394 видимость, 391 обновляемый регион, 391 общие сведения, 390, 1031 регион Рао, 398 системный регион, 391 п палитры, 697 алгоритм Флойда— Стейнберга, 739 в EMF, 922 квантование цветов, 726 логические, 705 основная палитра, 707 палитры {продолжение) реализация, 707 системная палитра, 698 сообщения, 710 фоновая палитра, 707 параллелограммы, блиттинг, 628, 667 перья, 427 логические, 427 расширенные, 433 стандартные, 429 печать, 947 архитектура, 106 драйвер принтера, 961 единая логическая система координат, 979 процессор печати, 958 прямой вывод в порт, 953 растры, 993 стандартные диалоговые окна, 965 пикселы, 370, 380 отсечение, 390 цвет, 402 подстановка шрифтов, 810 подстрочный интервал, 787 полная ширина, 787, 803 полупрозрачная заливка, 528 полутоновые палитры, 706 провайдер печати, 109 прокрутка, 373 пространственные фильтры, 686 процессоры печати PSCRIPT1, 111 назначение, ПО Р рабочий стол, вывод, 35 радиальные градиентные заливки, 530 Рао, регион, 319 растровая графика, 535 растровые кисти, 485 операции, кодировка, 609 шрифты, 758, 760 растры DIB-секции, 593 GIF, формат, 536 JPEG, формат, 536 TIFF, формат, 536 аппаратно-зависимые (DDB), 564
Алфавитный указатель 1069 растры {продолжение) аппаратно-независимые (DIB), 559 аффинные преобразования, 667 в EMF, 919 печать, 993 пометка команд меню, 584 пространственные фильтры, 686 совместимые контексты устройств, 563 расширенные перья, 433 реализация палитры, 707 регион, 509 в EMF, 921 контекста устройства, 310 метарегион, 319 окна, 391 отсечения, 319, 393 получение данных, 510 Рао, 319 системный, 318 создание объектов, 507 режимы заполнения фона, 427 оконный, 41 отображения MM_ANISOTROPIC, 352, 935 MM_HIENGLISH, 348 MM_HIMETRIC, 350 MMJSOTROPIC, 351 MM_LOENGLISH, 348 MM_LOMETRIC, 350 MM_TEXT, 348, 487, 842, 977 MM_TWIPS, 351 родительский контекст устройства, 315 с сегмент, 498 сектор, 498 семейство шрифтов, 754 системная палитра, 698 системные процессы, 72 системный регион, 318, 391 системы координат в EMF, 924 мировая, 341, 361 страничная, 341 устройства, 343 физическая, 341 совместимый контекст устройства, 316 спулер, 947 среда программирования, 50 стандартные кисти, 480 перья, 429 статические цвета, 702 страничная система координат назначение, 341 режимы отображения, 345 т твипы, 351 текстурные растры, 1050 текстуры, 292 тернарные растровые операции, 609 BLACKNESS/WHITENESS, 616 DSTINVERT, 617 MERGECOPY, 617 MERGEPAINT, 622 NOTSRCCOPY, 617 NOTSRCERASE, 622 PATCOPY, 617 PATINVERT, 635 SRCAND, 620, 639, 644 SRCERASE, 622 SRCINVERT, 623, 644 SRCPAINT, 645 список, 614 траектории, 461 в EMF, 922 замкнутые, 504 получение данных, 463 построение, 461 треугольные градиентные заливки, 523 У указатели и манипуляторы, 148 уменьшение цветовой глубины растра, 726 упакованные DIB-растры, 545 Ф фабрика класса, 1004 Флойда—Стейнберга, алгоритм, 739
1070 Алфавитный указатель фоновая палитра, 707 форматирование текста, 864 ц цвет фона, 427 цветовые ключи, 640 ш ширина символа, 840 шрифты, 744 FontSmart Homage Page (HP), 799 PANOSE, 815 TrueType, 765 TrueType/OpenType, 812 в GDI, 224 глифы, 751 шрифты (продолжение) кодировка, 745 логические, 767 моноширинные, 754 получение информации, 818 растровые, 760 семейства, 754 установка, 793 устройств, 810 штриховые кисти, 482 э эллипсы, 360, 498 Я язык описания страниц (PDL), 951 языковой монитор, 112
ишалтшаьекий аом специалистам ' книжного ^^^ \л/\л/\л/ pitpp г.пкл WWW.PITER.COM БИЗНЕСА! УВАЖАЕМЫЕ ГОСПОДА! ИЗДАТЕЛЬСКИЙ ДОМ «ПИТЕР» ПРИГЛАШАЕТ ВАС К ВЗАИМОВЫГОДНОМУ СОТРУДНИЧЕСТВУ. МЫ ПРЕДЛАГАЕМ ЭКСКЛЮЗИВНЫЙ АССОРТИМЕНТ КОМПЬЮТЕРНОЙ, МЕДИЦИНСКОЙ, ПСИХОЛОГИЧЕСКОЙ, ЭКОНОМИЧЕСКОЙ И ПОПУЛЯРНОЙ ЛИТЕРАТУРЫ. МЫ ГОТОВЫ РАБОТАТЬ ДЛЯ ВАС НЕ ТОЛЬКО В САНКТ-ПЕТЕРБУРГЕ. НАШИ ПРЕДСТАВИТЕЛЬСТВА НАХОДЯТСЯ В МОСКВЕ, МИНСКЕ, КИЕВЕ, ХАРЬКОВЕ. ЗА ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ ОБРАЩАЙТЕСЬ ПО СЛЕДУЮЩИМ АДРЕСАМ: Россия, г. Москва Россия, г. С.-Петербург Представительство издательства «Питер», Представительство издательства «Питер», м. «Калужская», ул. Бутлерова, д. 176, оф. 207 м. «Электросила», ул. Благодатная, д. 67, и 240, тел./факс (095) 777-54-67. тел. (812) 327-93-37,294-54-65. E-mail: sales@piter.msk.ru E-mail: sales@piter.com Украина, г. Харьков Украина, г. Киев Представительство издательства «Питер», Филиал Харьковского представительства тел. (0572) 14-96-09, факс: (0572) 28-20-04, издательства «Питер», тел./факс: (044) 490-35-68, 28-20-05. Почтовый адрес: 61093, г. Харьков, 490-35-69. Адрес для писем: 04116, г. Киев-116, а/я 9130. E-mail: piter@tender.kharkov.ua а/я 2. Фактический адрес: 04073, г. Киев, пр. Красных Казаков, д. 6, корп. 1. E-mail: otfice@piter-press.kiev.ua Беларусь, г. Минск Представительство издательства «Питер», тел./факс (37517) 239-36-56. Почтовый адрес: 220100, г. Минск, ул. Куйбышева, 75. 000 «Питер М», книжный магазин «Эврика». E-mail: piterbel@tut.by КАЖДОЕ ИЗ ЭТИХ ПРЕДСТАВИТЕЛЬСТВ РАБОТАЕТ С КЛИЕНТАМИ ПО ЕДИНОМУ СТАНДАРТУ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР». £^ Ищем зарубежных партнеров или посредников, имеющих выход на зарубежный рынок. ^ Телефон для связи: (812) 327-93-37. E-mail: grigorjan@piter.com fy£ Редакции компьютерной, психологической, экономической, юридической, медицинской, ^ учебной и популярной (оздоровительной и психологической) литературы Издательского дома «Питер» приглашают к сотрудничеству авторов. Обращайтесь по телефонам: Санкт-Петербург - тел. (812) 327-13-11, Москва-тел.: (095)234-38-15,777-54-67.
ПЗЛАТЕПЬСКПП ПОМ [>^ППТЕР* *4^ WWW.PITER.COM Башкортостан Уфа, «Азия», ул. Зенцова, д. 70 (оптовая продажа), маг. «Оазис», ул. Чернышевского, д. 88, тел./факс (3472) 50-39-00. E-mail: asiaufa@ufanet.ru Дальний Восток Владивосток, «Приморский Торговый Дом Книги», тел./факс (4232) 23-82-12. Почтовый адрес: 690091, Владивосток, ул. Светланская, д. 43. E-mail: bookbase@mail.primorye.ru Хабаровск, «Мире», тел. (4212) 30-54-47, факс 22-73-30. Почтовый адрес: 680000, Хабаровск, ул. Ким-Ю-Чена, д. 21. E-mail: postmaster@bookmirs.khv.ru Хабаровск, «Книжный мир», тел. (4212) 32-85-51, факс 32-82-50. Почтовый адрес: 680000, Хабаровск, ул. Карла Маркса, д. 37. E-mail: postmaster@worldbooks.knt.ru Европейские регионы России Архангельск, «Дом Книги», тел. (8182) 65-41-34, факс 65-41 -34. Почтовый адрес: 163061, пл. Ленина, д. 3. E-mail: book@atnet.ru Калининград, «Вестер», тел./факс (0112) 21 -56-28, 21 -62-07. Почтовый адрес: 236040, Калининград, ул. Победы, д. 6. Магазин «Книги & книжечки». E-mail: nshibkova@vester.ru; www.vester.ru Ростов-на-Дону, ПБОЮЛ Остроменский, пр. Соколова, д. 73, тел./факс (8632) 32-18-20. E-mail: ostrom@don.sitek.net Северный Кавказ Ессентуки, «Россы», ул. Октябрьская, 424, тел./факс (87934) 6-93-09. E-mail: rossy@kmw.ru Сибирь Иркутск, «ПродаЛить», тел. (3952) 59-13-70, факс 51 -30-70. Почтовый адрес: 664031, УВАЖАЕМЫЕ ГОСПОДА! КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР» ВЫ МОЖЕТЕ ПРИОБРЕСТИ ОПТОМ И В РОЗНИЦУ У НАШИХ РЕГИОНАЛЬНЫХ ПАРТНЕРОВ. Иркутск, ул. Байкальская, д. 172, а/я 1397. E-mail: prodalit@irk.ru; http:/www.prodalit.irk.ru Иркутск, «Антей-книга», тел./факс (3952) 33-42-47. Почтовый адрес: 664003, Иркутск, ул. Карла Маркса, д. 20. E-mail: antey@irk.ru Красноярск, «Книжный Мир», тел./факс (3912) 27-39-71. Почтовый адрес: 660049, Красноярск, пр. Мира, д. 86. E-mail: book-world@public.krasnet.ru Нижневартовск, «Дом книги», тел. (3466) 23-27-14, факс 23-59-50. Почтовый адрес: 628606, Нижневартовск, пр. Победы, д. 12. E-mail: book@nvartovsk.wsnet.ru Новосибирск, «Топ-книга», тел. (3832) 36-10-26, факс 36-10-27. Почтовый адрес: 630117, Новосибирск, а/я 560. E-mail: office@top-kniga.ru; http://www.top-kniga.ru Тюмень, «Друг», тел./факс (3452) 21-34-39, 21-34-82. Почтовый адрес: 625019, ул. Республики, д. 211. E-mail: drug@tyumen.ru Тюмень, «Фолиант», тел. (3452) 27-36-06, факс 27-36-11. Почтовый адрес: 625039, Тюмень, ул. Харьковская, д. 83а. E-mail: foliant@tyumen.ru Татарстан Казань, «Таис», тел. (8432) 72-34-55, факс 72-27-82. Почтовый адрес: 420073, Казань, ул. Гвардейская, д. 9а. E-mail: tais@bancorp.ru Урал Екатеринбург, магазин № 14, ул. Челюскинцев, д. 23, тел./факс (3432) 53-24-90. E-mail: gvardia@mail.ur.ru Екатеринбург, «Валео-книга», ул. Ключевская, д. 5, тел. (3432) 42-07-75, факс 42-56-00. E-mail: valeo@etel.ru
Фень Юань Программирование графики для Windows Е^ППТЕР