/
Author: Юань Ф.
Tags: компьютерные технологии программирование программное обеспечение компьютерная графика операционная система windows
ISBN: 5-318-00297-8
Year: 2002
Text
СЕРИЯ
>М>^
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
Е^ППТЕР