феньюань Программирование графики для Windows Перевел с английского Е. Матвеев
зГд7ющиТре™аРкцией
£
Руководитель проекта Литературный редактор
ХУДОЖНИК
Иллюстрации
Верстка
^и.корнеев Е
~
Mamies
-
327
-
А.Жданов
г,
. . .
„
Шендерова
в.листова
Ю. Сергиенко
1
Программирование графики для Windows (+CD). — СПб.: Питер 2002 —
^~т,~-
К ро
API. к м7тогТв~^^^^^^^
.
„
.....
282
Глава 6. Системы координат и преобразования
340
Глава
7. Пикселы
ГЛЭВЗ
8. 9>
ЛИНИИ И КрИВЫв Замкнутые обЛЭСТИ
Глава 10. Основные сведения о растрах иваются
ОТ
В
Н
мГмТп^пя™?' Р "«Р^^М^О^ТПО^^ «« И В\«^Е мах wmj^, 32-разрядные возможности, реализованные только в Windows NT/2000 и новейшие пасший
Глава 11. Нетривиальное использование растров
Z »;Г"Г™с: "Г,;,™0." Г" "Г™9!-в"""" -"""" ~"™ *=-
Глава 12
-=-
=Z=
й
±л
%=аЕ5===5Ь=- == ^=
Глава 13. Палитры
Пюва14
-
шрифты
ГЛЭВЭ 15.
ТвКСТ
© Перевод на русский язык, Е. Матвеев, 2002 © Издательский дом «Питер», 2002
ГЛЭВЭ 16.
МСТЭфаЙЛЫ
Ппя
Права на издание получены по соглашению с Prentice Hall Inc ра а 6
Фо р:е е з ^^^^^1^^^^^ъ^ воспроизведена в какой бь, то ни было а±И^Г2Г^,ГС==:Г3 —' —1Х издательство, как Не
-=-=а-=;ЕЕЕе™= 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 0тпеча ™носдаапозитивоввФГУП«Псчатныйдаор»им.А.М.Горы[ог0 Министерства РФподелам.печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.
380 422 '. ... 479
535 608
- Графические алгоритмы и растры Windows . . . . 663
Original English language Edition Copyright ©by Hewlett-Packard Company 2001
е
~
Глава 5. Абстракция графического устройства
ГЛЭВа
15в№5-зТ8-00297-8
Рассмат
30
Глава 4. Мониторинг графической системы Windows . . . . 240
Юань Фень Ю12
ОСНОВНЫв ПРИНЦИПЫ И ПОНЯТИЯ
Глава 3. Внутренние структуры данных GDI/DirectDraw . . 143
ББК 32 973-0183 УДК681
1.
Глава 2. Архитектура графической системы Windows . . . . 84
н.Биржаков В
к°ррект°р
ГЛЭВЭ
А Ваппьгя
научный редактор
Краткое содержание
Глава 17. Печать
6.
™
801
897
947
Глава 18. DirectDraw и непосредственный ^ ^ ^^
Алфавитный указатель
^
loss
Содержание
Глава 2. Архитектура графической системы Windows
Содержание Благодарности Введение
84
Компоненты графической системы Windows
84
Мультимедиа
21 22
87
Video for Windows
88
Still Image
89
OpenGL
89
Windows Media
91
Компоненты режима ядра
92
Драйверы режима ядра
92
О чем эта книга Как организована эта книга Как читать эту книгу
23 24 27
Что находится на компакт-диске
28
Чтодальше?
29
Функции, экспортируемые из GDI32.DLL
94
От издательства
29
Группы функций GDI
94
Вызовы системных функций GDI
97
От Win32 GDI API к системным функциям механизма GDI
98
Глава 1. Основные принципы и понятия
зо
Основы программирования для Windows на C/C++
31
Hello World, версия!: запуск браузера Hello World, версия 2: вывод текста на рабочем столе
32 34
Hello, World, версия 3: создание полноэкранного окна
35
Hello, World, версия 4: вывод средствами DirectDraw Ассемблер
42 46
Среда программирования
Архитектура GDI
93
. . . 99
Архитектура DirectX
f
Компоненты DirectX
100
Архитектура DirectDraw
102
Архитектура системы печати
105
Клиент спулера Win32
108
50
Служба спулера
Разработка и тестирование Компиляторы
50 52
Провайдер печати
Microsoft Platform SDK
55
Процессор печати
58 59
Языковой монитор и монитор порта
108 108 109 110 112 113 114
Microsoft Driver Development Kit Microsoft Developer Network Формат исполняемых файлов Win32 Каталог импорта Каталог экспорта Архитектура операционной системы Microsoft Windows HAL Микроядро Драйверы устройств Управление окнами и графическая система Исполнительная часть Системные функции Системные процессы Службы Платформенные подсистемы Итоги Примеры программ
61 65
Маршрутизатор спулера
Процесс спулера изнутри Графический механизм Системные функции графического механизма
_
116
69
Механизм графической визуализации
118
71 73
Структуры данных графического механизма
120
Преобразование в примитивы Шрифтовые драйверы
121 122
73 74 76 77 78 79 81 81 82 82
Драйверы экрана Драйвер видеопорта и мини-драйвер видеопорта
123 123
Назначение драйвера экрана
124
Инициализация драйвера экрана Вывод на поверхность, перехват и возврат
124 125
Дополнительные возможности драйвера Поддержка DirectDraw/Direct3D на уровне драйвера экрана
127 127
Драйверы принтеров Управляющие драйверы принтеров от Microsoft
129 130
8
Содержание
Графическая библиотека DLL драйвера принтера Драйвер принтера для вывода документа HTML Итоги Примеры программ
,
131
WinDbg и расширение отладчика GDI
183
134 141 142
Структуры данных режима ядра
195
Глава 3. Внутренние структуры данных GDI/DirectDraw . . . 143 Манипуляторы и объектно-ориентированное программирование
•
144
Класс и объект Инкапсуляция и маскировка реализации
144 145
Указатели и манипуляторы
148
Тождественное отображение
149
Табличное отображение
149
Когда манипулятора недостаточно
150
Расшифровка манипуляторов объектов GDI
151
Манипуляторы стандартных объектов — константы
153
HGDIOBJ не является указателем Максимальное количество манипуляторов GDI на уровне процесса —12 000 Максимальное количество манипуляторов GDI на уровне системы —16 384
153
154
Часть HGDIOBJ содержит индекс
154
Часть HGDIOBJ содержит тип объекта GDI
154
153
Поиск таблицы объектов GDI
156
Расшифровка таблицы объектов GDI
162
Указатель pKernel ссылается на выгружаемый пул
166
Поле nCount иногда используется как счетчик выбора объектов
166
Поле nProcess связывает манипулятор GDI с конкретным процессом nUpper: дополнительная проверка пТуре: внутренний тип объекта pUser: указатель на структуру данных пользовательского режима Структуры данных пользовательского режима Структура данных пользовательского режима для кистей: оптимизация создания однородных кистей Структура данных пользовательского режима для регионов: оптимизация прямоугольных регионов Структура данных пользовательского режима для шрифтов: таблица значений ширины Структура данных пользовательского режима для контекста устройства: атрибуты Обращение к адресному пространству режима ядра
Содержание
. . . . 167 168 169 169 170 170 171 172 172 177
Таблица объектов 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
Примеры программ
Глава 4. Мониторинг графической системы Windows Отслеживание вызовов функций Win32 API
238
240 *. . . 241
Построение программы мониторинга
242
Внедрение DLL-разведчика
243
Подключение к цепочке вызовов функций API
246
Сбор информации
248
Вывод данных
254
Управляющая программа
257
Отслеживание вызовов Win32 GDI
260
Файл определения GDI API
260
Декодер данных GDI Полный мониторинг API Отслеживание СОМ-интерфейсов DirectDraw Таблица виртуальных функций Определение DirectDraw API Модификация таблицы виртуальных функций Отслеживание системных вызовов GDI Отслеживание интерфейса DDI Итоги Примеры программ
262 264 268 268 269 270 271 275 279 280
10
Содержание
Глава 5. Абстракция графического устройства Современные видеоадаптеры
282 «
282
Кадровый буфер Формат пикселов Двойная буферизация, z-буфер и текстуры
283 286 290
Аппаратное ускорение
293
Экранное устройство и перечисление режимов Контекст устройства
293 296
Содержание
11
Режимы отображения MMJ.OENGLISH и MMJHIENGLISH
348
Режимы отображения MMJ.OMETRIC и MM_HIMETRIC Режим отображения MM_TWIPS Режим отображения MMJSOTROPIC
350 351 351
Режим отображения MM_ANISOTROPIC Базовые точки окна и области просмотра
352 355
Другие функции окна и области просмотра Мировая система координат
357 357
298
Аффинные преобразования
358
Получение информации о возможностях устройства
299
Функции мировых преобразований в Win32 API
361
Атрибуты в контексте устройства Связь контекста устройства с окном
304 307
Использование мировых преобразований
363
Графический вывод в многооконной среде
307
Создание контекста устройства
Получение контекста устройства, связанного с окном
309
Общий контекст устройства Классовый контекст устройства
313 313
Закрытый контекст устройства Родительский контекст устройства
314 315
Прочие контексты устройств
315
Информационный контекст устройства Совместимый контекст устройства
315 316
Метафайловый контекст устройства
316
Формальное представление контекста устройства Пример:родовой класс рамочного окна Класс панели инструментов
318 321 322
Класс строки состояния
323
Класс холста
323
Класс рамочного окна Тестовая программа
324 326
Пример программы: графический вывод в контексте устройства Обновляемый регион окна Сообщение WM_PAINT Наглядное представление сообщений перерисовки окна Итоги Примеры программ
Глава 6. Системы координат и преобразования Физическая система координат Система координат устройства Страничная система координат и режимы отображения Режим отображения ММ_ТЕХТ
328 328 329 331 339
Использование систем координат Реализация преобразований в GDI
370 372
Пример программы: прокрутка и масштабирование Игра го в классе KScrollCanvas
373 377
Итоги Примеры программ
378 379
Глава 7. Пикселы Объекты GDI, манипуляторы и таблица объектов Хранение объектов GDI Таблица объектов GDI Манипулятор объекта GDI API объектов GDI Обнаружение утечки объектов GDI Отсечение Конвейер отсечения Простые регионы Регион отсечения Метарегион Пять регионов контекста устройства Наглядное представление регионов в контексте устройства
380 380
382 383 '. . . 384 385 387
. 390 . . . .390 391 392
396 398 398 402
340
Цвет Цветовое пространство RGB Цветовое пространство HLS Индексируемые цвета и палитры Нетривиальные возможности
341 343 345 348
Вывод пикселов, Пример: множество Мандельброта Итоги Примеры программ
415
339
403 406 411 415 418 421
12
Содержание
Глава 8. Линии и кривые Бинарные растровые операции
422 ,
422
Режим заполнения фона и цвет фона
426
Перья Объект логического пера
427 427
Стандартные перья
429
Простые перья Расширенные перья Получение информации о логических перьях
430 433 439
Класс для работы с объектами перьев GDI
440
Линии
442
Кривые Безье
447
Содержание
13
Многоугольники
500
Режим заполнения многоугольников Замкнутые траектории
501 504
Регионы
506
Создание объекта региона Операции с объектами регионов Прорисовка регионов Градиентные заливки
507 510 521 523
Градиентная заливка прямоугольников
525
Применение градиентных заливок для создания объемных кнопок
527
Практическое использование заливок
528
PolyDraw
451
Альтернативное определение кривых Безье
453
Полупрозрачная заливка Реализация градиентных заливок в цветовом пространстве HLS Радиальные градиентные заливки
454
Текстурные и растровые заливки
532
Узорные заливки
532
Дуги Определение дуги в градусах: функция AngleArc
455
Рисование дуг пером со стилем PS_INSIDEFRAME
456
Преобразование дуг в кривые Безье Траектории
457 461
Итоги Пример программы
Глава 10. Основные сведения о растрах
528 529 530
533 534
535"
Построение траектории
461
Аппаратно-независимые растры
Получение информации о траектории
463
Преобразование объекта траектории
467
Графические операции с использованием траекторий Преобразование пути в регион
471 473
Файловый формат BMP Упакованный аппаратно-независимый растр Разделенный аппаратно-независимый растр Класс для работы с DIB
536 545 546 546
473 477
Отображение DIB в контексте устройства StretchDIBits
556 556
Пример: рисование нестандартных стилевых линий Итоги Пример программы
Глава 9. Замкнутые области Кисти Объект логической кисти Стандартные кисти
478
479 479 479 480
Пользовательские кисти
481
Кисти системных цветов
488
Структура LOGBRUSH Прямоугольники Прямоугольник как структура данных Рисование прямоугольников Прорисовка границ и элементов управления Эллипсы, секторы, сегменты и закругленные прямоугольники
489 490 490 492 495 497
« . . . 535
Исходный прямоугольник Приемный прямоугольник и режимы масштабирования
556 557
Преобразование цветового формата Растровая операция
559 559
Пример использования функции StretchDIBits SetDIBitsToDevice
560 561
Совместимые контексты устройств Аппаратно-зависимые растры CreateBitmap CreateBitmapIndirect GetObject и DDB CreateCompatibleBitmap и CreateDiscardableBitmap CreateDIBitmap LoadBitmap
563 564 565 566 567 567 569 570
14
Содержание
Копирование растров между форматами DIB и DDB
571
Прямой доступ к массиву пикселов DDB
575
Использование DDB-растров
576
15
Содержание
Преобразования цветов
672
Преобразование растров в оттенки серого
675
Гамма-коррекция
676
Отображение DDB-растров
576
Использование растров в меню
584
Родовой класс преобразований пикселов
Использование растра в качестве фона окна
589
Родовой класс цветоделения
682
CreateDIBSection
594
Пример выделения каналов
684
Класс для работы с DIB-секциями
596
Функции GetObjectType и GetObject для DIB-секций
598
GetDIBColorTable и SetDIBColorTable
599
Фильтры сглаживания и резкости
691
Применение DIB-секций: аппаратно-независимый вывод
600
Выделение границ и рельеф
692
Применение DIB-секций: вывод в высоком разрешении
603
Морфологические фильтры
693
Итоги Примеры программ
Глава 11. Нетривиальное использование растров Тернарные растровые операции
607 607
608 608
Преобразование пикселов в растрах
678 678
Гистограмма
686
Пространственные фильтры
686
Итоги
695
Примеры программ
696
Глава 13. Палитры
697
Системная палитра
697
609
Параметры экрана
698
Диаграмма тернарных растровых операций
612
Получение системной палитры
699
Часто используемые растровые операции
614
Коды растровых операций
Прозрачные растры
627
Функция PlgBIt
628
Кватернарные растровые операции: MaskBIt
635
Цветовые ключи: TransparentBIt Прозрачность без маски Прозрачный вывод с использованием геометрических фигур
640 644 644
Прозрачный вывод с использованием отсечения
646
Предварительная подготовка изображений
647
Альфа-наложение Пример альфа-наложения с постоянным коэффициентом
649 652
Постепенное проявление и исчезновение растров
653
Прозрачные окна
653
Альфа-канал: класс AirBrush
655
Имитация альфа-наложения Итоги
659
Примеры программ
661 661
Глава 12. Графические алгоритмы и растры Windows . . . . без Прямой доступ к пикселам Аффинные преобразования растров
664 667
Быстрые специализированные преобразования растров
670
Статические цвета
702 '
Логическая палитра
« • • 704
Палитра по умолчанию
705
Полутоновая палитра
706
Создание специализированной палитры
708
Сообщения палитры
710
WM.QUERYNEWPALETTE WM.PALETTEISCHANGING
710 . . . . . '
711
WM_PALETTECHANGED
711
Тестовая программа
712
Палитра и растры Аппаратно-зависимые растры и палитры Аппаратно-независимые растры и палитры
716 717 720
Индекс палитры в цветовой таблице DIB
723
DIB-секций и палитра
725
Квантование цветов Сокращение цветовой глубины растра Итоги Пример программы
Глава 14. Шрифты Что такое шрифт? Наборы символов и кодировки
726 736 742 743
744 745 745
16
Содержание
Глифы
751
Шрифт
753
Семейство шрифтов и начертание Растровые шрифты
754 758
Векторные шрифты
762
• Шрифты TrueType
765
Формат файлов шрифтов TrueType
765
Заголовок шрифта
768
17
Содержание
Простой вывод текста Выравнивание текста Вывод текста справа налево Дополнительные интервалы
833 833 836 839
Ширина символа
841
Нетривиальный вывод текста Преобразование символов в глифы
846 846
Максимальный профиль
769
Кернинг Расположение символов
Отображение символов в индексы глифов
770
Функция ExtTextOut
850
Индексная таблица Данные глифов
772 773
Uniscribe Доступ к данным глифов
854 855
Инструкции глифа
781
Горизонтальные метрики
786
Кернинг
789
Метрики OS/2 и Windows Другие таблицы
790 791
Коллекции TrueType Установка и внедрение шрифтов
792 793
Ресурсные файлы шрифтов
793
Установка открытых шрифтов
794
Установка закрытых шрифтов и шрифтов Multiple Master OpenType Установка шрифтов из образа в памяти Внедрение шрифтов
794 795 795
Системная таблица шрифтов Итоги Примеры программ
Глава 15. Текст Логические шрифты
799 800 800
801 801
847 848
Форматирование текста Вывод текста с табуляцией Простое абзацное форматирование
864 864 866
Аппаратно-независимое форматирование текста
868
Эффекты при выводе текста
871
Цветтекста Начертания Геометрические эффекты Работа с текстом в растровом формате
872 875 877 882
Текст как совокупность кривых
888
Текст как регион
894
Итоги
895
Пример программы
896
Глава 16. Метафайлы
897
Общие сведения о метафайлах Создание расширенного метафайла
897 898
Воспроизведение расширенного метафайла Получение информации о расширенном метафайле Передача расширенных метафайлов Строение расширенных метафайлов
900 903 907 911
*
Метрики шрифтов в Windows Стандартные шрифты
802 804
Создание логических шрифтов Подстановка шрифта
805 810
Записи EMF Классификация типов записей EMF
912 914
Система подстановки шрифтов PANOSE
811
Получение информации о логическом шрифте Метрики растровых и векторных шрифтов Метрики шрифтов TrueType/OpenType Структура LOGFONT и метрики шрифта Точность шрифтовых метрик
817 819 822 827 827
Расшифровка записей EMF Простые объекты GDI в EMF Растры в EMF Регионы в EMF Траектории в EMF Палитры в EMF
916 918 919 921 922 922
18
Содержание
Системы координат в EMF Команды вывода в EMF
„
Аппаратная независимость EMF
924
Растры
993
926
Печать графики в формате JPEG
993
929
Перечисление записей EMF
930
КлассС++для перечисления записей EMF
931
Замедленное воспроизведение EMF
932
Трассировка воспроизведения EMF
933
Динамическое изменение EMF
935
Построение производных метафайлов
937
EMF как средство программирования
941
Декомпилятор EMF
941
Сохранение EMF-файла спулера
943
Итоги
19
Содержание
945
Дополнительная информация
946
Примеры программ
946
Итоги Дополнительная информация
998 999
Примеры программ
999
Глава 18. DirectDraw и непосредственный режим DirectSD Технология СОМ
юоо
• • •
Ю01
СОИ-интерфейсы
1001
СОМ-классы
1002
Создание СОМ-объекта
1004
HRESULT
1004
DirectX и СОМ
1005
Общие сведения о DirectDraw
1007
Интерфейс IDirectDraw?
1008
Интерфейс IDirectDrawSurface?
1010
947
Вывод на поверхности DirectDraw
1014
948
Подбор цветов
1018
Язык управления принтером
949
Интерфейс IDirectDrawClipper
1020
Прямой вывод в порт
952
Глава 17. Печать
947
Знакомство со спулером Процесс печати
Печать с использованием спулера
954
Процессор печати EMF
958
Перечисление принтеров
959
Получение информации о принтере
961
Настройка драйвера принтера
961
Базовая печать средствами GDI
965
Стандартные диалоговые окна печати
965
Создание контекста устройства принтера
971
Получение информации о контексте устройства принтера
973
Последовательность формирования заданий печати
975
Поддержка печати в программах
978
Единая логическая система координат
978
Имитация внешнего вида
страницы
981
Одновременный вывод
страниц
982
Печать нескольких страниц на одном листе Родовой класс печати Вывод в контексте устройства принтера Единицы измерения Текст
983 984 .'
989 989 990
Простое окно DirectDraw
1021
Построение графической библиотеки DirectDraw
1023
Вывод пикселов
1024
Вывод линий
1026
Заливка замкнутых областей
1029
Отсечение
1031
Внеэкранные поверхности
1033
Поддержка прозрачности посредством цветовых ключей
1035
Шрифт и текст
1035
Спрайты
Ю39
Непосредственный режим DirecOD
1043
Подготовка среды непосредственного режима Direct3D
1044
Изменение размеров окна
1047
Двухэтапный вывод
1048
Использование DirectSD в окне Текстурные поверхности
1049 ••
Пример использования непосредственного режима DirectSD Итоги Примеры программ
Алфавитный указатель
1050 1052
. .
1055 1056
Ю58
С любовью и благодарностью посвящаю эту книгу своим родителям... маме и светлой памяти отца, а также жителям моего родного города, восточного сада Сучжоу
Благодарности Эта книга никогда бы не появилась на свет без помощи, поощрения и поддержки многих людей, которым я искренне благодарен. Я хочу поблагодарить редактора 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 на выходных, по вечерам и даже ночью. Наш сын, Чао Чу, тоже старался помочь и каждый вечер перед сном разглядывал экран монитора. Наконец-то у меня появится свободное время и этим летом мы непременно достроим его подводного робота. Фень Юань
О чем эта книга
23
О чем эта книга
Введение Новая книга, посвященная программированию для Windows, принесет пользу лишь в том случае, если будет содержать глубокую, полную, современную, достоверную и практичную информацию. Глубокая книга не останавливается на уровне API, а проникает в архитектурные концепции, внутренние структуры данных и принципы реализации API. Кроме того, она должна предоставить читателю средства для самостоятельных исследований. Полная и современная книга уделяет основное внимание лучшей из существующих на сегодняшний день реализаций Win32 API - Windows 2000, основе будущих операционных систем Microsoft, и описывает ее новые возможности. Достоверная книга базируется на экспериментальных исследованиях Win32 API и внимательной проверке всей информации. Отталкиваться только от документации Microsoft нельзя, поскольку в ней описывается абстрактный интерфейс Win32 API, также зачастую попадается неполная, устаревшая и неточная информация. Практичная книга выходит за рамки простого описания API и тривиальных пояснительных примеров. Она ориентируется на практические задачи; содержит программный код, который может использоваться в реальных программах; предоставляет в распоряжение читателя полезные утилиты и помогает ему в написании профессиональных программ. Как известно, Win32 GDI (и графическое программирование для Windows в целом) является одним из краеугольных камней любой Windows-программы. Этой теме посвящено немало книг, но все сообщество программистов, часто работающих с Windows GDI, определенно нуждается в более глубокой, более полной, более современной, более достоверной и более практичной информации. Именно этими целями автор руководствовался при написании книги.
Книга посвящена графическому программированию для Windows с использованием Win32 GDI API. Кроме того, в ней приведены начальные сведения о DirectDraw и еще более краткое введение в непосредственный режим DirectSD. Рассматриваются стандартные возможности, поддерживаемые на всех платформах 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 и непосредственного режима DirectSD. Прикладной уровень расположен над уровнем 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. В главе 11, «Нетривиальное использование растров», рассматриваются тернарные растровые операции, вывод прозрачных растров, реализация прозрачности без применения масок, альфа-наложение и одна из новых возможностей 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 и непосредственный режим DirectSD», содержит вводный курс программирования для DirectX, ориентированный на опытных программистов GDI. В ней излагаются основы СОМ, приводятся классы среды DirectDraw и поверхностей DirectDraw. Здесь описаны три способа вывода в DirectDraw, объекты отсечения, внеэкранные поверхности и вывод текста в DirectDraw. Кроме того, приведены классы для простейших операций непосредственного режима DirectSD, двойной буферизации, работы с текстурами и окон с поддержкой 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/ windowsZOO/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. На этом сайте также можно найти ответы на часто встречающиеся вопросы, обновления, описания наиболее сложных примеров и т. д.
От издательства Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты
[email protected] (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете на web-сайте издательства http://www.piter.com.
Основы программирования для Windows на C/C++
31
ПРИМЕЧАНИЕ Предполагается, что читатель уже обладает некоторым опытом программирования для Windows, поэтому материал излагается очень кратко.
Основы программирования для Windows на C/C++
Глава 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.
Профессия программиста прошла драматический путь развития от «средневековых» машинных кодов до современных языков программирования — таких, как С, 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).
Глава 1. Основные принципы и понятия
С учетом тенденций перехода на объектно-ориентированное программирование примеры программ в этой книге, написаны в основном на C++, а не на С. Чтобы приведенный код приносил пользу программистам, работающим на С, C++, MFC, ATL, C++ Builder и даже Delphi с Visual Basic, в книге не используются ни экзотические возможности C++, ни специфические средства MFC/ATL.
Hello World, версия 1: запуск браузера Довольно теории — перейдем к написанию несложных Windows-программ на C++. Ниже приведен исходный текст нашей первой программы. //Hellol.cpp #define STRICT finclude <windows.h> finclude
finclude 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. SW_SHOWNORMAL); assert( hRslt > (HINSTANCE) HINSTANCEJRROR); return 0: ПРИМЕЧАНИЕ
Примеры программ на прилагаемом компакт-диске находятся в каталогах, имена которых соответствуют номерам глав — ChaptOl, Chapt02 и т. д. Весь общий код расположен в дополнительном каталоге include на одном уровне с каталогами глав. В каталоге каждой главы находится один файл рабочей области Microsoft Visual C++, содержащий все проекты данной главы. Каждый проект находится в отдельном подкаталоге; например, проект Hello 1 расположен в каталоге Chapt_01\Hellol. В ссылках на общие файлы (например, win.h) в исходном тексте используются относительные пути вида ..\\..\include\win.h.
Перед вами не стандартная программа «Hello, World», ограничивающаяся выдачей текстового сообщения, а новый представитель этого семейства из эпохи Интернета. Если выполнить эту программу, функция Win32 API Shel I Execute запустит браузер и откроет в нем заданную web-страницу. В этой простой программе следует обратить внимание на некоторые особенности, которые редко встречаются в тривиальных примерах, приводимых в других книгах. Автор включил в нее эти аспекты, поскольку они способствуют развитию правильного стиля программирования. Программа начинается с определения макроса STRICT. Это сделано для того, чтобы при включении заголовочных файлов Windows различные типы объектов
Основы программирования для Windows на C/C++
33
интерпретировались по-разному, и компилятору было проще выдавать программисту предупреждения о том, что он путает HANDLE с HINSTANCE или HPEN — с HBRUSH. Когда читатели жалуются, что примеры из некоторых книг даже не компилируются, скорее всего, эти примеры не были протестированы с определением макроса STRICT. Дело в том, что новые версии заголовочных файлов Windows включают STRICT по умолчанию, а старые версии этого не делают. Включение файла обеспечивает возможность компиляции одного исходного текста в двоичный код как с поддержкой Unicode, так и без нее. Программы, предназначенные для операционных систем из семейства Windows 95/98, не рекомендуется компилировать в режиме Unicode, а если это все же делается — программист должен действовать очень внимательно и избегать применения функций API на базе Unicode, не реализованных в Win95/98. Помните, что параметр IpCmd функции WinMain никогда не кодируется в Unicode; для получения TCHAR-версии полной командной строки следует воспользоваться функцией GetCommandLineO. Включение файла относится к области защищенного программирования. Желательно, чтобы программист в пошаговом режиме выполнил каждую строку своей программы и убедился в отсутствии ошибок. Проверка параметров и возвращаемых значений функций директивой assert помогает обнаруживать непредвиденные ситуации на протяжении всей фазы разработки. Существует и другой способ перехвата ошибок программирования — обработка исключений в программе. Два определения массивов const TCHAR гарантируют, что эти строковые константы будут размещены в области данных, доступных только для чтения, окончательной версии двоичного кода, сгенерированной компилятором и компоновщиком. Если включить строки вида _Т( "print") прямо в вызов Shell Execute, скорее всего, они в итоге попадут в область данных, доступных для чтения/записи. Размещение констант в области данных, доступных только для чтения, гарантирует, что эти данные будут только читаться, а при попытке записи в них произойдет общая ошибка защиты (General Protection Fault, GPF). Кроме того, эти данные могут совместно использоваться разными экземплярами программы, что позволяет экономить память при запуске нескольких экземпляров одного модуля в системе. Имя второго параметра функции WinMain (обычно он называется hPrevInstance) при вызове не указывается, поскольку в программах Win32 он не используется. В WinlG параметр 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> fi include #include void Center-Text (HDC hDC. int x. int y. LPCTSTR szFace, LPCTSTR szMessage. int point) HFONT hFont - CreateFont( -point * GetDeviceCapsChDC. LOGPIXELSY) / 72. 0. 0. 0. FWJOLD. TRUE, FALSE. FALSE. ANSI_CHARSET, OUT_TT_PRECIS. CLIP DEFAULT_PRECIS. PROOF_QUALITY. VARIABLE_PITCH, szFace): assert(hFont); HGDIOBJ hold - SelectObjectChDC. hFont): SetTextAlignthDC. TA_CENTER | TA_BASELINE):
Основы программирования для Windows на C/C++
35
SetBkModeChDC, TRANSPARENT); SetTextColor(hDC. 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): Center-Text (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++, сначала регистрирует несколько классов окон, после чего создает главное окно (возможно — несколь-
Глава 1. Основные принципы и понятия
36
ко дочерних окон) и входит в цикл, в котором все поступающие сообщения направляются соответствующим обработчикам. Вероятно, многие читатели хорошо знакомы с подобными примерами простейших Windows-программ. Чтобы не создавать очередной дубликат, мы попробуем написать простую объектно-ориентированную оконную программу на C++, не используя MFC. ' Для этого нам понадобится очень простой класс KWindow, реализующий основные операции по регистрации класса окна, созданию окна и доставке оконных сообщений. Первые две задачи решаются просто, но с третьей дело обстоит сложнее. Конечно, нам хотелось бы оформить функцию обработки сообщений как виртуальную функцию класса KWindow, но Win32 API запрещает использование подобных функций в качестве функции окна. При вызове функций классов C++ передается неявный указатель thi s, а их схемы передачи параметров могут отличаться от той, которая используется функцией окна. Одно из распространенных решений заключается в применении статической функции окна, которая передает запросы соответствующей функции класса C++. Для этого статическая функция окна должна иметь указатель на экземпляр KWindow. В нашем примере эта задача решается передачей указателя на экземпляр KWindow при вызове CreateWi ndowEx и его сохранением в структуре данных, связанной с каждым окном. ПРИМЕЧАНИЕ Имена всех классов C++ в этой книге начинаются с буквы «К» вместо традиционного префикса «С». Это упрощает работу с классами в программах, использующих MFC, ATL или другие библиотеки классов.
Ниже приведен заголовочный файл класса KWindow. // win.h fpragma once class KWindow virtual void OnDraw(HDC hDC)
virtual void OnKeyDowntWPARAM wParam. LPARAM IParam)
virtual LRESULT WndProcCHWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam): static LRESULT CALLBACK WindowProcCHWND hWnd, UINT uMsg. WPARAM wParam. LPARAM IParam); virtual void GetWndClassEx(WNDCLASSEX & we): public: HWND mJiWnd;
Основы программирования для Windows на C/C++
KWindow(void) { mJiWnd }
37
- NULL:
virtual -KWindowCvoid) { }
virtual bool CreateExtDWORD dwExStyle. LPCTSTR IpszClass. LPCTSTR IpszName. DWORD dwStyle. int x, int y, int nWidth. int nHeight, HWND hParent, HMENU hMenu. HINSTANCE hlnst); bool RegisterClass(LPCTSTR IpszClass. HINSTANCE hlnst): virtual WPARAM MessageLoop(void): BOOL ShowWindowtint nCmdShow) const { return ::ShowWindow(m_hWnd. nCmdShow); } BOOL UpdateWindow(void) const { return ::UpdateWindow(m_hWnd);
Класс KWindow содержит всего одну переменную m_hWnd, в которой хранится манипулятор окна. В классе присутствует конструктор, виртуальный деструктор, функция для создания окна, а также функции цикла обработки сообщений, отображения и обновления окон. Закрытые (private) функции класса KWi ndow определяют структуру WNDCLASSEX и обрабатывают сообщения данного окна. Статическая функция WindowProc создается в соответствии с требованиями Win32 API; она передает сообщения виртуальной функции WndProc. Многие функции класса определяются как виртуальные, чтобы их поведение могло быть изменено в классах, производных от KWindow. Например, разные классы будут иметь разные реализации OnDraw, а в их реализации GetWndClassEx будут использоваться разные меню и курсоры. Удобная директива компилятора Visual C++ (#pragma once) помогает избежать многократного включения одного заголовочного файла. Чтобы добиться того же эффекта, можно определить д^щ каждого заголовочного файла уникальный макрос и пропускать заголовочн файл в том случае, если макрос уже определен, Ниже приведена реализац: класса KWindow. // win.cpp tfdefine STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> ^include #include
Глава 1. Основные принципы и понятия
38 finclude ".\win.h" LRESULT KWindow::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM 1 Param) { switch( uMsg ) { case WMJCEYDOWN: OnKeyDown(wParam. IParam): return 0; case WM_PAINT:
{
PAINTSTRUCT ps; BeginPaint(m_hWnd, &ps): OnOraw(ps.hdc); EndPaint(m_hWnd. &ps);
} return 0: case WM_DESTROY: PostQultMessage(O); return 0; return DefWindowProc(hWnd. uMsg. wParam. IParam);
Основы программирования для Windows на C/C++
WNDCLASSEX we; if ( ! GetClassInfoEx(hInst. 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;
KWindow * pWindow;
// Использовать MDICREATESTRUCT для поддержки дочерних окон MDI MDICREATESTRUCT mdic: memset(& mdic, 0, sizeof(mdic)): mdic.IParam - (LPARAM) this: m_hWnd - CreateWindowEx(dwExStyle. IpszClass. IpszName. dwStyle, x. y. nWidth, nHeight. hParent, hMenu, hlnst, &mdic);
if ( uMsg==WM_NCCREATE )
return m hWnd!=NULL;
LRESULT CALLBACK KWindow::WindowProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam)
assertt ! IsBadReadPtr((void *) IParam, sizeof(CREATESTRUCT)) ); MDICREATESTRUCT * pMDIC - (MDICREATESTRUCT *) ((LPCREATESTRUCT) 1Param)->1pCreateParams: pWindow = (KWindow*) (pMDIC->lParam); asserU ! IsBadReadPtrfpWindow, sizeof(KWindow)) ): SetWindowLongthWnd. GWLJJSERDATA. (LONG) pWindow); else pWindow=(KWindow *)GetWindowLong(hWnd. GWL_USERDATA): if ( pWindow ) return pWindow->WndProc(hWnd, uM'sg. wParam. IParam): else return DefWindowProc(hWnd. uMsg. wParam, IParatn);
} bool KWindow::RegisterClass(LPCTSTR IpszClass. HINSTANCE hlnst)
void KWindow::GetWndClassEx(WNDCLASSEX & we) { ,, raemset(& we, 0, sizeof(wc)):
wc.cbSize - sizeof(WNDCLASSEX); we.style = 0: wc.lpfnWndProc = WindowProc; wc.cbClsExtra = 0: wc.cbWndExtra - 0: wc.hlnstance = NULL; wc.hlcon = NULL; wc.hCursor = LoadCursor(NULL. IDC_ARROW): wc.hbrBackground = (HBRUSH)GetStockObject(WHITEJRUSH): wc.lpszMenuName = NULL: - wc.lpszClassName - NULL: ,м'Wc.hlconSm = NULL:
39
Глава 1. Основные принципы и понятия
40 WPARAM KWindow::MessageLoop(void) { MSG msg: while ( GetMessage(&msg. NULL. 0, 0) ) {
TranslateMessage(&msg): DispatchMessage(Smsg);
}
return msg.wParam: }
Реализация KWindow довольно проста, если не считать статической функции WindowProc. Функция WindowProc отвечает за передачу сообщений от операционной системы Windows соответствующим обработчикам класса KWindow. Для этого мы должны иметь возможность получить указатель на экземпляр класса KWindow в функции окна Win32. С другой стороны, указатель передается только при вызове CreateWindowEx. Чтобы значение, передаваемое всего один раз, могло использоваться многократно, мы должны его где-то сохранить. В MFC информация хранится в глобальной карте, связывающей значения HWND с указателями на экземпляры класса CWnd, поэтому каждый раз, когда требуется доставить сообщение, производится хэшированный поиск нужного экземпляра CWnd. В нашей простой реализации класса KWindow было выбрано другое решение — указатель на экземпляр KWindow хранится в структуре данных, поддерживаемой в операционной системе Windows для каждого окна. WindowProc обычно получает указатель на KWindow во время обработки сообщения WM_NCCREATE, которое обычно отправляется перед сообщением WM_CREATE и содержит то же значение указателя на структуру CREATESTRUCT. Указатель сохраняется вызовом SetWindowLong(GWL_USERDATA) и позднее читается вызовом GetWindowLong(GWLJJSERDATA). Так в нашем простом примере организуется связь между WindowProc к KWindow: :WndProc. У традиционных обработчиков сообщений (на базе С) есть существенный недостаток: при необходимости обратиться к дополнительным данным им требуются глобальные данные. При создании нескольких экземпляров окна, использующих общий обработчик сообщений, этот обработчик обычно не работает. Чтобы разные экземпляры окна 'могли использовать один общий класс окна, каждый экземпляр должен иметь собственную копию данных, доступ с которой осуществляется через общий обработчик сообщений. В классе KWindow эта проблема решена: мы создаем обработчик сообщений C++, который получает доступ к данным экземпляров. Функция KWindow: :CreateEx не передает указатель this непосредственно при вызове функции Win32 CreateWi ndowEx; вместо этого указатель передается в поле структуры MDICREATESTRUCT. Это необходимо для поддержки многодокументного интерфейса MDI (Multiple Document Interface) с использованием того же класса KWindow. Чтобы создать дочернее окно MDI, приложение посылает клиентскому окну MDI сообщение WM_MDICREATE и передает ему структуру MDICREATESTRUCT. Именно клиентское окно, реализуемое операционной системой, отвечает за итоговый вызов функции создания окна CreateWindowEx. Также следует учитывать, что функция CreateEx регистрирует класс окна и создает окно за один вызов. Каж-
Основы программирования для Windows на C/C++
41
дый раз, когда требуется создать окно, функция проверяет, не был ли класс зарегистрирован ранее, и регистрирует класс только в случае необходимости. После создания класса KWindow нам уже не придется снова и снова решать задачи регистрации класса, создания окна и организации цикла сообщений - достаточно создать класс, производный от KWindow, и определить в нем только специфические аспекты. Ниже приведена третья версия программы «Hello, World» - вполне обычная программа C++, работающая в оконном режиме. // НеПоЗ.срр #define STRICT fdefine WIN32_LEAN_AND_MEAN ^include <windows.h> finclude finclude finclude "..\..\1nclude\win.h"
const const const const
TCHAR TCHAR TCHAR TCHAR
szMessage[] = JC'Hello, World !"): szFace[] = _T( "Times New Roman"); szHint[] - _T( "Press ESC to quit."): szProgram[] - _T("HelloWorld3") :
// Функция CenterText копируется из Hello2.cpp class KHelloWindow : public KWindow void OnKeyOown(WPARAM wParam, LPARAM IParam) { if ( wParam—VKJSCAPE ) PostMessage(m_hWnd. WM_CLOSE, 0. 0);
void OnDraw(HDC hDC)
{
TextOut(hDC. 0. 0. szHint. Istrlen(szHint)) : CenterText (hDC. GetDeviceCaps(hDC. HORZRES)/2. GetDeviceCaps(hDC. VERTRES)/2. szFace. szMessage. 72);
public:
int WINAPI WinMaintHINSTANCE hlnst. HINSTANCE. LPSTR IpCmd. int nShow) { KHelloWindow win: win.CreateExCO. szProgram. szProgram. WS POPUP. O.~0.
42
Глава 1. Основные принципы и понятия
GetSystemMetrics( SM_CXSCREEN ). GetSystemMetrics( SM_CYSCREEN ). NULL. NULL, hlnst): wi n.ShowWi ndow(nShow); win.UpdateWlndowO;
Основы программирования для Windows на C/C++
LPDIRECTDRAW Ipdd: LPDIRECTDRAWSURFACE Ipddsprimary; void OnKeyDowntWPARAM wParam. LPARAM IParam) { if ( wParam==VK_ESCAPE ) PostMessage(m_hWnd. WM_CLOSE. 0. 0);
return win.MessageLoopO;
}
void Blend(int left, int right, int top. int bottom);
В этой программе класс KHelloUindow создается как производный от класса «Window. Виртуальная функция OnKeyDown переопределяется в нем для обработки клавиши Esc, а виртуальная функция OnDraw переопределяется для обработки сообщения WM_PAINT. Главная программа создает в стеке экземпляр класса KHelleWorld, строит полноэкранное окно, отображает его и входит в обычный цикл обработки сообщений. Где же наше сообщение «Hello, World»? Функция OnDraw выводит его в процессе обработки сообщения WM_PAINT. Итак, мы написали на С++ программу для Windows, в которой нет ни одной глобальной переменной.
Hello, World, версия 4: вывод средствами DirectDraw Вторая и третья версии «Hello, World» напоминают старые DOS-программы, которые обычно захватывали весь экран и записывали данные прямо в видеопамять. Интерфейс DirectDraw, изначально разработанный компанией Microsoft для программирования быстрой графики в играх, позволяет программам работать на еще более низком уровне, обращаясь к экранному буферу и используя нетривиальные возможности современных видеоадаптеров. Ниже приведена простая программа, в которой вывод осуществляется средствами DirectDraw.
void OnDraw(HDC hDC) {
TextOut(hDC. 0. 0. szHint. Istrlen(szHint)): CenterText(hDC. GetSystemMetrics(SM_CXSCREEN) /2. GetSystemMetrics(SM_CYSCREEN)/2. szFace, szMessage. 48); Blend(80. 560. 160. 250);
public: KDDrawWindow(void) Ipdd = NULL; Ipddsprimary - NULL: } -KDDrawWindow(void) if ( Ipddsprimary ) lpddsprimary->Re1ease(): Ipddsprimary = NULL; }
// Hello4.cpp fdefine STRICT
if ( Ipdd )
fdefine WIN32_LEAN_AND_MEAN
#iinclude finclude finclude finclude
<windows.h>
lpdd->Release(): Ipdd = NULL: } }
finclude "..\..\include\win.h"
bool CreateSurface(void); }: •
const const const const
bool KDDrawWindow::CreateSurface(void)
TCHAR TCHAR TCHAR TCHAR
szMessage[] - JVHello. World !"): szFace[] - _T("Times New Roman"); szHint[] - _T("Press ESC to quit."); szProgram[] - _T("HelloWorl'd4");
// Функция CenterText копируется из Hello2.cpp class KDDrawWindow : public KWindow
HRESULT hr; hr - DirectDrawCreate(NULL. &lpdd. NULL): if (hr!-DD_OK) return false;
43
44
Глава 1. Основные принципы и понятия
Основы программирования для Windows на C/C++
45
hr - lpdd->SetCooperativeLeve1(m_hWnd. DDSCL_FULLSCREEN | DOSCLJXCLUSIVE): if (hr!=DD_OK)
return false:
1pddsprimary->Unlock(ddsd.IpSurfасе):
hr = lpdd->SetD1splayMode(640. 480. 32); if (hr!=DD_OK) return false;
int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE. LPSTR IpCmd. int nShow)
DDSURFACEDESC ddsd; memset(& ddsd. 0. sizeof(ddsd)): ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
KDDrawWindow win;
return lpdd->CreateSurface(&ddsd, &lpddsprimary. NULL) ==DD OK: 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: :Blend(int left, int right. int top. int bottom) { DDSURFACEDESC ddsd: memset(&ddsd. 0. sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd): HRESULT hr = lpddsprimary->Lock(NULL, Sddsd. DDLOCKJURFACEMEMORYPTR | DDLOCK_WAIT, NULL): assert(hr==DD_OK):
unsigned char *screen = (unsigned char *) ddsd. IpSurf ace: for (int y=top; y
// Слева // Справа
::Blend(pixel-ddsd.lPitch, pixel); // Сверху ::Blend(pixel+ddsd.lPitch, pixel); // Снизу
win.CreateEx(0. szProgram. szProgram. WS_POPUP. 0. 0. 6etSystemMetrics( SMJXSCREEN ). GetSystemMetrics( SM_CYSCREEN ). NULL. NULL, hlnst): win.CreateSurfaceO; win.ShowWindow(nShow): win.UpdatewindowO; return win.MessageLoopO; В этой простой программе с помощью DirectDraw организуется непосредственная запись в экранный буфер. Вероятно, вы заметили, что мы снова воспользовались классом KWi ndow, не добавив ни единой строки кода для создания окна, простейшей обработки сообщений и организации цикла сообщений. При работе с DirectDraw в каждом экземпляре класса KDDrawWi ndow, производного от KWi ndow, приходится хранить дополнительные данные, а именно указатель на объект DirectDraw и указатель на объект DirectDrawSurface; оба указателя инициализируются при вызове функции CreateSurface. Функция CreateSurface переключает экран в разрешение 640 х 480 с глубиной цвета 32 бит/пиксел и создает одну первичную поверхность DirectDraw. Интерфейсные указатели освобождаются при вызове деструктора. Функция OnDraw выводит небольшое справочное сообщение в левом верхнем углу и большое синее сообщение «Hello, World» в центре экрана; в обоих случаях, как и в предыдущем примере, используются обычные вызовы функций GDI. Впрочем, есть и отличия — после отображения текста вызывается новая функция Blend. В начале своей работы функция KDDrawWindow:: ВТ end фиксирует в памяти экранный буфер и возвращает указатель, по которому можно напрямую работать с памятью экрана. До появления DirectDraw получить прямой доступ к экранному буферу при помощи функций GDI (и даже непосредственно в GDI) было невозможно, поскольку доступ находился под контролем драйверов графических устройств. В нашем примере используется режим с цветовой глубиной 32 бит/пиксел, поэтому каждый пиксел занимает в памяти 4 байта. Адрес пиксела в памяти вычисляется по следующей формуле: pixel - (unsigned char *) ddsd.IpSurfасе + у * ddsd.lPitch + left * 4:s
46
Глава 1. Основные принципы и понятия
функция сканирует прямоугольную область экрана сверху вниз и слева направо и ищет в ней пикселы, цвет которых отличен от белого (фон окрашен в белый цвет). При обнаружении не белого пиксела его цвет размывается по отношению к соседям, расположенным слева, справа, сверху и снизу. В результате размывания пикселу присваивается значение, равное среднему арифметическому значений двух пикселов. На рис. 1.1 изображен результат плавного размывания надписи «Hello, World!» на белом фоне.
Рис. 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 работают с важной информацией уровня программного потока —
Глава 1. Основные принципы и понятия
48
кодом последней ошибки (функции SetLastError/GetLastError), цепочкой обработчиков исключений, локальными данными потока и т. д. На ассемблерном уровне при вызове функций Win32 API используется стандартная схема передачи параметров, то есть параметры заносятся в стек справа налево. Следовательно, вызов функции окна из цикла обработки сообщений ' unsigned rslt = WindowProc(hWnd. uMsg, wParam. IParam):
преобразуется в следующий фрагмент на ассемблере:
mov push mov push mov push mov push call mov
eax. IParam eax eax. wParam eax eax, uMsg eax eax. hWnd eax Wi ndowProc rslt. eax
Из возможностей процессоров Intel Pentium, недоступных на уровне C/C++, следует упомянуть одну инструкцию, которая представляет особый интерес для программистов, занятых оптимизацией своих программ. Речь идет об инструкции RDTSC (Read Time Stamp Counter). Эта инструкция возвращает количество тактов с момента запуска процессора в виде 64-разрядного целого без знака. Число возвращается в паре регистров общего назначения EDX и ЕАХ. Это означает, что на Pentium с частотой 200 МГц выполнение программы можно замерять с точностью до 5 не в течение 117 лет. На данный момент инструкция RDTSC не поддерживается в Visual C++ даже на уровне встроенного ассемблера, хотя оптимизатор, похоже, понимает, что при ее использовании изменяется содержимое регистров EDX и ЕАХ. Чтобы воспользоваться этой инструкцией, следует вставить в программу ее машинное представление OxOF, 0x31. Ниже приведен исходный текст класса-таймера, использующего инструкцию RDTSC.
Ассемблер
49
КТ1mer(void)
m_overhead = 0; StartO: m_overhead = StopО: void Start(void) { m_startcycle = GetCycleCountO: } unsigned int64 Stop(void) { return GetCycleCount()-m_startcycle-m_overhead:
Класс KTimer хранит данные хронометража в виде 64-разрядного числа, поскольку 32-разрядная версия на компьютере с процессором в 200 мегагерц обеспечивает слишком низкую точность. Функция GetCycleCount возвращает текущее количество тактов в виде 64-разрядного числа без знака. Результат, сгенерированный инструкцией RDTSC, соответствует формату 64-разрядного возвращаемого значения функций С. Таким образом, функция GetCycleCount представляет собой одну машинную инструкцию. Функция Start читает количество тактов в начале интервала; функция Stop останавливает хронометраж и возвращает разность. Чтобы повысить точность измерений, необходимо учесть время, потраченное на выполнение функций RDTSC. Для этого конструктор класса KTimer запускает и останавливает таймер. Полученная величина вычитается из результатов последующих измерений. В приведенном ниже примере класс KTimer используется для измерения количества тактов и времени, необходимого для создания однородной кисти. // GDISpeed.cpp fdefine STRICT #define WIN32_LEAN_AND_MEAN
// Timer.h fpragma once
finclude <windows.h> #include
inline unsigned int64 GetCycleCount(void)
finclude "..\..\include\timer.h"
_asm asm
_emit OxOF emit 0x31
KTimer timer; TCHAR mess[128]:
class KTimer unsigned
int64 m_startcycle
publi с: unc-innaH
int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE. LPSTR IpCmd. int nShow)
timer.StartO: SleepClOOO); unsigned cpuspeedlO = (unsigned)(timer.StopO/100000):
int fid m overhead:
timer. StartO:
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 win.32k.sys загружается по адресу ОхаООООООО, драйвер экрана загружается по адресу Oxfbef7000 и т. д. Утилита 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-библиотеки С абсолютно необходимо — из них вы узнаете, как начинается и завершается работа вашего модуля и какие операции выполняются с памятью в системной куче при вызове mal I oc/f гее и new/del ete. Кроме того, вы узнаете, почему в некоторых ситуациях встроенная версия 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.microsoft.com/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 - CreateFile("\\\\.\\Physica1Memory"....):
57
Среда программирования
Но самое интересное в Platform SDK — это, конечно, коллекция программпримеров (если вас не пугает чтение старомодных Windows-программ, написанных на С). Вы не встретите в примерах C++, MFC, ATL или интенсивного применения runtime-функций С. Даже примеры СОМ и DirectX написаны на С, и вместо виртуальных функций C++ в них используются таблицы указателей на функции. Также в этом разделе приведены исходные тексты нескольких утилит SDK, в том числе windiff и spy. Читая эти программы, помните, что они были написаны в начале 90-х годов, причем большинство из них создавалось для WinlG API. В этих примерах часто встречаются места, которые можно покритиковать за плохой стиль программирования — низкий уровень модульности кода, злоупотребление глобальными переменными, недостаточная проверка ошибок и явное влияние на WinlG API. И все же из этих примеров можно вынести немало полезного. В табл. 1.1 перечислены некоторые программы, связанные с тематикой книги. Таблица 1.1. Примеры программ Platform SDK, относящиеся к графике Путь к программе
Краткое описание программы
\graphics\directx
Два мегабайта примеров DirectX
\graphics\gdi\complexscript
Вывод сложных текстов на арабском, тайском и иврите
\graphics\gdi\fonts
Многосторонняя демонстрация шрифтовых функций API
\graphics\gdi\metafile
Загрузка, отображение, редактирование и печать расширенных метафайлов
\graphics\gdi\printer
Функции печати, линии, перья, кисти
\graphics\gdi\setdisp
Динамическое переключение разрешения экрана
\graphics\gdi\showdib
Обработка аппаратно-независимых растров
\graphics\gdi\textfx
Применение эффектов к тексту
\graphics\gdi\wincap32
Сохранение экрана, перехватчики (hooks)
\graphics\gdi\winnt\plgblt
Применение функции PlgBlt
\graphics\gdi\winnt\wxform
Демонстрация мировых преобразований
\graphics\gdi\video\palmap
Преобразование формата видеоданных (DIB)
\sdktools\aniedit
Редактор анимационных указа?елей мыши
\sdktools\fontedit
Редактор растровых шрифтов
\sdktools\image\drwatson
\sdktools\imageedit \sdktools\winnt\walker
Программа DrWatson. Демонстрирует работу с символической таблицей, дизассемблирование, простую отладку, просмотр списка процессов, просмотр стека, создание аварийных дампов и т. д. Простой редактор растровых изображений Просмотр пространства виртуальной памяти процесса Продолжение
Глава 1. Основные принципы и понятия
58 . Продолжение Путь к программе
Краткое описание программы
\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 документируются
Среда программирования
функции Win32 GDI API, а в файле winddi.h — интерфейс между механизмом GDI и драйверами экрана или принтера. В примерах DirectDraw файл ddraw.h документирует функции DirectDraw API, а файл ddrawint.h определяет интерфейс драйвера DirectDraw в Windows NT. Библиотечные файлы делятся по типу сборки на две категории: свободные (free) и проверяемые (checked). В DDK также входят библиотеки импортируемых функций для системных DLL ядра — например, win.32k.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
61
Формат исполняемых файлов 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.
руемых функций. В РЕ-файлах для таких целей резервируется 16 специальных таблиц, называемых каталогами (directories). Чаще всего используются каталоги импорта, связанного импорта (bound import), отложенного импорта (delayed import), экспорта, настройки адресов (relocation), ресурсов и отладочной информации. Объединяем секции и каталоги, добавляем пару заголовков со служебной информацией — и получаем РЕ-файл (рис. 1.4).
IMAGE DOS HEADER Заглушка DOS Сигнатура РЕ-файла IMAGE FILE HEADER
Таблица 1.4. Часто используемые секции РЕ-файлов
Имя
Содержимое
Атрибуты
.text
Исполняемый код
Код, исполнение, чтение
.data
Инициализированные глобальные данные
Инициализированные данные, чтение/запись
.rsrc
Ресурсы
Инициализированные данные, только для чтения
.bss
Неинициализированные глобальные данные
Чтение/запись
.rdata
Неизменные данные
Инициализированные данные, только для чтения
.idata
Каталог импорта
Инициализированные данные, чтение/запись
.edata
Каталог экспорта
Инициализированные данные, только для чтения
.reloc
Таблица настройки адресов
Инициализированные данные, удаляемая (discardable) память, только для чтения
.shared
Общие данные
Инициализированные данные, общая память, чтение/запись
Исполняемый код и глобальные данные в РЕ-файлах практически не структурируются — никто не хочет помогать хакерам взламывать свои программы. Но в остальных данных операционной системы время от времени приходится выполнять поиск. Например, при загрузке модуля загрузчик должен провести поиск в таблице импортируемых функций и настроить значения адресов; когда пользователь вызывает GetProcAddress, поиск производится в таблице экспорти-
63
Формат исполняемых файлов Win32
IMAGE_OPTIONAL_ HEADER R32 Таблица секций IMAGE_SECTION_HEADER fl Секция .text (двоичный код) Секция .data (инициализированные данные) Секция .reloc (таблица настройки адресов) Секция .rsrc (константы) Секция .rdata (ресурсы)
Рис. 1.4. Структура файлов формата Portable Executable
РЕ-файл начинается с заголовка ЕХЕ-файла DOS (структура IMAGE_DOS_HEADER), потому что Microsoft хочет, чтобы программы Win32 можно было запускать в сеансе DOS. Непосредственно за IMAGE_DOS_HEADER следует заглушка (stub) — крошечная DOS-программа, которая генерирует программное прерывание для вывода сообщения об ошибке и завершает работу программы. После заглушки следует настоящий заголовок РЕ-файла (IMAGE_NT_HEADERS). Обратите внимание: длина программы-заглушки не фиксируется, поэтому для определения смещения структуры IMAGE_NT_HEADERS следует использовать значение поля е_1 fanew структуры IMAGE_DOS_HEADER. Структура IMAGE_NT_HEADERS начинается с 4-байтовой сигнатуры, которая должна быть равна IMAGE_NT_SIGNATURE'. 1
Макрос, определяемый в winnt.h. — Примеч. перев.
64
Глава 1. Основные принципы и понятия
В противном случае это может быть файл OS/2 или VxD. Структура IMAGE_FILE_ HEADER содержит идентификатор целевого процессора, количество секций в файле, время сборки, указатель на таблицу символических имен и размер «необязательного» заголовка. Несмотря на свое название, структура IMAGE_OPTIONAL_HEADER не является необязательной (optional). Она встречается в каждом РЕ-файле, поскольку хранящаяся в ней информация слишком важна. В этой структуре хранится рекомендуемый базовый адрес модуля, размеры кода и данных, базовые адреса кода и данных, конфигурация кучи и стека, требования к версии ОС и подсистемы, а также таблица каталогов. РЕ-файл содержит множество адресов для ссылок на функции, переменные, имена, таблицы и т. д. Некоторые из них хранятся в виде виртуальных адресов, которые могут напрямую использоваться после загрузки модуля в память. Если модуль не удается загрузить по рекомендуемому базовому адресу, загрузчик исправляет данные в соответствии с фактическим адресом. Однако большинство адресов задается по отношению к началу заголовка РЕ-файла. Такие адреса называются «относительными виртуальными адресами» (relative virtual addresses, RVA). Обратите внимание: значение RVA не совпадает со смещением в РЕфайле перед его загрузкой в память. Дело в том, что в РЕ-файлах секции обычно выравниваются по 32-разрядным границам, а операционная система использует выравнивание по страницам. Для процессоров Intel размер страницы равен 4096 байт. Адреса RVA вычисляются в предположении, что секции выравниваются по страницам — это уменьшает затраты ресурсов во время выполнения программы. Ниже приведен простой класс C++ для выполнения несложных операций с модулями Win32, загруженными в память. Конструктор показывает, как получить указатели на структуры IMAGE_DOS_HEADER и IMAGE_NT_HEADER. В функции 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 * GetDirectorytint id): PIMAGE_IMPORT_DESCRIPTOR GetImportDescriptor(LPCSTR pDllName):
Формат исполняемых файлов Win32
65
const unsigned * GetFunctionPtr(PIMAGE_IMPORT_DESCRIPTOR plmport. LPCSTR pProcName): FARPROC SetlmportAddresstLPCSTR pDllName. LPCSTR pProcName, FARPROC pNewProc): FARPROC SetExportAddress(LPCSTR pProcName, FARTPROC pNewProc); KPEFile::KPEFile(HMODULE 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(pDOSHeader->e_lfanew). sizeof(IMAGE_NT_HEADERS)) ) pNTHeader = NULL; else pNTHeader - (PIMAGE_NT_HEADERS) RVA2Ptr(pDOSHeader-; e Ifanew):
// Функция возвращает адрес каталога РЕ const void * KPEFile: : GetDi rectory tint id) { return RVA2Ptr(pNTHeader->OptionalHeader.DataDirectory[id]. VirtualAddress):
Получив общее концептуальное представление о файловом формате РЕ, давайте рассмотрим несколько практических примеров.
Каталог импорта При использовании в программе функции Win32 API (например, LoadLibraryW) генерируется двоичный код следующего вида: DWORD _imp_LoadLibrary(a4 = Ox77E971C9: call dword ptr[ imp LoadLibraryW@4]
Обратите внимание на любопытную подробность: компилятор создает внутреннюю глобальную переменную и использует косвенный вызов вместо прямого. Впрочем, для этого у компилятора есть довольно веские причины. Компоновщик не знает точного адреса LoadLibraryWIM на стадии компоновки, хотя он может сделать предположение на основании одной версии kernel32.dll (указан-
66
Глава 1. Основные принципы и понятия
ной в каталоге связанного импорта). Следовательно, в большинстве случаев загрузчик модуля должен найти правильный адрес импортируемой функции и внести исправления в загружаемый образ модуля. Одна и та же функция (такая, как LoadLlbraryW) может вызываться в модуле многократно. По соображениям быстродействия загрузчик предпочел бы вносить исправления в минимальном количестве мест, в идеальном случае — в одном месте на каждую импортируемую функцию. Таким местом является переменная, содержащая адрес импортируемой функции. Обычно подобным переменным присваиваются внутренние имена вида Imp ххх. Адреса импортируемых функций либо выделяются в отдельную секцию (как правило, ей присваивается имя .idata), либо объединяются с секцией . text для экономии места. Каждый модуль обычно импортирует по несколько функций из разных модулей. В РЕ-файле каталог импорта ссылается на массив структур IMAGE_IMPORT_ DESCRIPTOR, каждая из которых соответствует одному импортируемому модулю. Первое поле IMAGE_IMPORT_DESCRIPTOR содержит смещение в таблице хинтов/имен, а последнее поле содержит смещение в таблице импортируемых адресов. Две таблицы имеют одинаковую длину, а каждый элемент соответствует одной импортируемой функции. Элемент таблицы импортируемых адресов содержит порядковый номер, если установлен старший бит (импортирование по порядковому номеру), или смещение 16-разрядного хинта, за которым следует имя импортируемой функции (импортирование по имени). Таким образом, таблица хинтов/имен может использоваться для поиска в каталоге экспорта того модуля, из которого мы импортируем. В исходном РЕ-файле таблица импортируемых адресов может содержать ту же информацию, что и таблица хинтов/имен — то есть смещение хинта, за которым следует имя функции. В этом случае загрузчик находит адрес импортируемой функции и модифицирует элемент таблицы импортируемых адресов. Следовательно, после загрузки РЕ-файла таблица импортируемых адресов в действительности превращается в таблицу адресов импортируемых функций. Компоновщик также может связать модуль с некоторой библиотекой DLL, чтобы таблица инициализировалась адресами импортируемых функций для определенной версии DLL. В последнем случае таблица импортируемых адресов содержит адреса связанных импортируемых функций. В обоих случаях таблица импортируемых функций содержит внутренние переменные вида imp LoadLibrary@4. Давайте попробуем реализовать функцию KPEFile: :SetImportAddress. Эта функция изменяет адрес импортируемой функции в модуле и возвращает первоначальное значение адреса. // Функция возвращает значение поля PIMAGE_IMPORT_DESCRIPTOR // для импортируемого модуля PIMAGE_IMPORT_DESCRIPTOR KPEFile::GetImportDescriptor( LPCSTR pDllName)
{ // Получить IMAGE_IMPORT_DESCRIPTOR PIMAGE_IMPORT_DESCRIPTOR plmport - (PIMAGE_IMPORT_DESCRIPTOR) GetDi rectory(IMAGE_DIRECTORY_ENTRY_IMPORT): if ( plmport—NULL )
формат исполняемых файлов Win32
return NULL: while ( pImport->FirstThunk )
{
if ( stricmptpDllName. RVA2Ptr(pImport->Name))==0 ) return plmport; // Перейти к следующему импортируемому модулю plmport ++;
} return NULL:
// Функция возвращает адрес переменной imp ххх // для импортируемой функции const unsigned * KPEFile::GetFunctionPtr( PIMAGE_IMPORT_DESCRIPTOR plmport, LPCSTR pProcName) { PIMAGE_THUNK_OATA 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.AddressOfData)+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 plmport = GetlmportDescriptor(pDllName); if ( plmport ) { const unsigned * pfn = GetFunctionPtrtpImport. pProcName);
67
68
Глава 1. Основные принципы и понятия If ( IsBadReadPtr(pfn. sizeof(DWORD)) ) return NULL: // Получить исходный адрес функции FARPROC Oldproc = (FARPROC) * pfn: DWORD dwWritten; // Заменить новым адресом функции HackWriteProcessMemory(GetCurrentProcess(). (void*) pfn. & pNewProc. sizeof(DWORD). & dwWritten);
Формат исполняемых файлов Win32
69
pe.SetImportAddress("user32.dll". "MessageBoxA". (FARPROC) MyMessageBoxA); MessageBoxA(NULL. "Test". "SetlmportAddress". MB_OK): Программа заменяет импортируемый адрес MessageBoxA в текущем модуле адресом функции MyMessageBoxA, реализованной нашим приложением, после чего все вызовы MessageBoxA поступают в MyMessageBoxA. В нашем примере эта функция добавляет в текст и заголовок дополнительное слово «intercepted» («перехвачено») и отображает окно сообщения функцией MessageBoxU.
return oldproc: else
Каталог экспорта return NULL;
}
В работе SetlmportAddress используются две вспомогательные функции. Функция GetlmportDescrlptor просматривает каталог импорта и ищет в нем структуру IMAGE_IMPORT_DESCRIPTOR для того модуля, из которого импортируется функция. Структура передается функции 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_PRECOMPOSED. pText. -1. wText. MAX_PATH). wcscat(wText. L" - intercepted"); MultiByteToWideChar(CP_ACP. MB_PRECOMPOSED. pCaption, -1. wCaption. MAX_PATH): wcscattwCaption, L" - intercepted"): return MessageBoxWthWnd. wText. wCaption. uType); int WINAPI WinMain(HINSTANCE hlnstance. HINSTANCE. LPSTR, int) {
KPEFile pe(hlnstance):
Чтобы ваша программа могла импортировать функцию/переменную из системной библиотеке DLL, эта функция/переменная должна быть соответствующим образом экспортирована. Для экспортирования функции/переменной из DLL РЕ-файл должен содержать три объекта данных — порядковый номер, адрес и необязательное имя. Вся информация, относящаяся к экспортируемым функциям, объединяется в структуру IMAGE_EXPORT_DIRECTORY, к которой можно обратиться через каталог экспорта в заголовке РЕ-файла. Хотя экспортироваться могут как функции, так и переменные, обычно экспортируются только функции. По этой причине даже в названиях полей в структурах РЕ-файлов упоминаются только функции. Структура IMAGE_EXPORT_DIRECTORY содержит информацию о количестве экспортируемых функций и количестве имен, которое может быть меньше общего количества функций. Большинство DLL экспортирует функции по имени. В некоторых DLL (например, comctl32.dll) одни функции экспортируются по имени, а другие — по порядковому номеру. Некоторые DLL (например, MFC DLL) экспортируют тысячи функций, поэтому для экономии места, занимаемого именами, все функции экспортируются по порядковому номеру. Библиотеки COM DLL экспортируют фиксированное количество хорошо известных функций (например, DllRegisterServer) с одновременным предоставлением служебных интерфейсов или таблиц виртуальных функций. Некоторые DLL вообще ничего не экспортируют — в них используется только точка входа в DLL. Более интересная информация в IMAGE_EXPORT_DIRECTORY включает RVA таблицы адресов функций, таблицы имен функций и таблицы порядковых номеров функций. Таблица адресов содержит RVA всех экспортируемых функций. Таблица имен содержит RVA строк с именами функций, а таблица порядковых номеров содержит разности между реальным и базовым порядковыми номерами. Зная структуру таблицы экспорта, можно легко реализовать функцию GetProcAddress. Однако такая реализация уже существует в Win32 API (к сожалению, она не имеет Unicode-версии). Вместо этого давайте попробуем реализовать функцию KPEFile: :SetExportAddress. Как было показано выше, функция SetlmportAddress модифицирует таблицу импорта модуля и изменяет адрес одной импортируемой функции в одном моДуле. На другие модули процесса (в том числе и модули, загруженные процессом позднее) эти изменения не распространяются. Функция SetExportAddress
70
Глава 1. Основные принципы и понятия
работает иначе. Она модифицирует таблицу экспорта модуля и поэтому влияет на все экземпляры экспортируемой функции в будущем. Ниже приведен код функции SetExportAddress. FARPROC KPEFile::SetExportAddress(LPCSTR pProcName. FARPROC pNewProc) { PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_D1RECTORY) GetDi rectory(IMAGE_DIRECTORY_ENTRY_EXPORT); if ( pExport==NULL ) return NULL: unsigned ord = 0; if ( (unsigned) pProcName < OxFFFF ) // По порядковому номеру? ord = (unsigned) pProcName; else
{
const DWORD * pNames = (const DWORD *) RVA2Ptr(pExport->AddressOfNames); const WORD * pOrds = (const WORD *) RVA2Ptr(pExport->AddressOfNameOrdinals): // Найти элемент с именем функции for (unsigned i=0; iAddressOfNames: i++) if ( stricmptpProcName. RVA2Ptr(pNames[i]))«0 ) { // Получить соответствующий порядковый номер
ord = pExport->Base + pOrds[i]; break;
if ( (ordBase) || (ord>pExport->NumberOfFunctions) return NULL; // Использовать порядковый номер для получения адреса, // по которому хранится RVA экспортируемой функции DWORD * pRVA - (DWORD *) RVA2Ptr(pExport->AddressOfFunctions) + ord - pExport->Base; // Прочитать исходный адрес функции DWORD rs.1t = * 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 .dll /export. В полученном списке присутствуют такие экспортируемые функции, как HalDisableSystemlnterrupt, HalMakeBeep, HalSitRealTimeClock, READ_PORT_UCHAR, WRITE_PORT_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 принадлежат к микроядру. Имена всех функций, поддерживаемых микроядром, начинаются с префикса «Ке». Например, функция KeAcqui reSpinLock предназначена для получения блокировки, обеспечивающей безопасную работу с общими данными в многопроцессорной системе. Функция Kelm'tializeEvent инициализирует структуру события уровня ядра, которая затем может использоваться функциями 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, вы встретите в нем точки входа графической системы (например, EngBltBlt или PATHOBJ_bMoveTo), но не найдете ни одной точ-
Архитектура операционной системы Microsoft Windows
77
ки входа системы управления окнами. Дело в том, что функции управления окнами никогда не вызываются другими компонентами ядра ОС, а функции графической системы должны вызываться драйверами графических устройств. Библиотеки gdi32.dll и user32.dll обращаются к win.32k.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. Функции диспетчера кэша имеют префикс «Сс». Диспетчер кэша экспортирует такие функции, как CcIsThereDirtyData и CcCopyWrite. Функции компонента структуры процессов (Process Structure) предназначены для создания и завершения системных потоков режима ядра, а также для оповещения процессов/потоков и обработки запросов к ним. Например, диспетчер памяти может воспользоваться функцией PsCreateSystemThread для создания потока ядра, обеспечивающего запись «грязных» (dirty) страниц. Диспетчер объектов (Object Manager) управляет общим поведением объектов, поддерживаемых исполнительной частью. Исполнительная часть обеспечивает создание объектов для каталогов, событий, файлов, символических ссылок, таймеров и др. такими функциями, как ZwCreateDirectoryObject и ZwCreateFile. После того как объект создан, функции ObReferenceObject и ObDereferenceObject диспетчера объектов обновляют счетчик ссылок, функция ObReferenceObjectByHandl e проверяет манипулятор объекта и возвращает указатель на сам объект. Диспетчер ввода-вывода (I/O Manager) транслирует запросы ввода-вывода от программ пользовательского режима или других компонентов режима ядра в правильную последовательность обращений к различным драйверам. Количество функций, поддерживаемых этим компонентом, очень велико. Например, функция loCreateDevice инициализирует объект устройства для его использования драйвером, функция l o C a l l D r i v e r передает пакеты запросов ввода-вывода следующему драйверу более низкого уровня, а функция loGetStackLlmits проверяет границу стека текущего программного потока. Исполнительная часть Windows NT/2000 также поддерживает небольшую runtime-библиотеку, аналогичную runtime-библиотеке С, но имеющую гораздо меньшие размеры. Runtime-библиотека ядра обеспечивает преобразования Unicode, поразрядные операции, операции с памятью и большими числами, обращения к реестру, преобразование времени, строковые операции и т. д. В Windows NT/2000 исполнительная часть и микроядро упакованы в один модуль ntoskrnl.exe, экспортирующий свыше 1000 точек входа. Функции, экспортируемые ntoskrnl.exe, обычно начинаются с двухбуквенного префикса — признака компонента, к которому относится данная функция. Например, префикс «Сс» означает диспетчер кэша, «1о» — диспетчер ввода-вывода, «Ке» — микроядро, «Ob» — диспетчер объектов, «Rtl» — runtime-библиотеку, «Dbg» — поддержку отладки и т. д.
Архитектура операционной системы Microsoft Windows
79
Хотя при вызове используется всего один номер прерывания, нужный номер из более чем 900 системных функций Windows NT/2000 задается в регистре ЕАХ (для процессоров Intel). Программа ntoskrnl.exe поддерживает таблицу системных функций с именем KiServiceTable; в win32k.sys присутствует своя таблица W32pServi ceTabl е. Таблицы системных функций регистрируются вызовом KeAddSystemServiceTable. Когда KiSystemService получает вызов системной функции, она проверяет, допустим ли индекс системной функции и доступны ли ожидаемые параметры, после чего передает вызов обработчику данной системной функции. Рассмотрим примеры системных функций в отладчике Microsoft Visual C++ с использованием отладочных символических файлов Windows 2000. Если проследить за вызовом CreateHalftonePalette в Win32GDI, вы увидите следующий фрагмент:
JtGdi CreateHal ftonePal ette@4: mov eax. 1021И lea edx. [esp+4] int 2Eh ret 4 Пользовательская функция Win32 GetDC реализуется следующим образом: _NtUserGetDC@4: mov e a x . 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 имеется команда ntcal 1, которая позволяет получить информацию о некоторых системных функциях ядра. За дополнительной информацией о системных функциях обращайтесь к статье Марка Руссиновича (Mark Russinovich) «Inside the Native API» на сайте www.sysinternals.com. Системные функции CGI будут более подробно описаны в главе 2.
Системные функции Богатая функциональность, поддерживаемая ядром операционной системы Windows NT/2000, предоставляется модулям пользовательского режима через узкий «шлюз». На процессорах Intel это прерывание Ох2Е. Прерывание обслуживается функцией KiSystemService, которая находится в файле ntoskrnl.exe, но не экспортируется. Поскольку обработчик прерывания работает в режиме ядра, процессор автоматически переключается в привилегированный режим, что делает возможными обращения к адресному пространству ядра.
Системные процессы В операционной системе Windows NT/2000 работает несколько системных процессов, управляющих регистрацией пользователя в системе, службами и пользовательскими процессами. Список системных процессов можно просмотреть в диспетчере задач; также можно воспользоваться утилитой tlist, входящей в поставку Platform SDK.
80
Глава 1. Основные принципы и понятия
Во время работы Windows NT/2000 существует три иерархии процессов. Первая иерархия состоит из единственного системного процесса, идентификатор которого всегда равен 0. Ко второй иерархии относятся все остальные системные процессы. Она начинается с процесса с именем system, который является родительским по отношению к процессу диспетчера сеанса (smss.exe). Процесс диспетчера сеанса является родителем процесса подсистемы Win32 (csrss.exe) и процесса регистрации пользователя в системе (winlogon.exe). Третья иерархия начинается с процесса диспетчера программ (explorer.exe), являющегося родителем всех пользовательских процессов. Дерево процессов Windows 2000, отображаемое командой t l i s t -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 OSA.EXE (744) Reminder IMGICON.EXE (760) Утилита Process Walker (pwalker.exe) выводит дополнительную информацию о каждом процессе. Process Walker показывает, что процесс System Idle Process состоит из одного программного потока с начальным адресом 0. Вполне возможно, что это не реальный процесс, а некий механизм, при помощи которого организуется период пассивного ожидания в системе. Процесс System обладает действительным адресом в адресном пространстве ядра и состоит из десятков потоков с начальными адресами, принадлежащими адресному пространству ядра. Следовательно, процесс System также является родительским для системных потоков режима ядра. Если преобразовать начальные адреса потоков этого процесса в символические имена, вы найдете немало интересных имен типа PhaselInitialization, ExpWorkerThread, ExpWorkerThreadBalanceManager, MiDereferenceSegmentThread, MiModifiedPageWriter, KeBal ancedSetManager, FsRtl Worker-Thread и т. д. Хотя все перечисленные потоки создаются исполнительной частью, потоки ядра могут создаваться и другими компонентами ядра. Но системные процессы 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 Svcs: PolicyAgent. SamSs 360 svchost.exe Svcs: RpcSs 400 spoolsv.exe Svcs: Spooler 436 svchost.exe Svcs: EventSystem, Netman. NtmsSvc. RasMan.SENS. TapiSrv 480 mstask.exe Svcs: Schedule Из этих служб для нас особый интерес представляет спулер (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 режима ядра, win32k.sys, основную часть платформенной подсистемы Win32 вместе со всеми драйверами графических устройств. Библиотеки DLL подсистемы Win32 очень хорошо знакомы всем программистам Windows. Библиотека kernel32.dll управляет виртуальной памятью, вводом-выводом, кучей, процессами, программными потоками и синхронизацией; user32.dll обеспечивает управление окнами и передачу сообщений; gdi32.dll реализует графический вывод и печать; advapi32.dll отвечает за операции с реестром и т. д. Библиотеки DLL подсистемы Win32 обеспечивают прямой доступ к системным функциям ядра ОС и предоставляют полезные дополнительные возможности, не поддерживаемые системными функциями ОС. Примером возможностей Win32 API, не поддерживаемых напрямую в win32k.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\Chart_01\Hellol
Программа «Hello, World» — запуск браузера
Sample\Chart_01\Hello2
Программа «Hello, World» — вывод текста на рабочем столе
Sample\Chart_0 l\Hel Io3
Программа «Hello, World» — простой класс окна
Sample\Chart_01\Hello4
Программа «Hello, World» — размывание текста средствами DirectDraw
Sample\Chart_01\GDISpeed
Использование ассемблера для хронометража
Sample\Chart_01\SetProc
Простой перехват функций API посредством модификации каталогов импорта/экспорта в РЕ-файле
Компоненты графической системы Windows
85
потоки, файлы, ввод-вывод, межпроцессные взаимодействия, безопасность и т. д. О Функции пользовательского интерфейса, обычно называемые пользовательским сервисом, — управление окнами, очереди сообщений, диалоговые окна, элементы управления, стандартные элементы управления, стандартные диалоговые окна, ресурсы, пользовательский ввод, командный интерпретатор и т. д. О Графические и мультимедийные функции — управления цветом, DirectX, GDI, Video for Windows, Still Image, OpenGL, Windows Media и т. д.
Глава 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), процессы,
О Функции 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
Спулер 2 Q О
||
сч о. О О. 0)
Д DС[ С
| |о г
CD
b
8.
W
3. о
MCD о. 01
m
с: с
Вызов системной функции Системные функции Диспетчер ввода-вывода д Видеопорт (AGP)
е»
Видеоминипорт Драйвер (VPE, DxApi, TV) Still Image
Пользова' тельский режим Режим ядра
Графический механизм (DirectDraw, DDML)
Сервер MCD Драйвер экрана (DirectDraw, DirectSD, MCD)
87
висимый интерфейс графического программирования для приложений. При выводе на принтер GDI общается с драйвером принтера, который в Windows 2000 может работать в пользовательском режиме. Работа драйверов принтеров пользовательского режима в значительной степени зависит от функций, поддерживаемых графическим механизмом. Заданиями печати управляет специальный системный процесс — спулер. В его работе используются специализированные компоненты, которые могут модифицироваться производителем оборудования, в том числе процессор печати (print processor), монитор печати (print monitor) и провайдер печати (print provider). DirectX добавляет в эту схему относительно новый набор системных DLL Win32, реализующих СОМ-интерфейсы DirectX. Фактическое взаимодействие с реализацией DirectX в адресном пространстве ядра происходит через GDI. В DirectX входят следующие компоненты: DirectDraw, DirectSound, DirectMusic, Directlnput, DirectPlay, DirectSetup, AutoPlay и DirectSD. В этой книге из всех компонентов DirectX рассматривается только DirectDraw. Ниже GDI и DirectDraw будут описаны существенно более подробно. А пока давайте кратко познакомимся с другими компонентами, которые не войдут в книгу.
Мульти медиа
О ф о. О
a
Компоненты графической системы Windows
Шрифтовой Драйвер Драйвер драйвер Порта Шрифты принтера принтера
Мультимедийная часть 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_OPEN, DRV_ENABLE, DRV_CONFIGURE, DRV_CLOSE и т. д.
HAL
Шина, монитор, камера, сканер, принтер и сетевое оборудование Рис. 2.1. Архитектура графической и мультимедийной системы в Windows 2000
Теперь «пройдемся» по части пользовательского режима по горизонтали. GDI (Graphics Device Interface, интерфейс графических устройств) и ICM (Image Color Management, система управления цветом) обеспечивают аппаратно-неза-
ПРИМЕЧАНИЕ• Чтобы узнать, какие мультимедийные драйверы доступны, откройте файл 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 такой точности не обеспечивает; для чтения счетчика используется обращение к ядру ОС через системную функцию.
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, DrawDlbUpdate и т. д. По своим возможностям этот интерфейс 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. (Кстати, интересно, почему его не назвали Directlmage? Наверное, скоро назовут.) Относительная новизна этого стандарта позволила Microsoft такую роскошь, как реализация STI с использованием СОМ-интерфейсов вместо традиционных функций Win32 API. Microsoft STI состоит из монитора событий, поставляемых производителем оборудования мини-драйверов пользовательского режима, и панели управления сканером или камерой. Монитор событий на системном уровне следит за устройствами ввода статических изображений и их событиями. Кроме того, он ведет список зарегистрированных приложений по обработке статических изображений, которые могут автоматически запускаться при обнаружении события. Мини-драйвер обнаруживает события от конкретного устройства и оповещает о происходящем монитор событий. Кроме того, он передает данные изображения из драйвера режима ядра в пользовательский режим. При помощи панели управления сканером/камерой пользователь ассоциирует устройства ввода статических изображений с приложениями, в которых предусмотрена их поддержка. Приложение панели управления сканером/камерой (sticpl.dll), монитор (stimon.dll, stisvc.exe) и приложения обработки статических изображений — все они используют СОМ-объект STI (CLSID_Sti), реализующий интерфейс ISti 11 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 и gdi32.lib, а также runtime-DLL opengl32.dll и gdi32.dll. Для повышения быстродействия OpenGL реализация позволяет драйверам, предоставленным производителями оборудования, выполнять специализированную оптимизацию и производить прямой доступ к оборудованию. Для удобства работы драйверов OpenGL Microsoft поддерживает архитектуру мини-клиента (MCD). OpenGL.dll загружает mcd32.dll — клиентскую DLL, предоставляемую операционной системой, и необязательный драйвер OpenGL пользовательского режима, предоставляемый производителем оборудования. Чтобы найти свой драйвер OpenGL, проведите в реестре поиск строки OpenGLDrivers. Клиент MCD и драйвер OpenGL пользовательского режима используют функцию GDI ExtEscape для отправки команд графическому механизму и драйверу в режиме ядра. Для поддержки MCD-части необходим драйвер экрана, обеспечивающий оптимизацию OpenGL, с поддержкой сервера MCD уровня ядра в mcdsrv32.dll. В наши дни производители видеоадаптеров довольно часто поддерживают аппаратное ускорение DirectDraw, DirectSD и OpenGL в одном пакете. Всегда интересно видеть, как разные архитектуры (в данном случае GDI и OpenGL) используются для похожих целей. Первоначально GDI проектировался как простой интерфейс графического программирования, ориентированный на стандартное оборудование PC-индустрии того времени — а именно, 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) DirectSD по сравнению с 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, DirectSD и OpenGL вызовы пользовательского режима проходят через библиотеку gdi32.dll, предоставляющую интерфейс к сотням системных функций. Для взаимодействия с драйверами видеопорта и мультимедийными драйверами вызовы пользовательского режима используют обычный интерфейс API файлового ввода-вывода, входящий в базовый сервис Windows. Вызовы системных функций файлового ввода-вывода обрабатываются диспетчером ввода-вывода исполнительной части режима ядра, который обращается к соответствующим драйверам. Вызовы GDI, DirectDraw, DirectSD и OpenGL проходят через графический механизм, который передает их драйверам конкретных устройств. К числу модулей операционной системы относятся ntoskrnl.exe (передача системных функций, диспетчер ввода-вывода), win32k.sys (графический механизм), mcdsvr32.dll (сервер MCD) и hal.dll (HAL). Исполнительная часть ядра Windows NT/2000, ntoskrnl.exe, является самой важной составляющей ядра ОС. В графической системе она в основном отвечает за передачу вызовов функций графической системы графическому механизму, поскольку в последнем используется тот же механизм вызова системных функций, что и другие системные функции. HAL предоставляет в распоряжение драйвера графического устройства средства для таких операций, как чтение и запись аппаратных регистров. Благодаря этому другие компоненты ядра в меньшей степени зависят от платформы. За дополнительными сведениями об исполнительной части и HAL обращайтесь к главе 1.
Драйверы режима ядра Графическая и мультимедийная система Windows NT/2000 работает с конечными устройствами через несколько уровней драйверов, предоставленных производителем оборудования. Самую важную роль играет драйвер экрана, который должен обеспечивать поддержку GDI, DirectDraw, DirectSD и 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 name AbortDoc AbortPath AddFontMemResourceEx AddFontResourceA AddFontResourceExA AddFontResourceExw 6 0001FE4F AddFontResourceTracki ng 7 00020085 AddFontResourceW 8 000264DE AngleArc
ordinal hint RVA О 00027В89 00027У19 0001FEOB 0001CE3D 0001FCCC 00020095
533 214 00028106 WidenPath 534 215 00031B4C XFORMOBJ_bApplyXForm 535 216 OOOOF9FE XFORMOBJJGetXform 536 217 00031A98 XLATEOBJ_cGetPalette 537 218 00031AB4 XLATEOBJJiGetColorTransform 538 219 00031AA6 XLATEOBJJXlate 539 21A 0002B02A XLATEOBJ_piVector
540 541 542 543
21B 21C 21D 21E
000014F9 blnitSystemAndFontDirectonesW 0000143B bMakePathNameW 000015AA cGetTFFromFOT 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, SetPixelFormat и GetPixel Format, описанные в документации OpenGL для Windows. О EUDC. Функции поддержки символов, определяемых пользователем (enduser-defined characters); при помощи этих функций пользователи могут добавлять в шрифты новые символы. Функции EUDC документируются в разделе International Features Platform SDK, в категории Window Base Services. GDI экспортирует такие функции, как EnableEUDC, EudcLoadLinkW и т. д. О Поддержка других системных DLL. Функции, используемые только другими системными DLL Например, user32.dll вызывает функции GDI G d i D l l I n i t i a lize, GdiPrinterThunk, GdiProcessSetup и т. д.; ddraw.dll вызывает GdiEntryl, GdiEntry2 и т. д.; служба спулера spoolsrv.exe вызывает GdiGetSpoolMessage и GdilnitSpool; wow32.dll вызывает GdiQueryTable и GdiCleanCacheDC. О Прочие недокументированные функции. Недокументированные функции, об picпользовании которых ничего не известно, — например, GdiConvertDC, GdiConsvertBitmap, SetRelAbs и т. д. Рисунок 2.2 иллюстрирует наше представление об архитектуре клиентской стороны GDI. Верхний уровень соответствует категориям функций (документированные или недокументированные); под ним находятся сотни функций, разделенные на основные группы. На нижнем уровне расположены вызовы системных функций. Недокументированные или частично документированные функции
Документированные функции Win32 GDI API
со г s
"О CN
W
s
X
со X
(? - 3
CD
С
| i нсо
1
у
•1§. *~
е>s
i. с
CQ
Вызовы системных функций GDI Рис. 2.2. Группы функций GDI
Регионы
5 О.
•о
угольники (user32.
ш 3 ш
'о!
Траектории
есколько монитор
О.
IЯ 1
Q
с
0 Q Ш
ь X
CL о.
s I
3
^
В
X X 5
зжка других систе!
'
;§
S
ТЗ
ка DirectDraw/Dire
о.
VJ
I
файлы (mf3216.dl
а
га
^~,
Линии и кривые
6
2 о.
!аполненные фигу
I
3
Контекст устройст
0)
]инаты и преобра:
яры (msimg32.dll)
s
а
2 g
О (Л
ддержка Open GL
к т
s
о.
о. со g С
g с
¥
си
I
•& Ш 5
I
га
1 tф
I X
s
5^ с
Архитектура GDI
97
Вызовы системных функций GDI По сравнению с DLL разных подсистем Win32 модуль gdi32.dll относительно невелик. В Windows 2000 размер gdi32.dll составляет всего 223 килобайта — меньше, чем comdlg.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 входит функция SymEnumerateSymbol s, которая позволяет вызвать заданную пользователем функцию косвенного вызова (callback function) для каждого символического имени в модуле. Зная символическое имя, можно определить его адрес в образе модуля и прочитать двоичный код, начинающийся с этого адреса. Сравнивая этот код с шаблоном вызова системной функции, можно найти все функции GDI, из которых вызываются системные функции. Программа SysCall делает все, о чем говорится выше, и выводит список всех функций, использующих системные функции DLL подсистемы Win32. Вы можете вывести информацию о вызовах системных функций из user32.dll, ntdll.dll или gdi32.dll. Ниже приведен фрагмент списка из 351 (для Windows 2000) вызова системной функции из gdi32.dll, отсортированного по индексам системных функций. syscalK 0x1000. 1) gdi32.dll!NtGdiAbortDoc syscal1(0x1001. 1) gdi32.d11!NtGdiAbortPath syscall(0x1002. 6) gdi32.dll!NtGdiAddFontResourceW syscall(0x1003. 4) gdi32.dllINtGdiAddRemoteFontToDC syscal1(0x1004. 5) gdi32.dllINtGdiAddFontMemResourceEx syscall(0x1005. 2) gdi32.dll!NtGdiRemoveMergeFont syscall(0x1006. 3) gdi32.dllINtGdiAddRemoteMMInstanceToDC syscall(0x1007. 12) gdi32.dll!NtGdiAlphaBlend syscalH0x1008. 6) gdi32.dll INtGdiAngleArc syscall(0x1125. 11) gd132.dll INtGdiTransparentBlt syscal1(0x1126. 2) gdi32.dllINtGdiUnloadPrinterDriver syscall (0x1128. 1) gd132.dll !NtGdil)nrealizeObject syscal1(0x1129. 1) gdi32.dll!NtGd1UpdateColors syscalI(0xll2a. 1) gdi32.dllINtGdiWidenPath syscall(OxlleS. 3) gdi32.dllINtUserSelectPalette syscall(0x1244. 3) gdi32.dll!NtGdiEngAssociateSurface syscal1(0x1245. 6) gdi32.dll!NtGdiEngCreateBitmap syscal1(0x1246, 4) gdi32.dllINtGdiEngCreateDeviceSurface
98
Глава 2. Архитектура графической системы Windows
syscall(0x1247, 4) gdi32.dllINtGdiEngCreateDeviceBitmap syscal1(0x1248. 6) gd132.dll!NtGd1EngCreatePalette syscall(0x1280, 1) gd132.dll!NtGdiEngCheckAbort syscalК0x1281. 4) gdi32.dll!NtGdiHT_Get8BPPFormatPa1ette syscall(0x1282. 6) gd132.dll!NtGd1HT_Get8BPPMaskPalette syscal1(0x1283. 1) gdi32.dll!NtGdiUpdateTransform 356 total syscalIs found В списке приводится индекс вызываемой системной функции, количество передаваемых параметров, а также имя модуля и функции, из которой производится вызов. Программа SysCall также отображает адреса функции, которые здесь не приводятся для экономии места. Центральной частью программы SysCall является класс KImageModule. Работа этого класса основана на использовании Win32 Image Help API — интерфейса, предназначенного для обработки загружаемых образов исполняемых файлов Win32. Класс загружает и выгружает модули с отладочными символическими файлами, выполняет преобразование между именами и адресами, а также перечисляет символические имена. Список вызовов системных функций реализуется перечислением всех символических имен внутри модуля и проверкой по стандартному шаблону вызова системной функции.
От Win32 GDI API к системным функциям механизма GDI Сравнивая два списка (функций, экспортируемых GDI32, и системных функций, вызываемых из GDI32), нетрудно догадаться или по крайней мере сделать обоснованное предположение относительно того, как функции Win32 GDI отобра. жаются на системные функции win32k.sys. Например, функция печати AbortDoc наверняка вызывает NtGdiAbortDoc, системную функцию с индексом 0x1000; функция поддержки драйверов принтера пользовательского режима, EngBitBlt — это простой псевдоним для NtGdiEngBitBlt, поскольку обе функции имеют одинаковые адреса. Некоторые функции Win32 API существуют в простой версии, которой проще пользоваться, и в расширенной версии с поддержкой дополнительных возможностей. Например, такую пару составляют функции AddFontResource и AddFontResourceEx. Логично предположить, что для этих функций Microsoft не создает двух разных системных вызовов — просто AddFontResource вызывает AddFontResourceEx. Функции, получающие строковые параметры, обычно существуют в Win32 API в двух версиях: имя ANSI-версии заканчивается символом «А», а имя Unicode-версии заканчивается символом «W». Системная функция NT/2000 существует только в Unicode-версии, поскольку базовой кодировкой ОС является именно Unicode. Возможно, вы обратили внимание на то, что трем вызовам AddFontResourceXXX соответствует единственная системная функция, NtGdi AddFontResourceW. Сравнение списка экспортируемых функций GDI со списком системных функций GDI показывает, что некоторые области функциональности GDI реализуются чисто на пользовательском уровне клиента GDI, без промежуточных обращений к механизму GDI. Хорошим примером являются операции с мета-
Архитектура DirectX
99
файлами Windows и расширенными метафайлами, для которых в списке системных вызовов не обнаруживается ни малейшего следа. То же относится и к функциональным возможностям, основанным на использовании EMF — например, функций печати EMF в обход спулера GdiStartDocEMF, GdiStartPageEMF, GdiPlayPageEMF и т. д. В списке также отсутствуют различные функции Win32 API, предназначенные для чтения и записи системных атрибутов — например, GetBkMode, SetTextColor и т. д. Вероятно, ближайшими системными вызовами являются более общие NtGdi GetDCDword и NtGdi GetAndSetDCDword. Как выяснится позднее, некоторые атрибуты контекстов устройств для упрощения доступа хранятся в памяти пользовательского режима, а другие хранятся в структуре данных режима ядра. Подведем итог: DLL подсистемы Win32 gdi32.dll реализует Win32 GDI в основном за счет простого отображения вызовов функций Win32 API в вызовы системных функций, реализуемые графическим механизмом GDI в файле winSZk.sys. Некоторые области (работа с метафайлами и расширенными метафайлами, печать EMF в обход спулера) относятся к числу действительно новых возможностей, обеспечиваемых gdi32.dll без прямой поддержки со стороны механизма GDI. Клиентские библиотеки GDI также обеспечивают реализацию других системных компонентов — DirectDraw, DirectSD, OpenGL, печати и спулинга.
Архитектура DirectX Хотя для большинства прикладных программистов вполне хватало быстродействия и возможностей GDI API, компания Microsoft довольно долго боролась за то, чтобы привлечь на свою сторону и программистов игр. В играх прежде всего нужна быстрая графика, для которой аппаратно-независимые API типа Windows GDI совершенно не приспособлены. Microsoft пыталась внедрить DrawDIB API (часть Video for Windows), WinG (небольшая библиотека, ускоряющая вывод растровых изображений), WinToon (механизм работы с анимированными спрай1 тами), Game SDK и, наконец, остановилась на DirectX . Интерфейс 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, не зависящий от используемого протокола, транспорта или вида сетевых услуг. О DirectSD обеспечивает два уровня API для работы с трехмерной графикой в играх — непосредственный режим (Immediate Mode) и абстрактный режим (Retained Mode). Непосредственный режим DirectSD представляет собой низкоуровневый API трехмерной графики, который идеально подходит для опытных программистов, занимающихся переносом существующих игр и мультимедийных приложений в DirectX. Абстрактный режим DirectSD представляет собой высокоуровневый API, позволяющий легко реализовать приложения с трехмерной графикой; он основан на использовании непосредственного режима DirectSD. DirectSD поддерживает переключаемый буфер глубины, равномерную закраску и закраску Гуро, освещение сцены несколькими разнотипными источниками света, а также работу с материалами и текстурами, трансформациями и отсечением. В настоящее время разработка абстрактного режима DirectSD прекращена, и в будущем ему на смену придет новая технология. О 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, непосредственный и абстрактный режим DirectSD. Функциональность всех перечисленных компонентов предоставляется через набор СОМ-интерфейсов. DirectShow и DirectAnimation строятся поверх этих базовых компонентов DirectX, их работа также зависит от различных фильтров. На верхнем уровне находятся игры, мультимедийные приложения, апплеты Java, web-страницы и т. д. Каждый компонент DirectX представлен одной или несколькими DLL подсистемы Win32 с легко узнаваемыми именами. Например, ddraw.dll и ddrawex.dll реализуют DirectDraw API; d3dim.dll реализует API непосредственного режима DirectSD; d3drm.dll реализует API абстрактного режима DirectSD. В отличие от традиционного интерфейса Win32 API, состоящего из сотен функций, доступ к DirectX API осуществляется через интерфейсы модели СОМ (Component Object Model). COM-интерфейс представляет собой группу семантически связанных функций с заранее определенными типами параметров и возвращаемых значений. В парадигме программирования языка С СОМ-интерфейс может рассматриваться как таблица функций; в мире C++ СОМ-интерфейс является аналогом абстрактного базового класса. СОМ-интерфейсы реализуются СОМ-классами. Но поскольку в идеологии СОМ реализация должна быть четко отделена от интерфейса, клиентские программы могут создавать только экземпляры СОМ-классов (также называемые СОМ-объектами) и выполнять операции с ними через интерфейсы СОМ. После публикации СОМ-интерфейс «замораживается». Это означает, что определение интерфейса изменять нельзя, хотя можно свободно изменять его реализацию. Чтобы предоставить приложению новые возможности, существует только один путь — спроектировать и опубликовать новые интерфейсы. Из-за этого встречаются интерфейсы с именами IDirectDraw, IDirectDraw2 и IDirectDraw?. На рис. 2.3 изображена лишь часть СОМ-интерфейсов, поддерживаемые некоторыми компонентами DirectX. Большинство компонентов DirectX определяет слишком много интерфейсов, которые не поместятся на рисунке.
102
Глава 2. Архитектура графической системы Windows
Клиентские элементы DirectAnimation DirectAnimation (danim.dll) Диспетчер фильтров DirectShow
DirectDraw (ddraw.dll, ddrawex.dll)
DirectSound (dsound.dll)
DirectMusic (dmusic.dll)
| IDirect3DRMMaterial2
? о
| | |
| | | |
"gT о
IDirect3DRM3 IDirectSDRMDeviceS IDirectSDRMLight
J_
Фильтр Фильтр преобразования воспроизведения IDirect3D3 IDirectSDDevice IDirectSDExecuteBuffer IDirectSDLight
g
| |
1 g
IDirectMusic IDirectMusicLoader IDirectMusicCollection IDirectMusicComposer
1
|
Ts Q.
IDirectSound IDirectSoundBuffer IDirectSoundSDBuffer IDirectSoundCapture
(DirectDraw
IDirectDrawSurface IDirectDrawPalette IDirectDrawClipper
Фильтр источника
103
О I DirectDraw — базовый интерфейс DirectDraw, на основе которого могут создаваться другие объекты DirectDraw. Последней версией является I DirectDraw?. Интерфейсы IDirectDraw обеспечивают создание других объектов DirectDraw, управление поверхностями, выбор разрешения и глубины цвета, получение информации о состоянии экрана, выделение памяти и т. д. При вызове DirectDrawCreate создается объект DirectDraw, который поддерживает различные интерфейсы IDirectDraw.
Игры DirectX, мультимедийные приложения, апплеты Java, HTML-страницы и т. д. Подключаемые модули браузера Элементы Media Player
Архитектура DirectX
2 о
НепосредАбстрактный ственный режим DirectSD режим DirectSD (d3dim.dll) (d3dim.dll)
GDI и прочий сервис ОС Рис. 2.3. Основная архитектура DirectX
Архитектура DirectDraw Главной темой этой книги является программирование двумерной графики в Windows — другими словами, GDI и DirectDraw. Информацию об остальных компонентах DirectX можно почерпнуть из документации MSDN, других книг и ресурсов Интернета, а мы перейдем к рассмотрению архитектуры DirectDraw. DirectDraw можно рассматривать как специализированную версию GDI. Первая стадия специализации заключается в том, что вывод направляется только на видеоадаптер, а не на принтер, плоттер или любое другое из существующих графических устройств. Второй стадией является сокращение функциональных возможностей, поддерживаемых GDI. B DirectDraw нет прямой поддержки режимов отображения, мировых преобразований, шрифтов и текста, линий и кривых; работа осуществляется только с растровыми изображениями. Последней стадией является реализация ограниченного подмножества с учетом аппаратного ускорения и добавлением возможностей, имеющих важное значение для игр и мультимедийного программирования. DirectDraw реализует семь основных интерфейсов, два из которых существуют в нескольких версиях.
О Интерфейс IDirectDrawSurface обеспечивает все операции вывода в DirectDraw. Последней версией является IDirectDrawSurface7. В этот интерфейс входят операции с поверхностями — получение информации о возможностях, блокировка и ее снятие, выбор палитры, отсечение и т. д. При блокировке поверхности память видеоадаптера отображается в виртуальное адресное пространство приложения, что позволяет организовать прямой доступ к ней, связать контекст устройства GDI с поверхностью DirectDraw и осуществлять вывод на поверхности средствами GDI. Еще важнее то, что IDirectDrawSurface поддерживает блиттинг между поверхностями и переключение поверхностей с аппаратным ускорением. Чтобы выполнить более сложные операции, вам придется либо реализовать их самостоятельно, либо обратиться к GDI. О Интерфейс IDirectDrawPalette поддерживает создание и непосредственные операции с цветовой палитрой на 256-цветном экране. О Интерфейс IDirectDrawClipper управляет отсечением поверхностей DirectDraw с использованием списков отсечения (clip lists), представленных структурами RGNDATA GDI API. Поскольку DirectDraw не поддерживает создание списков отсечения, в вашем распоряжении остается богатый ассортимент операций с регионами, существующих в GDI. О Интерфейс IDirectDrawColorControl управляет цветом поверхностей и оверлеев за счет регулировки яркости, контраста, оттенка, насыщенности и гаммакоррекции. О Интерфейс IDirectDrawGammaControl управляет процессом гамма-коррекции, в ходе которого значения цветов в кадровом буфере преобразуются в цвета, передаваемые аппаратному цифро-аналоговому преобразователю (DAC, digitalto-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.
8 T
(A 5
p
b
2
Q
t3 Ф
s
h
e
2
с о f>
О
(0
О
га О
o>
TJ 2>
Q
0
Q
5
D. 5
2
0) CL Q.
О 5
n
2
0 CD
0
0
Q TJ
•e о a. о •о го Q ti CD
(D
Q
DirectDraw HEL (ddraw.dll) GDI32 (gdi32.dll)
MOD (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 до GdiEntrylS. В разделе «Компоненты графической системы Windows» упоминалась программа SysCall, предназначенная для вывода списка системных функций, вызываемых в системной DLL (например, gdi32.dll). Если взглянуть на такой список для GDI32, вы обнаружите в нем десятки вызовов системных функций DirectDraw и DirectSD — например, NtGdiDdCreateSurface, NtGdiDSdTextureSwap и NtGdoD3dDrawPrimitives2. Разумеется, в самом интерфейсе GDI эти функции не используются. Возможно единственное объяснение: GDI экспортирует эти функции в интерфейсы DirectDraw/DirectSD для ddraw.dll и других DLL DirectX через недокументированные точки входа. Реализация DirectDraw состоит из нескольких уровней. Верхний уровень поддерживает СОМ-интерфейсы DirectDraw, стандартные экспортируемые функции
Архитектура системы печати
105
СОМ-объектов (DllGetClassObject и т. д.) и специальные функции создания объектов 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, PatBlt, BitBlt и т. д. Следовательно, ddraw использует функции GDI в процессе вывода. Наконец, воспользуйтесь какой-нибудь программой, выводящей списки символических имен в файлах с отладочной информацией, — например, отладчиком Visual C++. Вы найдете в ddraw.dll такие имена, как HELBIt, HELInitializeSpecialCases и generalAlphaBlt. В списке также встречается немало имен с префиксом «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, способный выводить прямые, кривые, растровые изображения и текст на любом графическом устройстве, для которого имеется соответствующий драйвер. Однако принтеры составляют особый класс графических устройств и заслуживают
особого внимания. Ниже перечислены важнейшие отличия принтеров от других графических устройств. О Пользователи обычно печатают не одну страницу, а целый документ, задавая при этом специальные параметры — качество печати, размер бумаги, режим двусторонней печати, количество копий и т. д. GDI содержит специальный принтерный API для постраничной печати, а также структуру DEVMODE для определения всех параметров печати. Такие аспекты, как разбиение документа на страницы и выбор размеров полей, находятся под контролем приложения. О Принтер обычно обладает гораздо большим разрешением (от 300 до 2400 dpi), чем экран монитора (от 75 до 120 dpi). Это приводит к увеличению объема обрабатываемых данных и возможной нехватке памяти для одновременного воспроизведения всей страницы. Механизм GDI позволяет драйверу принтера принимать данные небольшими частями (полосами) посредством спулинra 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
Локальная система WinNT/2000 System Приложение
Клиент спулера (winspool.drv)
Глава 2. Архитектура графической системы Windows
GDI (gdi32.dll)
106
Интерфейсная DLL драйвера печати
Е М F RPC
/
;
Служба спулера (spoolsv.dll)
Е1 I М
1 / Маршрутизатор спулера (spoolsv.dll) Локальный провайдер печати (localspl.dll)
Маршрутизатор спулера (spoolsv.dll)
Провайдер печати для сетей Windows (win32spl.dll)
Служба спулера (spoolsv.dll)
1/
Процессор печати (localspl.dll)
GDI (gd 32.dll) Драйвер принтера пользовательского режима Языковой монитор
Интерфейсная DLL драйвера печати
Монитор порта
Пользовательский режим
Файловый ввод-вывод
Kernel Mode Системные функции Графический механизм Шрифтовой драйвер Шрифты
Драйвер принтера режима ядра
Диспетчер ввода/вывода Сетевые драйверы
Драйверы порта принтера
Рис. 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 приведен исходный текст примерного провайдера печати. Главная точка входа провайдера называется InitializePrintProvider. Доступ к другим точкам входа осуществляется через таблицу функций, возвращаемую InitializePrintProvider.
110
Глава 2. Архитектура графической системы Windows
Локальный провайдер печати должен реализовать полный набор функций провайдера печати, включая отмену спулинга заданий печати^ обращения к интерфейсной DLL драйвера принтера и вызов процессоров печати для обработки задания.
Процессор печати Процессор печати отвечает за преобразование спулерных файлов задания печати в данные низкоуровневого формата, которые могут передаваться на принтер. Кроме того, они вызываются для выполнения управляющих операций с заданиями печати — для приостановки, возобновления и отмены запросов. Процессор печати вызывается локальным провайдером печати. Спулерные файлы заданий печати в Windows NT/2000 обычно хранятся в формате EMF. GDI помогает преобразовать заявку приложения на выполнения графических операций в формат EMF и быстро записать этот файл на диск, чтобы приложение могло возобновить свою нормальную работу. Спулерные файлы обычно хранятся в каталоге $SystemRoot$\spool\printers. Для заданий печати в формате EMF спулер генерирует два файла. Файл с расширением .shd содержит параметры задания — имя принтера, имя документа, имя порта, а также копию структуры DEVMODE. Файл с расширением .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\ spool\prtprocs\w32x86, написать маленькую программу с вызовом AddPrintProcessor для его установки, а затем повозиться с собственным процессором печати в отладчике. Windows NT 4.0 DDK содержит более полный пример процессора печати с поддержкой форматов EMF, RAW и TEXT. Главными точками входа процессора печати являются функции OpenPrijitProcessor, PrintDocumentOnPrinterProcessor и ClosePrintProcessor. Функция OpenPrintProcessor инициализирует процессор печати для приема задания; PrintDocumentOnPrinterProcessor обрабатывает задание печати, 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 се loControl. Второй важной функцией языкового монитора является вставка команд управления принтером в поток данных печати. Монитор порта работает на более низком уровне, чем языковой монитор; он обеспечивает канал взаимодействия между спулером и драйверами порта режима ядра, которые фактически обращаются к аппаратным портам ввода-вывода, к которым подключаются принтеры. Монитор порта как 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 модулей. Компоненты драйверов принтеров, предоставленные производителем оборудования, обычно загружаются во время печати и выгружаются после ее завершения.
tcpmon.dll usbrnon.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
DAWINNT5Q\system32\tcpmon.dll D AWI NN Т 50\sy stem32\usbmon. dll D AWI N N Т 5Q\sy stem32\msf axmon. dll D AWI NN Т 50\sy stern32\spool'\prtprocs\w32x86\sfrnpspr... DAWINNT5Q\system32Vnr20.dll D AWI NN Т 50\sy stem32\winrnr. dll D AWI N N Т 50\sy stem32\n wpro vau. dll DAWINNT50Ssystem32Smpr.dll D AWI N N Т 50\sy stem32S win32spl. dll D AWI NN Т 50\sy stem32\clbcatq. dll D AWI NN Т 50Ssystem32\oleaut32. dll D AWI N N Т 50\sy stem32\inetpp. dll D AWI N N Т 50\system32Mcmp. dll D AWI NN Т 50\system32\spool\driver s\w32x86\3SU NID... D AWI NN Т 50\system32Sspool\dr i veisS w32x86\3\U NID... D AWI NN Т 50\system32\rnscms. dll •'"• '
;
V.'S, ; ' " ' ? » '4 -
,'••",
'
,-, "'
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55'
|
Рис. 2.6. Модули, загруженные процессом службы спулера
Графический механизм В разделе «Компоненты графической системы Windows» была приведена диаграмма с архитектурой графической системы Windows NT/2000. На этой диаграмме имеется большой блок с надписью «Графический механизм». В предыдущих обсуждениях GDI, DirectDraw и Direct3D говорилось о том, что все они вызывают системные функции gdi32.dll, обрабатываемые графическим механизмом. Давайте поближе познакомимся с графическим механизмом Windows NT/2000 — опорой GDI, шлюзом к драйверам графических устройств и вспомогательной поддержкой для работы этих драйверов. Графический механизм Windows NT/2000 «скрыт» в DLL режима ядра, в которой также реализована функциональность управления окнами — то есть в win32k.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. Библиотека win32k.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, DirectSD и 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
чтобы графический механизм разбил команды на более мелкие и даже попросить у механизма визуализации помочь с выводом.
Графический механизм Таблица системных функций графического механизма
ЧЗН
*ll s|& ali ccga
О DirecrSD
Q.Q.P
DirectDraw
H I ro s <»
Плуоновые операции
2 w
Q
Эмуляция вещественных вычислений
Ч
o.m о m
Драйвер растровых шрифтов
SCQ 5|«
Механизм визуализации
GDI API Layer m о £& Ек
Шрифтовой драйвер Шрифты
Мини-драйвер видеопорта (VPE, DxApi, TV)
Обнаруживается довольно интересное имя, _W32pServiceTab1e — это начальный адрес таблицы указателей на все обработчики системных функций win32k.sys. Программа SysCall (см. раздел «Архитектура GDI») позволяет перечислить элементы таблицы, преобразовать их в символические имена и вывести в окне. Запустите программу SysCall и выберите команду View > System Call Tables > Win.32k.sys system call table — появляется список из 639 (в Windows 2000) обработчиков системных функций графического механизма и системы управления окнами (рис. 2.8).
о. 0) О
ау
сЗ5
Драйверы производителей оборудования Драйвер принтера
117
Графический механизм
Драйвер экрана (DirectDraw, DirectSD, MOD)
Рис. 2.7. Архитектура графического механизма Windows 2000 Ниже описаны отдельные компоненты графического механизма Windows 2000.
Системные функции графического механизма Как упоминалось в разделе «Архитектура GDI», gdi32.dll содержит сотни графических функций, используемых библиотеками компонентов подсистемы Win32 (а именно, GDI, DirectDraw, DirectSD и 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
D:\UINNT50\System32\win32k.sys loaded D:\ttINHT50\symbols\sys\win3 2k.dbg loaded. syscall(lOOO) syscall(lOOl) syscall(1002) syscall(1003) syscall(1004) syscall(1005) syscall(1006) syscall(1007) syscall(1008) syscall(1009) syscall(lOOa) syscall(lOOb) syscall(lOOc) syscall(lOOd) syscall(lOOe) syscall(100f) syscall(lOlO) syscall(lOll) syscall(1012)
NtGdiAbortDoc NtGdiAbortPath H t Gd i AddFon t Resourced NtGdiAddRemoteFontToDC HtGdiAddFontMemResourceEx NtGdiRemoveMergeFont H t Gd i AddRemo t eMMIns t anceToDC NtGdiAlphaBlend NtGdiAngleArc HtGdiAnyLinkedFonts NtGdiFontIsLinked NtGdiArcInternal NtGdiBeginPath HtGdiBitBlt NtGdiCancelDC HtGdiCheckBi tmapBi ts HtGdiCloseFigure NtGdiColorCorrectPalette N t Gd i Comb i neRgn
Рис. 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 (инициирующих прерывание Ох2Е) список обработчиков системных функций графического механизма (то есть фрагментов, обслуживающих прерывание Ох2Е для разных индексов функции) выглядит знакомо — разве что для win32k.sys этот список упорядочен по индексу системной функции и заполнен. Для парных функций из gdi32.dll и win32k.sys Microsoft использует одинаковые имена. Например, функция NtGdiAbortDoc с индексом 0x1000 присутствует в обеих таблицах. Имена функций графического механизма за редкими исключениями начинаются с NtGdi, а имена функций системы управления окнами — с NtUser. Как правило, для каждой системной функции графического механизма удается легко найти прототип среди функций GDI, DirectDraw, DirectSD, OpenGL или функций поддержки драйвера принтера. В остальных случаях системные функции могут предназначаться только для внутреннего использования. Примеры: О системная функция NtGdiAbortDoc, конечно, реализует функцию GDI AbortDoc; О системная функция NtGdi DdBlt имеет отношение к интерфейсу DirectDraw IDi rectDrawSurfасе; О системные функции NtGdiDoBanding и NtGdiGetPerBandlnfo используются при печати страниц по полосам; О системные функции NtGdiCreateClientObj и 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/DirectSD, или при использовании особых аппаратных конфигураций. В результате уменьшается сложность обычных драйверов графических устройств, повышается стабильность операционной системы и ускоряется разработка продуктов как производителями оборудования, так и самой компанией 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), расширенных и мировых преобразований. Максимальный размер DIB27 27 поверхности равен 2 х 2 пикселам, то есть 1,42 км х 1,42 км при разрешении 2400 dpi. О Поверхности. GRE обеспечивает полный контроль лишь для одного типа поверхностей — растров, управляемых GDI (GDI-managed bitmaps). Драйверы устройств могут создавать поверхности, управляемые устройствами (devicemanaged), в формате DIB или других форматах при помощи функции ЕпдCreateDeviceSurface. Затем драйвер управляет графическим выводом на такие поверхности. Примером поверхности Win32, управляемой устройством и не относящейся к формату DIB, являются аппаратно-зависимые растры (devicedependent bitmap). О Перехват и возврат вызовов. По умолчанию GRE производит весь вывод на поверхностях DIB, управляемых GDI. Однако драйвер устройства может перехватывать вызовы некоторых графических функций, чтобы реализовать их по-своему. Флаг Л Hook функции EngAssociateSurface определяет функции, перехватываемые драйвером. Например, драйвер может предоставить собственную функцию DrvBitBlt, для чего при вызове EngAssociateSurface передается флаг HOOK_BITBLT. В результате запросы на выполнение блиттинга будут передаваться DrvBitBlt. Однако при вызове DrvBitBlt может определить, что операция^слишком сложна. В этом случае драйвер возвращает запрос GDI, вызывая EngBitBlt.
122
Глава 2. Архитектура графической системы Windows
О При выводе косметических линий и кривых функция DrvStrokePath должна поддерживать сплошные и стилевые косметические линии с закраской однородной кистью и отсечением. В реализации DrvStrokePath драйвер может вызывать служебные функции объектов PATHOBJ и CLIPOBJ для разбиения параметров до линий толщиной в 1 пиксел и прямоугольников отсечения. Если траектория или область отсечения окажется слишком сложной, драйвер может переадресовать вызов графическому механизму, который разбивает вызов до линий толщиной в 1 пиксел с заранее вычисленным отсечением. Для разбиения стилевых линий и кривых Безье GDI аппроксимирует их отрезками прямых линий. О Геометрические линии обладают атрибутами толщины, стилем соединения (join-style) и завершением (end-cap). Если драйвер устройства не справляется с выводом такой линии, он преобразует вызов функции в более простые вызовы DrvFillPath или DrvPaint. В этом случае операция вывода линии преобразуется в заливку области. О Для заливки областей драйвер должен поддерживать DrvPaint. Реализация DrvPaint может воспользоваться служебными функциями CLIPOBJ для разбиения сложной области отсечения на совокупность прямоугольников отсечения. О Для функций блиттинга драйвер должен поддерживать функцию 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/DirectSD/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. После получения таблицы функций графический механизм обычно вызывает DrvEnabl ePDEV, чтобы драйвер создал экземпляр физического устройства и вернул важную информацию о графическом оборудовании и драйвере. При вызове DrvEnablePDEV графический механизм передает драйверу копию структуры DEVMODEW, описывающей характеристики графического устройства. Для видеоадаптеров DEVMODEW определяет частоту развертки, разрешение и особые режимы (например, вывод в оттенках серого или чересстрочный (interlaced) вывод). Для принтеров DEVMODEW содержит еще более важную информацию о типе и размере бумаги, ориентации, качестве печати, количестве копий и способе подачи.
Драйверы экрана
125
DrvEnablePDEV создает экземпляр структуры PDEV (Physical DEVice), определяемой драйвером и содержащей закрытые данные драйвера. Функция возвращает манипулятор структуры PDEV, по которому механизм GDI ссылается на данный экземпляр физического устройства при последующих вызовах. Кроме того, DrvEnablePDEV заполняет структуру GDI INFO, из которой GDI получает информацию о разрешении устройства, физическом размере, цветовом формате, битах DAC, коэффициенте вертикального сжатия, размере палитры, порядке цветовых плоскостей, размере и формате полутонового узора, частоте обновления и т. д. Информация также возвращается в структуре DEVINFO, описывающей графические возможности драйвера, шрифты по умолчанию, количество шрифтов устройства и формат смешивания цветов. Флаги графических возможностей сообщают графическому механизму, поддерживает ли устройство обработку кривых Безье, геометрическое расширение, типы заполнения многоугольников (ALTERNATE или WINDING), печать EMF, сглаживание текста, аппаратную растеризацию шрифтов, JPEG, загрузку гамма-таблиц, аппаратную поддержку альфа-курсора и т. д. После вызова DrvEnablePDEV графический механизм производит собственную внутреннюю инициализацию физического устройства и в завершение вызывает DrvCompl eteDEV, сообщая тем самым, что физическое устройство готово к работе. При завершении использования 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
драйверы экрана
127
Дополнительные возможности драйвера
Таблица 2.1. Перехватываемые графические операции Windows 2000 Индекс
Функция графического механизма
Функция драйвера
HOOKJITBLT
EngBitBlt
DrvBitBlt
HOOK_STRETCHBLT
EngStretchBlt
Помимо инициализации/завершения и перехвата графических вызовов DDI, драйвер экрана также должен представить графическому механизму точки входа для выполнения следующих операций:
DrvStretchBlt
О управление растрами устройств: DrvEnableDeviceBitmap и DrvDisableDeviceBitmap;
HOOK_PLGBLT
EngPlgBH
DrvPlgBlt
О управление палитрой: DrvSetPalette;
HOOKJEXTOUT
EngTextOut
DrvTextOut
HOOK_PAINT
EngPalnt
DrvPaint
О реализация кистей, смешение цветов (dithering) и поддержка ICM (Image Color Management): DrvRealizeBrush, DrvDitherColor, DrvIcmCreateColorTransform, DrvIcmCheckBitmapsBits и т. д.;
HOOK_STROKEPATH
EngStrokePath
DrvStrokePath
HOOKJILLPATH
EngFillPath
OrvFillPath
HOOK_STROKEANDFILLPATH
EngStrokeAndFillPath
DrvStrokeAndFil1 Path
HOOK_LINETO
EngLineTo
DrvLineTo
HOOK_COPYBITS
EngCopyBits
DrvCopyBits
HOOK_MOVEPANNING
EngMovePanm'ng
DrvMovePannlng
HOOK_SYNCHRONIZE
EngSynchronize
DrvSynchronize
HOOKJTRETCHBLTROP
EngStretchBltROP
DrvStretchBHROP
HOOKJYNCHRONIZEACCESS
EngSynchroni zeAccess
DrvSynchronizeAccess
HOOK_TRANSPARENTBLT
EngTransparentBIt
DrvTransparentBlt
HOOK_ALPHABLEND
EngAlphaBlend
DrvAlphaBlend
HOOK GRADIENTFILL
EngGradientFill
DrvGradientFill
О Если поверхность управляется устройством, драйвер может перехватывать несколько обязательных примитивов и предоставить графическому механизму разбивку всех остальных вызовов на примитивы. Драйвер также может перехватывать все графические вызовы для применения аппаратного ускорения или оптимизированной программной реализации. Скажем, пример драйвера sSvirge из Windows 2000 DDK обеспечивает оптимизированную ассемблерную реализацию для некоторых видов блиттинга между экранным буфером и внеэкранными DIB. В этом случае вызовы DrvBitBlt должны перехватываться. О Функция-перехватчик, реализуемая драйвером, может вызывать системные функции графического механизма для разбиения сложной области отсечения на более простые группы прямоугольников. Драйвер также может возвращать вызовы GRE, когда он не справляется с поставленной задачей. Например, упоминавшийся выше драйвер sSvirge для выполнения блиттинга между двумя внеэкранными DIB возвращает вызов графическому механизму.
О обходные обращения к GDI: DrvEscape, DrvDrawEscape (например, для сквозной передачи данных PostScript); О управление мышью: DrvSetPointerShape, DrvMovePointer; О получение информации о шрифтах и поддержка шрифтовых драйверов: DrvQueryFont, DrvQueryFontTree, DrvQueryFontData, DrvQueryFontFi 1 e, DrvQueryTrueTypeFontTabl e и т. д.; О печать: DrvQuerySpoolType, DrvStartDoc, DrvEndDoc, DrvStartPage, DrvEndPage и т. д. (печать подробно рассматривается в главе 3); О поддержка OpenGL: DrvSetPixelFormat, DrvSwapBuffers и т. д.; О поддержка DirectDraw/DirectSD: DrvEnableDirectDraw, DrvGetDirectDrawInfo-и DrvDi sableDi rectDraw.
Многие из перечисленных функций являются необязательными и реализуются драйвером графического устройства лишь при наличии у устройства соответствующих аппаратных возможностей.
Поддержка DirectDraw/DirectSD на уровне драйвера экрана Если драйвер экрана поддерживает DirectDraw/DirectSD, он должен экспортировать точку входа DrvGetDirectDrawInfo, через которую начинается взаимодействие графического механизма с аппаратной поддержкой DirectDraw. Когда приложение DirectDraw/DirectSD создает экземпляр объекта DirectDraw, графический механизм сначала вызывает DrvGetDirectDrawInfo, чтобы получить от драйвера экрана информацию о поддержке DirectDraw. DrvGetDirectDrawInfo возвращает GDI сведения о поддержке DirectDraw/DirectSD, включая описание аппаратных возможностей и список поддерживаемых форматов. Информация об аппаратных возможностях (аппаратный блиттинг, масштабирование, поддержка альфа-канала, отсечение, цветовые ключи, оверлеи, палитры, поддержка' DirectSD и т. д.) кодируется в структуре DDJHALINFO. Информация о форматах видеопамяти возвращается в виде массива структур VIDEOMEMORYINFO. Каждый формат кодируется 32-разрядным значением FOURCC, которое служит для обозначения типа носителя в мультимедийном API. DirectDraw использует FOURCC для описания форматов пикселов, поддерживаемых видеоадаптерами, и форматов пикселов в сжатых текстурах.
128
Глава 2. Архитектура графической системы Windows
Затем графический механизм вызывает функцию DrvEnableDirectDraw, тем самым приказывая драйверу включить аппаратную поддержкуВ1гес1Вгаш. Функция DrvEnableDirectDraw заполняет три структуры адресами функций косвенного вызова для интерфейсов IDirectDraw, IDirectDrawSurface и IDirectDrawPalette. Нечто похожее происходит в главной точке входа драйвера экрана, DrvEnableDriver, возвращающей индексированный список функций косвенного вызова для реализации DDL Каждая из трех структур данных, возвращаемых DrvEnableDirectDraw, соответствует одному из основных компонентов интерфейса DirectDraw (см. описание DirectDraw API). В структуру входит поле флагов, определяющее поддерживаемые функции косвенного вызова, и указатели на все функции косвенного вызова данного интерфейса. Например, структура DD_CALLBACKS относится к реализации DirectDraw и содержит информацию о девяти функциях косвенного вызова. Если в поле dwFlags присутствует флаг DDHAL_CB32_CREATESURFACE, то поле CreateSurface содержит адрес функции косвенного вызова, которой обычно присваивается имя DdCreateSurface. В действительности интерфейс IDirectDraw содержит более девяти методов. Часть методов реализуется внутри клиентской DLL DirectDraw Win32, а остальные функции уровня драйвера возвращаются в других структурах (например, DD_NTCALLBACKS). Поддержка DirectSD со стороны драйвера экрана обозначается несколькими флагами в структуре DD_HALINFO, возвращаемой DrvGetDirectDrawInfo. Флаг DDCAPSJ3D в поле ddCaps.dwCaps означает, что обслуживаемое драйвером устройство поддерживает ускорение трехмерной графики. Флаги поля ddCaps.ddsCaps (например, DDSCAPSJDDEVICE, DDSCAPSJEXTURE и DDSCAPSJBUFFER) описывают трехмерные возможности для поверхности видеопамяти. Кроме того, DD_HALINFO содержит указатель на структуру D3DNTHAL_CALLBACKS, содержащую адреса функций косвенного вызова DDI для DirectSD. В табл. 2.2 перечислены некоторые структуры, в которых передаются сведения о функциях поддержки DirectDraw/DirectSD в драйвере экрана. Таблица 2.2. Функции косвенного вызова DDI, обеспечивающие поддержку DirectDraw/DirectSD (неполный список) Структура
Функции
DO CALLBACKS
DdDestroyDriver, DdCreateSurface, DdSetColorKey, DdSetMode, DdWai tForVerti calBlank, DdCanCreateSurface, DdCreatePalette, DdGBetScalLine, DdMapMemory
DO SURFACECALLBACKS
DdDestroySurface, DdFlip, DdSetClipList, DdLock, DdUnlock, DdBlt, DdSetColorKey, DddAddAttachedSurface, DdGetBltStatus, DdGetFlipStatus, DdUpdateOverlay, DdSetOverlayPositions, . DdSetPalette
DD_PALETTECALLBACKS
DdDestroyPalette, DdSetEntries
DD_NTCALLBACKS
DdFreeDriverMemory, DdSetExclusiveMode, DdFlipToGDISurface
DD_COLORCONTROLCALLBACKS
DdColorControl
DDMISCELLANEOUSCALLBACKS
DdGetAvai1Dri verMemory
Драйверы принтеров
129
Структура
Функции
D3DNTHAL_CALLBACKS
D3dContextCreate, D3dContextDestroy, DSdContextDestroyAll, DSdSceneCapture, DSdTextureCreate, D3dTextureDestroy, DSdTextureSwap, DSdTextureGetSurf
D3DNTHAL CALLBACKS3
D3dClear2, DSdValidateTextureStageState, D2dDrawPrimitives2
Даже при кратком знакомстве с интерфейсами GDI DDI и DirectDraw/ DirectSD DDI становится очевидным, что они имеют абсолютно разную архитектуру. Ниже перечислены наиболее принципиальные различия. О Интерфейс GDI DDI работает на более примитивном уровне, нежели DirectDraw/DirectSD DDI. Следовательно, перед обращением к интерфейсу GDI DDI графическому механизму приходится выполнить большое количество предварительных операций, тогда как путь DirectDraw/DirectSD, ведущий от Win32 API, проще и прямее. О В поддержке GDI DDI драйвер экрана всегда может получить помощь от графического механизма, вернув ему полученный вызов. В интерфейсе DirectDraw/DirectSD DDI программная эмуляция выполняется в клиентской DLL пользовательского режима, поэтому драйвер экрана не сможет воспользоваться помощью уровня драйвера режима ядра. О Для получения информации о драйверных функциях косвенного вызова в интерфейсе GDI DDI используется простой и легко расширяемый способ, тогда как в DirectDraw/DirectSD DDI функции косвенного вызова описываются массивом структур данных. Итак, мы рассмотрели процесс инициализации драйвера экрана для поддержки GDI, DirectDraw и DirectSD и даже кое-что узнали об основных точках входа и функциях косвенного вызова драйвера. Мы еще вернемся к этой теме и посмотрим, как эти функции косвенного вызова используются графическим механизмом, при исследовании внутренних структур данных графической системы 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). Модуль визуализации обеспечивает нестандартную обработку графических команд, полутоновые преобразования и построение данных, готовых к передаче на принтер. Модуль пользовательского интерфейса позволяет настраивать страницы свойств принтера, структуру DEVMODE и процесс обработки событий печати.
драйверы принтеров
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 и DirectSD. Разумеется, драйвер принтера должен поддерживать основные функции, отвечающие за инициализацию драйверов графических устройств, — а именно, 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 должна выдавать соответствующую информацию при обработке запроса DRVQUERY_USERMODE. Все стандартные драйверы принтеров Microsoft Windows 2000 являются драйверами пользовательского режима. Таблица 2.3. Специализированные точки входа драйвера принтера Точка входа
Назначение
DrvQueryDriverlnfo (необязательна)
Интерпретация запроса зависит от драйвера. В настоящее время используется для получения информации от драйверов пользовательского режима
DrvQueryOevi ceSupport (необязательна)
Интерпретация запроса зависит от устройства. В настоящее время используется для запросов о поддержке JPEG и PNG
DrvStartDoc
Сообщает драйверу о готовности GDI к передаче документа
DrvEndDoc
Сообщает драйверу о завершении передачи документа
DrvStartPage
Сообщает драйверу о готовности GDI к передаче графических команд новой страницы
DrvSendPage
Сообщает драйверу о завершении передачи графических команд страницы — драйвер может передать обработанные данные спулеру
DrvStartBanding
У драйвера запрашивается информация о том, где на странице должна начинаться разбивка на полосы
DrvQueryPerBandInfо (необязательна)
У драйвера запрашивается структура PERBANDINFO с информацией о размере и разрешении полосы
DrvNextBand (необязательна)
Сообщает драйверу о завершении передачи графических команд полосы — драйвер может передать обработанные данные спулеру
Главной точкой входа драйвера принтера по-прежнему остается DrvEnablePrinter. Когда приложение вызывает CreateDC, чтобы создать контекст устройства для принтера, графический механизм проверяет, загружен ли драйвер принтера. Если драйвер не загружен, он загружается, после чего вызывается функция DrvEnableDriver.
Драйверы принтеров
133
Функция DrvEnablePDEV драйвера принтера вызывается графическим механизмом, когда приложение вызывает функцию CreateDC для принтера. По сравнению с драйвером экрана эта функция сложнее, поскольку ей приходится учитывать многочисленные параметры печати, переданные в структуре DEVMODE. В частности, драйверу приходится регулировать разрешение вывода в зависимости от выбранного качества печати, переключать размеры бумаги для альбомного (landscape) режима, вычислять размеры области вывода на основании размера бумаги и передавать информацию о полях. В структуре DEVMODE могут передаваться и другие параметры печати — режим двусторонней печати, разбор по копиям, количество экземпляров, количество страниц на листе, а также специализированные параметры, обеспечиваемые другими компонентами системы печати. Например, в 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 при содействии графического механизма передает весь документ драйверу принтера. Процесс вывода выглядит примерно так: DrvStartDoc(DocName. Jobld) for (int i = 0; 1
134
Глава 2. Архитектура графической системы Windows
Draw(conmand[c]): bMoreBands = DrvNextBandO:
} DrvSendPageO;
DrvEndDoc();
Вывод всего задания печати производится между вызовами DrvStartDoc и DrvEndDoc, а вывод отдельной страницы — между вызовами DrvStartPage и DrvEndPage. Для каждой страницы сначала вызывается функция DrvStartBanding, которая сообщает драйверу о начале разбивки на полосы. Графический механизм вызывает функцию DrvQueryPerBandlnfo для получения геометрической информации о выводимой полосе, после чего перебирает команды GDI, хранящиеся в файле, и передает все команды, задействованные в текущей полосе. После завершения полосы GDI переходит к следующей полосе и т. д. до завершения всей страницы. Графические команды DDI либо воспроизводятся GRE непосредственно на поверхности, управляемой GDI, либо передаются функциям, предоставленным драйвером. Для векторных устройств преобразованные команды могут передаваться спулеру при каждой операции графического вывода. Для растровых устройств полученный растр проходит полутоновое преобразование в соответствии с цветовой глубиной принтера, делится на несколько цветовых плоскостей (например, в схеме CYMK), сжимается и кодируется на языке принтера. При передаче данных спулеру в качестве параметра указывается манипулятор, переданный драйверу при вызове DrvEnablePDEV. Функция EngWriteSpooler использует этот манипулятор для общения со спулером. Драйвер принтера также может поддерживать функции DrvEscape и DrvDrawEscape для «официального» и «закулисного» взаимодействия приложения с драй.вером. Например, драйвер PostScript позволяет вставлять данные в формате PostScript прямо в поток данных при помощи функции DrvDrawEscape. Информация о поддержке этой возможности может быть получена при помощи функции DrvEscape. В процессе обработки задания печати драйвер принтера может вызвать функцию EngCheckAbort, чтобы проверить, не отменил ли пользователь печать. Ресурсы, выделенные для поверхности, должны освобождаться функцией драйвера DrvDisableSurface. При вызове функции DeleteDC приложением или GDI также вызывается функция DrvDisablePDEV, что позволяет драйверу освободить ресурсы, выделенные для физического устройства. Если приложение вызывает ResetDC в процессе печати, GDI вызывает DrvEnablePDEV для создания нового экземпляра. Далее вызывается функция DrvResetPDEV, чтобы драйвер мог скопировать информацию из старого экземпляра в новый, затем вызывается DrvDisableSurface для старого устройства, после чего вся последовательность операций вывода начинается заново для нового устройства. Перед выгрузкой графической DLL принтера вызывается функция DrvDisableDriver.
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;
Црайвер принтера для вывода документа HTML Вы когда-нибудь размышляли над вопросом, как преобразовать страницу документа в растровое изображение? Конечно, существует простое решение — со-
class KDevice
{
int -int
nNesting: nPages;
- '
// документ, страница // количество выведенных страниц
136
Глава 2. Архитектура графической системы Windows void void void void
Write(const char * pStr): WriteW(const WCHAR * pwStr); WriteHextunsigned val): Writeln(const char * pStr = NULL):
void
Write(DWORD index, const PAIR * pTable);
char void void public: int int
* CopyBlock(char * pDest. void * pData. int size); CopySurface(char * pDest. const SURFOBJ * pso); LogCalHint index, const void * para, int parano); width; height;
HPALETTE hPalette: HSURF
hSurface;
HDEV HANDLE
hDevice: hSpOOler;
int
nimage;
// ширина в дюймах * 10 // высота в дюймах * 10 // При использовании GDI палитра нужна // даже для 24-битных растров // Стандартная поверхность. // управляемая устройством // Манипулятор устройства GDI // Манипулятор спулера
void Oeate(void) {
nNesting = 0; nPages = 0; nimage = 0;
void
DumpSurface(const SURFOBJ * psoBM):
BOOL BOOL BOOL BOOL BOOL
CallEngine(int index, const void * para, int parano): StartDoc(LPCWSTR pszDocName. const void * firstpara, int parano): EndDoc(const void * firstpara. int parano): StartPage(const void * firstpara, int parano): SendPagetconst void * firstpara. int parano);
Класс KDevice содержит переменные для хранения размеров бумаги, а также манипуляторов палитры, поверхности, устройства и спулера. Другие переменные класса предназначены для внутреннего использования. Переменная nNesting обеспечивает правильную последовательность вызовов DrvStartDoc, DrvEndDoc, DrvStartPage и DrvSendPage; переменная nPages содержит количество печатаемых страниц; переменная nimage — порядковый номер ссылки HTML на графическое изображение. В классе KDevice нет ни полноценного конструктора, ни деструктора, ни виртуальных функций — мы не хотим включать runtime-поддержку C++ в DLL режима ядра. Частичная инициализация выполняется методом Create. Методы W r i t e сбрасывают данные HTML в поток данных, передаваемых спулеру. Метод DumpSurface записывает содержимое DIB-поверхности в отдельный растровый файл.
137
Драйверы принтеров
Обратите внимание на функцию «Device: :LogCall, предназначенную для вывода имени точки входа драйвера и списка параметров. При вызове передается индекс точки входа, адрес первого параметра в стеке и количество параметров (при передаче параметров используются соглашения языка Pascal). Функция ограничивается простой выдачей шестнадцатеричного дампа параметров. void KDevice: :LogCall(int index, const void * firstpara, int parano)
WriteCindex. Pair_DDIFunction): // Имя функции берется из таблицы WriteC'C'): const unsigned * pDWORD = (const unsigned *) firstpara; for (int i=0; i<parano; i++) { WriteHex(pDWORD[i]);
if ( i Kparano-1) ) Writer. ");
Функция KDevice:: Call Engine проверяет указатель на KDevice; если указатель отличен от NULL, она вызывает функцию LogCall. Функция CallEngine вызывается всеми графическими функциями DDI драйвера перед возвращением вызова графическому механизму. Таким образом, реализация CallEngine может избирательно блокировать некоторые функции DDI, чтобы заставить графический механизм разбить их на упрощенные вызовы. Интерфейс DDI драйвера реализуется в файле HTMLDrv.cpp. Файл начинается с таблицы поддерживаемых точек входа: const DRVFN DDI_Funcs[] = { INDEX_DrvEnablePDEV, INDEX_DrvCompletePDEV. INDEX_DrvResetPDEV. INDEX_DrvDisablePDEV, INDEX_DrvEnableSurface. INDEX_DrvDi sabl eSurface.
(PFN) (PFN) (PFN) (PFN) (PFN) (PFN)
DrvEnablePDEV. DrvCompletePOEV. DrvResetPDEV. DrvOisablePDEV. DrvEnableSurface. DrvDisableSurface.
INDEX_DrvStartDoc. INDEX_DrvEndDoc, INDEX_DrvStartPage. INDEX_DrvSendPage.
(PFN) (PFN) (PFN) (PFN)
DrvStartDoc. DrvEndDoc. DrvStartPage. DrvSendPage.
(PFN) DrvStrokePath. INDEX_DrvStrokePath. (PFN) DrvFillPath. INDEXJDrvFillPath. INDEX_DrvStrokeAndFi 11 Path, (PFN) DrvStrokeAndFillPath. (PFN) DrvLineTo. INDEX_OrvLineTo. (PFN) DrvPaint. INDEX_DrvPaint. (PFN) DrvBitBH. INDEX_DrvBitBH. (PFN) DrvCopyBits. INDEX OrvCopyBits.
138
Глава 2. Архитектура графической системы Windows
INDEX JlrvStretchBlt, INDEX DrvTextOut.
(PFN) DrvStretchBlt. (PFN) DrvTextOut
Функция DrvEnableDriver после несложной проверки передает таблицу функций графическому механизму: BOOL APIENTRY DrvEnableDriver(ULONG iEngineVersion, ULONG cj, DRVENABLEDATA *pded) '{ // Проверить параметры if (iEngineVersion < DDI_DRIVER_VERSION) { EngSetLastError(ERROR_BAD_DRIVER_LEVEL): return FALSE: } if (cj < sizeof(DRVENABLEDATA)) { EngSetLastError(ERRORJNVALID_PARAMETER); return FALSE; pded->iDriverVersion = DDI_DRIVER_VERSION: pded->c - sizeof(DDIJtooks) / sizeof(DDI_Hooks[0]): pded->pdrvfn = (DRVFN *) DDIJtooks; return TRUE:
139
Драйверы принтеров if (pDevice =- NULL) { EngSetLastError(ERROR_OUTOFMEMORY); return NULL; pDevice->Create(): pDevice->hSpooler pDevice->hPalette
= hDriver; = EngCreatePalette (PAL_BGR, 0. 0. 0, 0. 0);
if (pdm == NULL || pdm->dmOrientation == DMORIENTJWRAIT) { pDevice->width = PaperWidth; pDevice->height = PaperHeight; } else { pDevice->width = PaperHeight; pDevice->height = PaperWidth: } // Инициализация GDIINFO пропущена // Инициализация DEVINFO пропущена pdi->hpalDefault
= pOevice->hPalette:
return (DHPDEV) pDevice:
}
Ниже приведена часть функции DrvEnablePDEV, создающей новый экземпляр класса KDevice. Эта функция заносит информацию о возможностях драйвера в структуры GDI INFO и DEVINFO в соответствии со значениями полей полученной структуры DEVMODW. В данном случае программа проверяет ориентацию бумаги. DHPDEV APIENTRY DrvEnab1ePDEV(DEVMODEW *pdm. LPWSTR pwszLogAddress. ULONG cPat, HSURF *phsurfPatterns. ULONG cjCaps. ULONG *pdevcaps. ULONG cjDevInfo. DEVINFO *pdi. HDEV hdev. PWSTR pwszDeviceName. HANDLE hDriver) { if ( (cjCaps<sizeof(GDIINFO)) || (cjDev!nfo<sizeof(DEVINFO)) ) { EngSetLastError(ERROR_INVALID_PARAMETER): return FALSE:
Функция DrvEnableSurface создает полностраничную 24-битную DIB-поверхность, управляемую GDI, и сообщает графическому механизму, что драйвер желает перехватывать некоторые вызовы DDL Обратите внимание: размеры бумаги задаются в десятых долях дюйма, чтобы избежать выполнения операций с плавающей точкой в ядре. Поверхность инициализируется белым цветом не при создании, а при вызове DrvStartPage. HSURF APIENTRY DrvEnableSurface(DHPDEV dhpdev) { KDevice * pDevice = (KDevice *) dhpdev; SIZEL sizl = { pOevice->width * Dpi / 10. pDevice->height * Dpi / 10 }; pDevice->hSurface = (HSURF) EngCreateBitmap(sizl. sizl.cy. BMF_24BPP. BMF NOZEROINIT. NULL): if (pDevice->hSurface return NULL;
NULL)
EngAssociateSurface(pDevice->hSurface. pDevice->hDevice. HOOK_BITBLT | HOOK_STRETCHBLT | HOOKJEXTOUT | HOOK_PAINT | HOOK_STROKEPATH
KDevice * pOevice: // Создать объект физического устройства. Маркер = HTMD pDevice - (KDevice *) EngAllocMem (FL_ZERO_MEMORY, sizeof(KDevice). 'DMTH'):
HOOKJILLPATH | HOOK_STROKEANDFILLPATH | HOOK_COPYBITS | HOOK_LINETO);
return pDevice->hSurface;
140
Глава 2. Архитектура графической системы Windows
Хотя драйвер HTML перехватывает некоторые графические функции, вся реализация сводится к простому выводу информации о параметрах, после чего вызов возвращается графическому механизму. Ниже приведен лишь один типичный пример, который дает представление и об остальных реализациях. В первом параметре всех графических вызовов DDI передается указатель на SURFOBJ — структуру данных, используемую графическим механизмом для представления поверхности вывода. Поле dhpdev содержит манипулятор физического устройства, который предоставляется драйвером и возвращается функцией DrvEnablePDEV. В данном случае манипулятор преобразуется в указатель на KDevice.
25Т5520Ге2^
DivTextOut(el390e58, £b317828, e2727d08, ft>31792c, 0, ft>317834, e25154cc, e2515520, e251541c, dOd) 282. DrvSendPage(el390e58) 281.
Windows 2000 Printer Test Page
BOOL APIENTRY DrvBitB1t(SURFOBJ *psoTrg. SURFOBJ *psoSrc. SURFOBJ *psoMaskc. CLIPOBJ *pco. XLATEOBJ *pxlo. RECTL *prc1Trg, POINTL *pptlSrc. POINTL *ppt!Mask, BRUSHOBJ *pbo. POINTL *pptlBrush. ROP4 rop4) KDevice * pDevice = (KDevice *) psoTrg->dhpdev; if ( pDevice->CallEngine(INDEX_DrvBitBlt. &psoTrg. 11) ) return EngBitBlt(psoTrg. psoSrc. psoMask. pco, pxlo. prclTrg, pptlSrc. pptlMask. pbo, pptlBrush. rop4); else return FALSE:
Так выглядят самые интересные компоненты драйвера HTML. Ниже приведена сокращенная версия результатов, полученных при печати стандартной тестовой страницы. В web-браузере она выглядит вполне прилично (рис. 2.9).
Test Page DrvStartDoc(e2229558. ele86488. 2) <1 i >DrvSta rtPage(e2229558)1i > <1i>DrvFillPath(e2229558. fld2fa68 l) DrvBitBlt(e2229558. 0. 0. fld2f7bO fOfO) DrvTextOut(e2229558, fld2f824. ... <11>DrvTextOut(e2229558. fld2f824. ... <1 i>DrvSendPage(e22295586)!i> <11>DrvEndDoc(62229558. 0)
141
Итоги
Рис. 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
Каталог проекта
Описание
Samples\Chapt_02\SysCall
Вывод списка системных функций DLL подсистемы Win32 (ntdll.dll, gdi32.dll и user32.dll) и системных функций ядра ОС (ntoskrnl.exe, win32k.sys)
Samples\Chapt_02\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 всегда экспортируют функцию DIlGetClassObject, которая (при содействии фабрики класса) отвечает за создание новых объектов, поддерживаемых 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
Манипуляторы и объектно-ориентированное программирование
m_LogPen.lopnColor
поддерживает несколько функций для создания экземпляра объекта и несколько функций для его уничтожения. Чтобы продемонстрировать аналогию между объектно-ориентированным программированием и Win32 API, попробуем написать на C++ минимальную псевдо-реализацию GDI. Результат приведен в листинге 3.1.
147
= crColor;
int GetObjectdnt cbBuffer, void * pBuffer) { if ( pBuffer==NULL ) return sizeof(LOGPEN): else if ( cbBuffer>=sizeof(m_LogPen) ) {
Листинг 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;
memcpy(pBuffer. & m_LogPen, sizeof(m_LogPen)): return sizeof(LOGPEN);
else SetLastError(ERRORJNVALID_PARAMETER); return 0:
class _Pen : public _GdiObj
bool DeleteObject(void) { if ( this ) {
{ public: virtual int GetObjectType(void) {
delete this; return true;
return OBJ_PEN;
} else
}
virtual int GetObjecttint cbBuffer. void * pBuffer) = 0: virtual bool Del eteObjectt void) - 0: virtual bool Unreal izeObject(void) { return true;
return false;
} }: _Pen * _CreatePen(int fnPenStyle. int nWidth. COLORREF crColor) {
return new _RealPen(fnPenStyle, nWidth. crColor);
_Pen * _CreatePenCint fnPenStyle. int nWidth. COLORREF crColor): // gdi.cpp #define STRICT Idefine
void Test(void)
{
f include <windows.h> finclude "gdi .h"
}
class _RealPen : public _Pen { LOGPEN m_LogPen: public: _RealPen(int fnPenStyle, { m_LogPen.lopnStyle mJ-OgPen.lopnWidth.x m_LogPen.lopnWidth.y
_Pen * pPen = _CreatePen(PS_SOLID. 1. RGB(0. 0. O x F F ) ) ;
//// pPen->DeleteObjectt):
WIN32_LEAN_AND_MEAN
int nWidth. COLORREF crColor) - fnPenStyle: = nWidth: - 0;
В листинге 3.1 определяется абстрактный базовый класс _GdiObj, представляющий обобщенный объект GDI. Он состоит из четырех чисто виртуальных функций и не содержит ни одной переменной. Обобщенный класс пера _Реп определяется как производный от _GdiObj; он реализует две виртуальные функции и оставляет две другие чисто виртуальными. Функция _CreatePen создает экземпляры класса _Реп. В файле реализации (GDI.cpp) определяется настоящий класс пера (JtealPen), который хранит информацию о пере в структуре LOGPEN. Класс _Real Pen представляет собой полную реализацию абстрактного класса _Реп с конструктором и двух оставшихся виртуальных функций. Функция _CreatePen
148
Глава 3. Внутренние структуры данных GDI/DirectDraw
создает экземпляр класса _RealPen и передает указатель на него вместо указателя на обобщенный класс пера _Реп. Клиентская сторона не знает, сколько памяти занимает объект пера, откуда выделяется эта память и как реализуются виртуальные функции. Все эти подробности остаются скрытыми. Клиентская сторона должна знать лишь имена методов интерфейса, их назначение и семантику.
Указатели и манипуляторы При создании объекта в объектно-ориентированном языке необходимо выделить блок памяти для хранения переменных объекта. Если класс содержит виртуальные функции, вместе с переменными в памяти создается дополнительный указатель на таблицу всех реализаций виртуальных функций данного класса. В таких языках, как C++, центральное место занимают указатели на объекты. Указатели передаются всем не статическим функциям класса, что позволяет обращаться к переменным объекта и вызывать нужные виртуальные функции. В C++ указатель на текущий объект обозначается ключевым словом this. Рисунок 3.1 на примере класса _RealPen (см. листинг 3.1) показывает, на что именно ссылается указатель на объект. В классе _RealPen 16 байт нужны для хранения единственной переменной класса, m_LogPen, и еще 4 байта — для указателя на таблицу виртуальных функций. Следовательно, для каждого объекта необходимо выделить минимум 20 байт. Указатель на таблицу виртуальных функций ссылается на блок из четырех указателей на функции. В приведенном примере две функции реализуются классом _Реп, а две другие — классом _RealPen. Экземпляр класса_Реа!Реп Указатель^ на объект
Указатель на таблицу виртуальных функций LOGPEN
lopnStyle
Таблица виртуальных функций для класса _RealPen Class &
Pen::GetObjectType
& _RealPen::GetObject & _RealPen::DeleteObject
lopnWidth
& _Pen::UnrealizeObject
lopnColor Рис. 3.1. Пример представления объекта в C++
В СОМ указатель на объект обычно называется интерфейсным указателем и ссылается на указатель на таблицу функций. В приведенном примере функция _CreatePen создает экземпляр _RealPen, но возвращает указатель на класс _Реп. Как и в СОМ, клиентский код ничего не знает о внутреннем представлении данных. Хотя в Win32 API для каждого объекта где-то в памяти выделяется блок данных, разработчики Microsoft не стали возвращать указатель на него пользовательскому приложению. Возможно, это было сделано из тех соображений, что указатель несет слишком много информации для «умных» программистов — он выдает точное местонахождение объекта в памяти. Указатели позволяют выпол-
Манипуляторы и объектно-ориентированное программирование
149
нять операции чтения/записи с внутренним представлением объектов, которое операционная система предпочла бы скрыть от пользователя. Кроме того, указатели затрудняют совместное использование объектов из адресных пространств разных процессов. Чтобы скрыть эту информацию от программистов, функции создания объектов Win32 вместо указателя обычно возвращают манипулятор (handle) объекта. Манипулятор определяется как число, которое однозначно идентифицирует объект и может использоваться для косвенных ссылок на него. Взаимосвязь объектов с манипуляторами не документирована, ее неизменность в будущих версиях Windows не гарантируется, и вообще все подробности известны разве что Microsoft да еще нескольким производителям системных утилит. Можно считать, что отображение на манипуляторы указателей на объекты и наоборот производится двумя функциями Encode и Decode, прототипы которых приведены ниже. HANDLE Encode(void * pObject): // Преобразовать указатель в манипулятор void * Decode(HANDLE hObject); // Преобразовать манипулятор в указатель
Тождественное отображение Иногда значение манипулятора может совпадать со значением указателя на объект; в этом случае функции Encode и Decode ограничиваются преобразованием типа, а связь между указателями на объекты и манипуляторами является тождественной. В Win32 API манипулятор экземпляра (HINSTANCE) или манипулятор модуля (HMODULE) представляет собой обычный указатель на образ РЕ-файла, отображаемого на память. Считается, что функция LockResource фиксирует ресурс в памяти и отображает глобальный манипулятор на указатель, но в действительности их значения совпадают. Манипулятор ресурса, возвращаемый функцией LoadResource, в действительности представляет собой «замаскированный» указатель на ресурс, отображенный на память.
Табличное отображение Наиболее распространенным механизмом установления связи между объектом и его манипулятором является табличное отображение. Операционная система строит таблицу всех используемых объектов. При создании нового объекта в таблице находится пустая строка, которая заполняется данными объекта. При удалении объекта его переменные удаляются из памяти, а соответствующий элемент таблицы освобождается для последующего использования. В табличной схеме управления объектами индексы таблицы являются хорошими кандидатами на роль манипуляторов, а преобразование указателей в манипуляторы и наоборот выполняется тривиально. В 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 поддерживаются три типа растров — какой тип следует выбрать, чтобы сократить затраты системных ресурсов? Главным ресурсом компьютера является процессорное время. М1скировка внутреннего представления от программиста затрудняет оценку сложности выполнения некоторых операций при проектировании сложных алгоритмов. Допустим, вы строите сложный регион средствами GDI; какую сложность имеет ваш алгоритм — линейную, квадратичную, кубическую? Полная маскировка реализации также усложняет отладку. Если после 5 минут работы ваша программа начинает «гнать мусор», вероятно, где-то происходит утечка ресурсов, но где именно и как ее исправить? Если вы — системный администратор и в вашей системе работают сотни приложений, как при хронической нехватке системных ресурсов вычислить источник бед? Похоже, единственным инструментом для борьбы с утечками ресурсов является программа BoundsChecker, которая использует специальные средства наблюдения для поиска несоответствий при создании и удаления объектов. И все же самые серьезные проблемы возникают с совместимостью программ. Почему в Windows 95 программа может передавать объекты GDI от одного процесса к другому, а в Windows NT/2000 — не может? Почему Windows 95 не справляется с обработкой больших аппаратно-независимых растров? Похоже, «идеальная» абстракция API в разных системах обладает разной семантикой. В основной части этой главы мы поближе познакомимся с манипуляторами GDI и исследуем недокументированный мир, скрытый за манипуляторами Windows NT/2000.
Расшифровка манипуляторов объектов GDI
151
расшифровка манипуляторов объектов GDI При создании объекта GDI вы получаете манипулятор этого объекта. В зависимости от типа создаваемого объекта манипулятор может относиться к типу HPEN, HBRUSH, HFONT, НОС и т. д. Однако самым общим типом манипулятора объекта GDI является тип HGDIOBJ. Тип HGDIOBJ определяется как указатель на void. Определение типа HPEN, используемое при компиляции, изменяется в зависимости от состояния макроса компиляции STRICT. Если макрос определен, то HPEN определяется следующим образом: struct HPEN_ { int unused: }: typedef struct HPEN_ * HPEN; Если макрос STRICT не определен, то определение HPEN выглядит так: typedef void * HANDtE: typedef HANDLE HPEN: Проще говоря, если макрос STRICT определен, HPEN определяется как указатель на структуру с одним неиспользуемым полем, а если нет — как указатель на void. Компилятор C/C++ позволяет передать указатель на любой тип вместо указателя на void (но не наоборот!). Два указателя на разные типы, отличные от void, не являются взаимозаменяемыми. При определении STRICT компилятор выдает предупреждения при некорректной подмене типов манипуляторов объектов GDI или других объектов (скажем, HWND, HMENU и т. д.), а без определения STRICT вы можете спокойно смешивать разные типы манипуляторов, не рискуя получить предупреждение на стадии компиляции. Например, при определении STRICT можно передать HPEN функции, получающей HGDIOBJ (скажем, функции DeleteObject), но нельзя без предварительного преобразования передать HGDIOBJ функции, получающей HBRUSH (такой, как функция FillRgn). Столь четкое разделение различных манипуляторов GDI имитирует иерархию классов объектов GDI, хотя на самом деле иерархии классов не существует. Для каждого объекта GDI может быть создан только один манипулятор, поэтому вы не сможете создать другой манипулятор для объекта простым дублированием. Обычно манипуляторы объектов GDI действительны только в рамках конкретного процесса — другими словами, манипулятор может использоваться только тем процессом, который его создал. Манипуляторы, переданные из других процессов, недействительны. Как правило, объекты GDI могут создаваться несколькими разными способами, а уничтожаются одной функцией DeleteObject с параметром HGDIOBJ. Помимо непосредственного создания объекта, можно воспользоваться функцией GetStockObject для получения манипулятора заранее созданного объекта GDI или же при помощи более сложных функций преобразовать ресурс, связанный с модулем, в объект GDI. Функции загрузки ресурсов GDI (такие, как LoadBitmap или Loadlmage) создают необходимые объекты GDI за вас. Впрочем, все сказанное выше можно найти и в электронной документации. 'Мы же хотим узнать о манипуляторах объектов GDI гораздо больше. Подробности работы манипуляторов GDI Windows NT/2000 никогда не документировались, к тому же не существует никаких готовых программ, способных упростить наши исследования. Поэтому мы напишем свою, довольно сложную программу
152
Глава 3. Внутренние структуры данных GDI/DirectDraw
GDIHandles, главное окно которой состоит из трех страниц-вкладок Строение реализация и использование этой программы будут рассматриваться постепенно по мере изложения материала. А пока взгляните на первую страницу Decode GDI Handle (Расшифровка манипулятора GDI), изображенную на рис 3 2
_U>eate8DIH
01900011 01900013 0190001S 01900015 Olb00017
oiboooie OlbOOOlS 018a0028 018a0027 018a0029 018a0021 OlSaOOZS
"—a.
GetStockObject(BLACK_BRUSH) 0 GetStockObject(DKGPAY_BRUSH) 0 GetStockObject(HOLLOWJBRUSH) 0 GetStockObject(NULL_BRUSH) 0 GetStockObject(ВLACK_PEN) 0 GetStockObject (ITOLL_PEIIJ 0 GetStockObject (TJHITE_PEN) О GetStockObject(AHSI_FIXED_FOHT) 0 GetStockObject(ANSI_VAR_FQHT> 0 Get St о ck Ob j e ct (D E FAUL T_GUI_F OUT) 0 GetStockObject(SYSTEH_FOHT) 0 Get St о ck Ob j e ct (SYS Т EH_FIXED_F OUT) 0
Расшифровка манипуляторов объектов GDI
153
Манипуляторы стандартных объектов — константы Манипуляторы, возвращаемые функцией GetStockObject, всегда являются константами независимо от порядка их вызова. Например, функция GetStockObject (BLACK BRUSH) возвращает стандартный (встроенный) объект черной кисти с манипу" лятором 0x01900011; GetStockObject(BLACK_PEN) возвращает стандартный объект черного пера с манипулятором OxOlbOOOl? и т. д. Даже если запустить два экземпляра этой программы, GetStockObject возвращает одинаковые значения в обоих процессах. Можно предположить, что встроенные объекты создаются при инициализации системы и используются заново всеми процессами.
HGDIOBJ не является указателем Хотя в заголовочных файлах Windows манипуляторы GDI определяются как указатели, при ближайшем рассмотрении они совершенно не похожи на указатели. Создайте несколько объектов GDI и посмотрите на полученные манипуляторы; вы увидите, что их значения лежат в интервале от 0x01900011 до Oxba040389. Если бы значение типа HGDIOBJ в действительности было указателем, как утверждает заголовочный файл wingdi.h, то нижняя граница соответствовала бы недействительному указателю на свободную область пользовательского адресного пространства, а верхняя адресовала бы адресное пространство ядра. Можно предположить, что манипуляторы GDI на самом деле не являются указателями. Обращает на себя внимание еще один факт: значения манипуляторов, полученных при вызовах GetStockObject (BLACK_PEN) и GetStockObject (NULL_PEN), отличаются всего на 1, что явно меньше объема памяти, необходимого для хранения внутренних объектов GDI, если бы манипуляторы действительно были указателями. Поэтому можно уверенно сказать, что HGDIOBJ не является указателем.
Cancel Рис. З.2. Расшифровка манипуляторов GDI
В верхней части страницы расположены два комбинированных списка для Сгеат^ТЛ ? л°ЗДаНИЯ 0бЪСКТа (°Т РазнообРазнь1х вызовов GetStockObject до LreateEnhMetafile) и количества экземпляров (от 1 до 65 536). После выбора спосооа создания и количества экземпляров щелкните на кнопке Create - программа cm^ 3aflfН0е количество объектов. Манипуляторы, полученные в результате ВЫВОДЯТСЯ в большом с вместе г "I' ™ске в шестнадцатеричной записи, вместе с именем функции-создателя и порядковым номером в группе (нумеоаЦия начинается с 0). Цикл создания завершается при неудачном^ызове с^ункТОГ же ° манипулятора, что и при преДаваЙте проведем несколько экспериментов, понаблюдаем за процессом соИ Пр анализи ляторов ° РУем закономерности в значениях манипу-
Максимальное количество манипуляторов 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 - 15392. Учитывая манипуляторы объектов GDI, используемые окном GDIHandles и другими процессами, можно обоснованно предположить, что максимальное количество манипуляторов GDI в системе равно 16 384.
Часть HGDIOBJ содержит индекс Создавая многочисленные объекты GDI при помощи программы GDIHandles, обратите особое внимание на младшие слова отображаемых двойных слов; вы увидите, что их значения лежат в интервале от 0x0000 до OxSFFF. Младшие слова манипуляторов всегда уникальны в границах процесса; более того, их уникальность сохраняется и между процессами, если не считать стандартных объектов. Значения младших слов манипуляторов иногда увеличиваются, иногда уменьшаются, причем закономерность порой сохраняется даже между процессами. Например, при вызове CreatePen в одном процессе младшее слово манипулятора может быть равно ОхОЗС!, а при следующем вызове CreatePen в другом процессе младшее слово манипулятора оказывается равным ОхОЗС2. У этих фактов имеется простое объяснение: младшее слово HGDIOBJ представляет собой индекс в таблице системного уровня, содержащей информацию о 16 384 (0x4000) объектах GDI.
Часть HGDIOBJ содержит тип объекта GDI В Windows NT/2000 манипулятор объекта GDI всегда возвращается в виде 32разрядного числа. В программе GDIHandles это число отображается в виде 8 шестнадцатеричных цифр. Как было показано выше, младшие 4 шестнадцатеричные цифры манипулятора GDI содержат индекс объекта, поэтому мы можем заняться старшими 4 шестнадцатеричными цифрами.
155
Расшифровка манипуляторов объектов GDI
Если создавать объекты по типам (например, создать несколько кистей, затем несколько перьев, несколько шрифтов, контекстов устройств и т. д.), нетрудно убедиться в том, что манипуляторы GDI однотипных объектов имеют нечто общее — а именно, третья и четвертая шестнадцатеричные цифры их манипуляторов практически всегда совпадают. У кистей третья и четвертая цифры манипулятора всегда равны 0x90 и 0x10; у перьев — 0x30 и ОхЬО; у шрифтов — Ох8а и ОхОа; у палитр — 0x88 и 0x08; у растров — 0x05, а у контекстов устройств — 0x01. Манипуляторы, у которых старший бит этой группы цифр равен 1, принадлежат стандартным объектам. Таким образом, у нас имеется достаточно оснований, чтобы утверждать: третья и четвертая шестнадцатеричные цифры манипулятора содержат признак типа объекта и признак стандартного объекта GDI. Смысл двух оставшихся шестнадцатеричных цифр 32-разрядного манипулятора GDI пока остается неясным. Давайте подведем итог того, что мы знаем о манипуляторах Windows NT/2000. Манипулятор объекта GDI начинается с 8 старших бит, смысл которых пока неизвестен; далее следуют: 1 бит признака стандартного объекта, 7 бит с информацией о типе объекта и 16-битного индекса, старшие 4 бита которого всегда равны 0. Нам известны значения 7-битного типа объекта для контекста устройства, региона, растра, палитры, шрифта, кисти, расширенного метафайла, пера и расширенного пера. Структура манипулятора GDI изображена на рис. 3.3. 8 неизвестных бит
4 бита — не используются
\
/XV 1
12 бит — индекс 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 = OxOa. gdi_o_bjtypeb_brush - 0x10. gdi_objtypeb_enhmetafile = 0x21,
156
Глава 3. Внутренние структуры данных GDI/DirectDraw
gdi_objtypeb_pen gdi_objtypeb_extpen
= 0x30, = 0x50
Inline HGDIOBJ makeHGDIOBJ(unsigned top. bool stock, unsigned objtype, unsigned index)
{
return ((top & OxFF) «24) | ((stock & 1) « 23) | ((objtype & Ox7F) « 23) | (index & Ox3FFF):
inline bool IsStockObj(HGDIOBJ hGDIObj) { return ((unsigned) hGDIObj) 0x00800000: } inline unsigned GetObjType(HGDIOBJ hGDIObj) {
return ((unsigned) hGDIObj) » 16) & Ox7F:
} inline unsigned GetObjIndex(HGDIOBJ hGDIObj) { return ((unsigned) hGDIObj & Ox3FFF);
}
При помощи этих функций можно узнать, принадлежит ли манипулятор стандартному объекту GDI, а также получить тип и индекс объекта в таблице.
Поиск таблицы объектов GDI В ходе экспериментов раздела «Расшифровка манипуляторов объектов GDI» мы выяснили, что младшее слово манипулятора объекта GDI (HGDIOBJ) содержит индекс в интервале от 0 до OxSFFF. Возникает предположение, что где-то существует таблица объектов 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-гигабайтном пользовательском пространстве существуют и другие области, недоступные для чтения. Вдобавок запись, чтение и сравнение всех доступных для этого областей памяти потребует огромных расходов дискового пространства и будет происходить очень медленно. Чтобы сканирование пользовательского адресного пространства ограничивалось областями, доступными для чтения, мы воспользуемся функцией Win32 API Virtual Query, которая делит виртуальное адресное пространство на блоки с одинаковыми флагами защиты (например, доступ только для чтения, возможность записи и исполнения). Построение контрольных сумм для областей памяти, доступных для чтения, значительно уменьшает объем памяти, участвующей в сохранении и сравнении. Ниже приведен рабочий алгоритм с функцией, которая сохраняет содержимое блоков памяти и сравнивает их.
158
Глава 3. Внутренние структуры данных GDI/DirectDraw
void shot(vector & 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]) ) printf("Possible Table location Ш1х", start); start +» info.RegionSize:
} void SearchGDIObjectTable(void) vector UserRAM; shot(UserRAM): OeateSolidBnjsh(RGB(Oxll. 0x22. 0x33)); shot(UserRAM):
В наши дни такой неудобный интерфейс недопустим, поэтому в окне программы GDIHandles создается новая страница Locate GDI Handle Table (Поиск таблицы манипуляторов GDI). На ней отображается табличный список, в котором для каждого блока выводится контрольная сумма, начальный адрес, размер, состояние, тип и даже имя модуля и сегмента (если их удается определить). Для таких модулей, как gdi32.dll, имя модуля определяется функцией GetModuleFileName. Для секций РЕ-модуля (например, для секции .text, обычно содержащей исполняемый код) программа определяет имя секции анализом внутренней структуры РЕ-файла. Кроме того, программа пытается идентифицировать блоки с кучами (heaps) процессов и стеками программных потоков. На странице имеется кнопка Query Virtual Memory, позволяющая в любой момент получить «снимок» виртуальной памяти. Запустите программу и щелкните на кнопке Query Virtual Memory; функция Virtual Query делит 2-гигабайтное пространство виртуальных адресов на 100 с лишним блоков. Большинство блоков помечено флагами F (Free, свободная память) и R (Reserved, зарезервированная память). Для нас интерес представляют блоки с флагом С (Commited, актуализированная память).
159
Поиск таблицы объектов GDI
Большинство актуализированных блоков содержит сегменты ЕХЕ-файлов программ и системных DLL — таких, как kernel32.dll, gdi32.dll и даже msidle.dll (трудно сказать, почему msidle.dll отображается в это адресное пространство, но факт остается фактом). Несколько блоков содержат кучи; один блок содержит стек. Для каждого актуализированного блока слева выводится контрольная сумма. Перейдите на страницу Decode GDI Handle, создайте однородную кисть, вернитесь на страницу Locate GDI Handle Table и создайте второй снимок памяти. На этот раз почти для всех актуализированных блоков у нас имеются две контрольные суммы (до и после создания объекта). Некоторые блоки могут иметь только одну контрольную сумму, поскольку у них изменился начальный адрес или размер. На рис. 3.4 показано, как выглядит экран после создания второго снимка виртуальной памяти.
t
#5 •y
Locate GPJ HandleTable,] Decode (3D! Handfc Table | \ ^ 'J
<>i«:ac ' e&c
Ba.se '
y£ есОе
ООЗЬОООО 00002000 003b2000 00006000 OOSbSOOO 00048000 00400000 00001000 00401000 ОООЗаООО 0043bOOO 00004000 0043fOOO 00005000 00444000 00001000 00445000 00005000 0044aOOO 00006000 00450000 00043000 00493000 OOOOdOOO 004aOOOO 00060000 00500000 002aOOOO 007aOOOO 00001000
С Н er R И er
ПП7я 1 ППП ' - '- -'»,'
IT
afcl
1 p
= 69f2 = 39b8 ST 9bl2 SS dd77 SS d044 SS 9dla
I ш~ 1 I 1 1 1 1 1 чрд
69f2 39b8 ЭЬ12 dd77 d044 9dla
yf. a!6a
f49a
У* 7714
3132
= 491b
491b
A-/^>>-*i ' -
Sllllpp'v J*f*#*A«Hi№
;. ",
j Siae
ПППЛ#ПЯП >, , - ,. -t
syTsetel* J. -' . ;-„;-.-' !-;i.-;: v
-,;,:Ьч
Type
j Modulft
С I ewe
_*.(
Handles.exe
С I ewe С I ewe
-text . rdata
С I ewe С I ewe С I ewe
. data
J
. rsrc
F С И го
F С Н er Е Н er С Р rw
jd
'
' : ;
', , , , "JJo*ry Vi
-x«*;.'-> .-- .v-a -;.;.'•.;-•- V?,,-
•- OK -
J
1
C«nc«l
Рис. З.4. Поиск таблицы объектов GDI (отмечены изменившиеся блоки)
Перед теми блоками, у которых контрольные суммы совпадают, появляется зеленый знак равенства, а перед блоками с разными контрольными суммами — предупреждающий красный знак. Блоки с одной контрольной суммой после Двух снимков тоже считаются изменившимися.
162
Глава 3. Внутренние структуры данных GDI/DirectDraw
View или Dumpbin для gdi32.dll наглядно показывает, что ссылки встречаются во множестве функций, в том числе в Sel ectObject и GetObjectType. Однако среди функций, использующих указатели на таблицу объектов GDI, особый интерес вызывает одна недокументированная функция — GdiQueryTable. В высшей степени любопытное имя... Оно подсказывает, что где-то существует какая-то таблица, и при помощи этой функции можно получить информацию о ней. Давайте посмотрим, что же делает эта загадочная функция. // querytab.cpp #define STRICT finclude <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. " MyMessageBoxCNULL. temp. "GdiQueryTableO returns". MB_OK): return 0: }
Функция GdiQueryTable возвращает тот же адрес 0x45000, который был получен экспериментальным путем. После долгих хлопот с поисками в виртуальной памяти мы достигли своей цели — действительно, в Windows NT/2000 существует общесистемная таблица объектов GDI и даже имеется недокументированная функция GdiQueryTable, которая возвращает указатель на эту таблицу. В программах пользовательского режима эта таблица доступна только для чтения. Если на вашем компьютере установлены символические файлы для gdi32.dll, запустите программу Handles.exe в отладочном режиме, переключитесь в режим ассемблерного кода и выберите команду Edit > Go To — на экране появляется диалоговое окно. Введите в нем адрес Ox77f78008 или Ox77f780bc. Отладчик 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
165
Расшифровка таблицы объектов GDI
pGDITable = NULL; Работать с классом KGDITable очень просто. Ниже показано, как получить адрес структуры данных режима ядра для стандартного объекта черного пера. const void * BlackPenpKernel (void) {
KGDITable gditable; return gdi table[GetStockObject(BLACK_PEN)].pKernel:
На рис. 3.6 изображена новая страница свойств, Decode GDI Object Table (Расшифровка таблицы объектов GDI) нашей программы GDIHandles. На этой странице содержимое таблицы объектов GDI выводится в структурированном виде.
// GDITable.h Ipragma once class KGDITable GDITableCell * pGDITable; public: KGDITableO: GDITableCell operator[](HGDIOBJ hHandle) const return pGDITable[ (unsigned) hHandle & OxFFFF ]: GDITableCell operator[](unsigned nlndex) const return pGDITable[ nlndex & OxFFFF ];
}:
}
// GDITable.cpp #define STRICT #include <windows.h> #include finclude "Gditable.h"
1"' Decode SOI Harfe } Locate GDI !Handle Table Qeesd* SDJ Hands Таив 1 t,.';.
.*' f!/ toocess Only '£'••"In
JJo
'" * ."•
0 0 0 0 0 0 0 0 0 0 0. 0
57 4a9 4d4 ',„ , 4d8 '•* 4eS 4f2 ;' 4f4 4' 537 &• S3b д. SaS | ; Sa7
e271ale8 elecSOOS elec44c8 elec29e8 e2307328 e26012c8 e272e008 e2789388 e2713008 e2S89008 e27164c8 e21a4aa8
1
»
JQuery GDI Table
iiiiiiimi
" m« nCoi
»Pro«
iitfppar
nTvp«
ScS ScS
740S 0101 eeOa SeOl 6S04 SelO 9101 760a 4605 3fOa leOa 3310
OOOS 0401 OOOa 0001 0004 0010 0401 OOOa OOOS ffOOa OOOa 0010
ScS ScS ScS
ScS ScS ScS ScS ScS ScS ScS
j ptfeer 0 7aOS70 135e63 7a01dO 7b0018 0 7a03aO 13Se78 0 ISSeSO 13Se88 7bOOOO
Л
«f
—1,
^J
Щ Index: 55, Handle: 740SOOES, Type: OBJ_BITMAP
KGDITable:: KGDITableO typedef unsigned (CALLBACK * ProcO) (void);
.
'> \ * - * ' ~" ' -' ",*"<•' .
'.-T"1 • '" Ж '"'I . .Саойй "" ' '"
ProcO pGdiQueryTable = (ProcO) GetProcAddress( GetModuleHandle("GDI32.dll". "GdiQueryTable"): assert(pGdiQueryTable);
Рис. 3.6. Содержимое таблицы объектов GDI
if ( pGdiQueryTable ) pGDITable - (GDITableCell *) pGdi Query Tabl eO : else
При помощи флажка, находящегося в левом верхнем углу страницы, пользователь выбирает между выводом всех объектов таблицы или только тех объектов, которые были созданы текущим процессом. Попробуйте выделить любой
166
Глава 3. Внутренние структуры данных GDI/DirectDraw
объект GDI в списке; в нижней части страницы появится дополнительная информация о его индексе, значении HGIOBJ и типе объекта. Располагая таким замечательным инструментом для вывода содержимого таблицы объектов 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, — в этом поле хранится простой счетчик выбора, не накладывающий никаких ог-
167
Расшифровка таблицы объектов GDI
раничений. Выбор логического шрифта во втором контексте устройства проходит успешно, а значение поля nCount при этом увеличивается. Многие программисты задают один очевидный вопрос — существует ли в GDI какой-то механизм защиты от удаления объектов, выбранных в контексте устройства? Ответ — да, существует... по крайней мере, для палитр. Как видно из табл. 3.1, первый вызов DeleteObject после выбора палитры в двух DC завершается неудачей, но второй вызов DeleteObject после исключения палитры из обоих DC работает нормально. Впрочем, поле nCount в этой защите не используется. Другие объекты GDI (например, шрифты, растры, кисти и перья) могут быть удалены программистом в любой момент времени, при этом манипулятор выбранного объекта становится недействительным. Трудно сказать, почему Windows не поддерживает единые правила использования nCount, которые бы предотвращали удаление всех выбранных объектов. Таблица 3.1. Использование поля nCount Функция API
Растр (DDB)
Шрифт
Палитра
Create...»
Успех, nCount=0
Успех, nCount=0
Успех, nCount=0
SelectObject(hDCl)
Успех, nCount=l
Успех, nCount=l
Успех, nCount=0
SelectObject(hDC2)
Неудача, nCount=l
Успех, nCount=2
Успех, nCount=0 Неудача
DeleteObject О (De)SelectObject(hOC2)
Неудача, nCount=l
Успех, nCount=l
Успех, nCount=0
(De)SelectObject(hDCl)
Успех, nCount=0
Успех, nCount=0
Успех, nCount=0
DeleteObjectO
Успех
Успех
Успех
Поле nProcess связывает манипулятор GDI с конкретным процессом Если программа пытается воспользоваться манипулятором объекта GDI, относящегося к другому процессу, вызов функции Win32 API обычно завершается неудачей. За этим «волшебством» стоит поле nProcess структуры GdiTableCell. Для стандартных объектов (например, GetStockObject(BLACK_PEN)) поле nProcess равно нулю. Для других объектов GDI, созданных пользовательскими процессами, поле nProcess содержит идентификатор процесса, создавшего объект. Чтобы получить идентификатор текущего процесса, вызовите функцию GetCurrentProcessIdO. GDI проверяет, совпадает ли идентификатор текущего процесса с содержимым поля nProcess объекта GDI; тем самым обеспечивается выполнение требования о том, чтобы манипуляторы объектов не использовались другими процес' сами. Страница Decode GDI Object Table позволяет выбрать между отображением всех объектов GDI и только тех объектов, которые были созданы текущим процессом. Если щелкнуть в строке таблицы, в нижней части страницы выводится
168
Глава 3. Внутренние структуры данных GDI/DirectDraw
подробная информация о выбранном объекте — в том числе и информация, возвращаемая при вызове GetObject. Но если переключиться в режим вывода всех объектов GDI и щелкнуть на объекте, созданным другим процессом, вызов GetObject завершается неудачей, а программа выводит ошибку «Invalid Object». Согласно документации Microsoft, при завершении процесса освобождаются все созданные им объекты GDI. Вас когда-нибудь интересовало, как это делается? GDI просто перебирает все записи в таблице объектов GDI и удаляет все объекты с идентификатором текущего процесса.
Расшифровка таблицы объектов GDI
169
вы удаляете объект GDI и создаете новый объект в том же элементе таблицы, даже при совпадении типов объектов манипуляторы будут отличаться, поскольку значение счетчика повторного использования увеличилось. Все вызовы функций, в которых присутствует исходный манипулятор, завершатся неудачей. Применение счетчика продемонстрировано в разделе «Структуры данных пользовательского режима» (см. ниже).
пТуре: внутренний тип объекта nLlpper: дополнительная проверка Поле nUpper в таблице объектов GDI содержит точную копию двух старших байтов 4-байтового манипулятора — эта относительно малая избыточность обеспечивает дополнительную проверку манипуляторов объектов GDI. Предположим, вы создали шрифт; функция CreateFont возвращает Ox9dOa047f. Новый объект соответствует элементу таблицы с индексом Ox047f, у которого поле nUpper равно Ox9dOa. Теперь какая-нибудь другая часть программы удаляет шрифт, не зная, что он используется, в результате запись Ox047f освобождается; затем программа создает другой шрифт. Допустим, GDI почему-либо решает задействовать для нового объекта GDI элемент с индексом Ox047f и назначает ему манипулятор Ox9eOa047f. Если первая часть программы попытается воспользоваться манипулятором Ox9dOa047f, вызовы функций Win32 GDI завершатся неудачей — GDI обнаруживает, что Ox9dOa не совпадает с новым значением nUpper элемента Ox047f, которое теперь равно ОхЗреОа. Попробуйте изменить старший байт манипулятора GDI, сохранив три остальных байта, в которых хранится информация о типе объекта; вы увидите, что вызовы GetObject и GetObjectType завершаются неудачей. Хранение старшего слова манипулятора в таблице находит и другие применения. Если вам известен только индекс манипулятора в таблице GDI, вы сможете восстановить весь манипулятор, прочитав nUpper из таблицы и объединив эти два значения. Например, эта возможность используется при реализации 16разрядной поддержки GDI в Windows NT. Вспомните: в 16-разрядном интерфейсе GDI используется 16-разрядный манипулятор HGDIOBJ, фактически являющийся индексом. Чтобы 16-разрядная поддержка GDI работала в Windows NT, вызов необходимо переадресовать 32-разрядному интерфейсу GDI, работающему с полноценными 32-разрядными манипуляторами. При анализе структуры манипуляторов GDI в разделе «Поиск таблицы объектов GDI» нерасшифрованными остались лишь старшие 8 бит. Дополнительные эксперименты показывают, что в них хранится счетчик повторного использования — еще одно простое средство проверки манипуляторов. У каждого элемента таблицы объектов GDI первоначальное значение счетчика равно 0. Когда в элемент таблицы заносится информация о новом объекте GDI, его счетчик повторного использования увеличивается (когда значение достигает 255, счетчик снова сбрасывается в 0). Таким образом, когда элемент задействуется впервые, его счетчик повторного использования равен 0x01; это относится ко всем стандартным объектам GDI, которые создаются один раз и никогда не удаляются. Если
В процессе анализа структуры манипуляторов GDI (см. раздел «Расшифровка манипуляторов объектов GDI») мы выяснили, что в каждом манипуляторе присутствует 7-разрядная информация о типе объекта. Эта информация, расширенная до двух байт, имеется и в таблице объектов GDI. Младший байт пТуре обычно содержит те же 7 бит типа, что и HGDIOBJ, а старший байт обычно равен нулю. В поле пТуре манипулятор расширенного метафайла интерпретируется как манипулятор контекста устройства, а манипулятор расширенного пера — как манипулятор кисти. Для некоторых объектов старший байт пТуре определяет подтип объекта — скажем, подтип «совместимый контекст» (memory context) для типа «контекст устройства». Вот что мы знаем об этом слове внутреннего типа объекта: typedef enum { gdi_int_objtypew_dc = 0x0001. gdi_int_objtypew_memdc = 0x0401, // He все совместимые контексты gdi_int_objtypew_region = 0x0004. gdi_int_objtypew_bitmap - 0x0004. gdi_int_objtypew_palette = 0x0008. gdi_int_objtypew_font = OxOOOa. gdi_1nt_objtypew_brush = 0x0010. gdi_int_objtypew_enhmetafile - 0x0001, // Как для ОС gdi_int_objtypew_pen = 0x0030. gdi_intjDbjtypew_extpen = 0x0010. // Как для кисти
pUser: указатель на структуру данных пользовательского режима Вероятно, вы обратили внимание на симметричное расположение полей в структуре GdiTableCell: она начинается с указателя, затем следуют четыре 16-разрядных слова, а затем следует другой указатель pUser. Обычно указатель pUser равен NULL — исключение составляют некоторые типы объектов GDI. Если указатель отличен от NULL, он принимает такие значения, ' как 0x001420с8 или 0x790320. Как нетрудно убедиться, эти значения соответствуют действительным адресами блоков памяти, доступным для чтения и записи. Структуры данных объектов GDI более подробно рассматриваются в следующем разделе.
Глава 3. Внутренние структуры данных GDI/DirectDraw
170
Структуры данных юльзовательского режима Как было показано в предыдущем разделе, каждому объекту GDI соответствует элемент глобальной таблицы объектов GDI, в котором хранится указатель с именем pUser. Для большинства объектов GDI указатель pUser равен NULL (то есть не используется). Тем не менее для объектов кистей, регионов, шрифтов и контекстов устройств поля pUser в таблице объектов GDI ссылаются на довольно интересные структуры данных в адресном пространстве пользовательского режима. Этой теме и посвящен данный раздел.
Структура данных пользовательского режима для кистей: оптимизация создания однородных кистей Для однородных кистей указатель pUser ссылается на блок из 24 байт, в котором первые 12 байт содержат копию структуры LOGBRUSH. Если кисть является однородной, она обладает лишь одним атрибутом — цветом. Для остальных типов кистей 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 имеют одинаковый индекс OxSell. Их поля pKernel и pUser совпадают, но поля 1 bCol or в структуре LOGBRUSH, на которую ссылается pUser, различаются. Таблица 3.2. Повторное использование манипуляторов однородных кистей
Номер
Манипулятор
IbColor
pKernel
pUser
1
ОхабЮЗеИ
0x000000
Oxel25d710
0x870048
2
ОхЗсЮЗШ
0x202020
Oxel25d878
0x870060
3
Оха7103е11
0x404040
Oxel25d710
0x870048
4
Ох941031Ье
0x606060
Oxcl25da70
0x870078
5
ОхЗсИОЗШ
0x808080
Oxel25d878
0x870060
6
Оха8103е11
OxaOaOaO
Oxcl25d710
0x870048
7
Ox5fl03f49
OxcOcOcO
Oxel25d908
0x870090
8
Ox95l031be
OxeOeOcO
Oxel25da70
0x870078
Таблица 3.2 также иллюстрирует то, что говорилось выше о счетчике повторного использования (старшие 8 бит манипулятора GDI). Манипуляторы 1, 3 и_6 в табл. 3.2 создаются в одном и том же элементе таблицы GDI OxSell; все они соответствуют объекту кисти (0x10), однако их счетчики повторного использования отличаются на 1.
Структура данных пользовательского режима для регионов: оптимизация прямоугольных регионов Объекты регионов создаются такими функциями, как CreateRectRgn и ExtCreateRgn. По аналогии с кистями, поле pUser используется для простейшего случая — прямоугольных регионов. Размер блока данных прямоугольного региона, адресуемого указателем pUser, равен 24 байтам. Смысл первых двух двойных слов в этом блоке неизвестен, а остальные 16 байт образуют структуру RECT: typedef struct { DWORD dwUnknownl: // = 17 DWORD dwUnknownS: // - 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 не пусто (то есть в памяти была создана структура UserDataJtectRgn), и присваивает координатам заданные значения. Таким образом, регионы как объекты GDI являются изменяемыми (mutable).
Структура данных пользовательского режима для шрифтов: таблица значений ширины Для шрифтов в Windows GDI определяется больше структур данных, чем для любого другого объекта: LOGFONT, TEXTMETRIC, PANOSE, ABC, GLYPHSET и т. д. Однако в таблице объектов GDI не обнаруживается ни малейшего следа этих структур. Структура данных пользовательского режима для манипулятора шрифта устроена очень просто: typedef struct {
DWORD dwllnknown; // = О void *pCharWidthData; / / = 1 , 2 } UserDataJont;
Первое поле UserData_Font всегда равно нулю. Второе поле обычно равно нулю и изменяется только после вызова таких функций, как GetCharWidth. Функция GetCharWidth заполняет целочисленный массив сведениями о ширине символов, принадлежащих заданному интервалу. После вызова GetCharWidth указатель pCharWidthData указывает на структуру данных, выделенную из системной кучи, которая в основном содержит кэшируемую таблицу значений ширины. Перед нами еще один пример того, как GDI прикладывает дополнительные усилия для оптимизации быстродействия. Получив значение pCharUidthData (например, Ох1456ЬО), вы можете без особого труда вычислить, где находится этот адрес. На странице Locate GDI Object Table программы GDIHandles отображается список всех блоков памяти в пользовательском адресном пространстве. Из этого списка видно, что адрес Oxl456bO принадлежит первой куче, то есть куче процесса по умолчанию. Если дважды щелкнуть на строке первой кучи, на экране появляется окно дампа памяти (см. рис. 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 { }
ULONG ull: ULONG u!2: FLOATOBJ;
typedef struct {
}
FLOATOBJ efMll: FLOATOBJ efM12: FLOATOBJ efM21; FLOATOBJ efM22; FLOATOBJ efDx: FLOATOBJ efDy: int fxDx; i nt f xDy; long flAccel; MATRIX;
// Windows NT 4.0: 0x190 байт // Windows 2000 : OxlCS байт typedef struct void * ULONG HBRUSH HPEN
pvLDC; ul Dirty; hbrush; i hpen;
// 000
COLORREF ULONG
crBackgroundClr; ulBackgroundClr:
// 010
174
Глава З. Внутренние структуры данных GDI/DirectDraw
COLORREF ULONG
crForegroundClr, ulForegroundClr,-
#if (_WIN32_WINNT >= 0x0500) unsigned f20[4]; lendi f
// 020
int int BYTE BYTE BYTE BYTE
iCS_CP; IGraphicsMode; JROP2; jBkMode: jFillMode; jStretchBltMode:
// 030
POINT POINTFX long
ptlCurrent; ptfxCurrent: IBkMode:
// 03C // 044 // 04C
long long
IFillMode; IStretchBHMode;
// 050
#if (_WIN32_WINNT >- 0x0500) long flFontMapper; long UcmMode: unsigned hcmXform; HCOLORSPACE hColorSpace: unsigned f68: unsigned IcmBrushColor; unsigned IcmPenColor; unsigned f74; fendif
// 038
// 058 // 060
// 070
long long long long long long
fl Text Align: ITextAlign; ITextExtra: IRelAbs; IBreakExtra; cBreak:
// 078
HFONT MATRIX MATRIX MATRIX
MfntNew; mxWorldToDevice: mxDeviceToWorld: mxWorldToPage;
// // // //
unsigned int
f!48[8]: iMapMode:
// 080
090 094 ODO IOC
// 148 // 168
#if (_WIN32_WINNT >- 0x0500) DWORD dwlayout; long IWindowOrgx; fendif
// 16c // 170
ptlWindowOrg: szlWindowExt;
// 174 // 17 с
POINT SIZE
\
175
Структуры данных пользовательского режима
POINT SIZE
ptlViewportOrg: szlViewportExt;
// 184 // 18c
long SIZE SIZE POINT
flXform: szlVirtualDevicePixel; szlVirtualDeviceMm; ptlBrushOrigin;
// // // //
flbO[2]: VisRectRegion;
// IbO // Ib8
unsigned RECT } DCAttr:
194 198 laO Ia8
Смысл большей части полей структуры DC_ATTR понятен без объяснений. При выборе в контексте устройства объектов GDI (таких, как кисти, перья и шрифты) их манипуляторы сохраняются в соответствующих атрибутах. В структуре не видно и следа присутствия аппаратно-зависимых растров, палитр и регионов. Скалярные атрибуты (цвет текста, цвет фона, графический режим, бинарная растровая операция, режим блиттинга с растяжением, тип выравнивания текста и режим отображения) также хранятся в этой структуре. Некоторые атрибуты (цвет и выравнивание текста) по каким-то неизвестным причинам хранятся в двух экземплярах. В Windows NT/2000 GDI поддерживаются мировые преобразования между логической и физической системами координат. Путем мировых преобразований выполняются трансформации переноса, масштабирования, поворота и сдвига. Мировое преобразование описывается вещественной матрицей XFORM 2 x 3 , передаваемой при вызове функции SetWorldTransform. Матрица XFORM не сохраняется в структуре данных DC_ATTR непосредственно в вещественном формате. XFORM состоит из шести вещественных чисел с одинарной точностью, описывающих линейное преобразование на плоскости. Известно, что стандартное представление вещественных чисел с одинарной точностью в формате IEEE занимает 4 байта, тогда как представление числа с двойной точностью содержит 8 байт. Однако в представлении XFORM в DC_ATTR не используется ни один из этих двух форматов. XFORM представляется структурой MATRIX, состоящей из шести пар DWORD и трех 32-разрядных чисел. Ближайшим аналогом этих пар DWORD является структура FLOATOBJ, определяемая в WinNT DDK. Вещественное число с одинарной точностью в формате IEEE состоит из 32 бит. Оно содержит один знаковый бит, 8-разрядную экспоненту со смещением 127 и 23-разрядную мантиссу с одним скрытым битом, который всегда равен 1. Вещественное число вычисляется по формуле знак * 2А(экспонента-127) * (1«24 + мантисса)2л24. Например, число 1.0 хранится в виде ОхЗЕ800000. Знаковый бит соответствует положительному числу, экспонента равна 127, а все биты мантиссы равны нулю. В соответствии с приведенной выше формулой мы получаем: 1 * 24127-127) * (1 « 24 + 0)/2"24 = 1
Microsoft имитирует вещественные вычисления с помощью целочисленной арифметики с «высоким быстродействием» и точностью. Высокая точность означает длинную мантиссу, а быстродействие достигается конструированием формата, из которого в процессе вычислений легко выделяются компоненты числа.
176
Глава 3. Внутренние структуры данных GDI/DirectDraw
Для представления вещественных чисел в GDI Microsoft использует структуру FLOATOBJ. Эта структура делится на два 32-разрядных числа; старшее двойное слово (и12) определяет экспоненту, а младшее (ull) — мантиссу в сумме со знаковым битом. В отличие от формата IEEE скрытые биты или смещения в FLOATOBJ не используются. Преобразование структуры FLOATOBJ в вещественное число выполняется очень просто: double FLOATOBJ2Double(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. Помимо представления XFORM в формате FLOATOBJ, GDI также хранит в целочисленных полях XFORM_eDxI и XFORM_eDyI округленные версии смещений (eDx и eDy). В структуре DC_ATTR содержится немало полей, смысл которых так и остается для нас загадкой. В то же время многие функции контекстов устройств не приводят к непосредственным изменениям DC_ATTR; например, вызовы SelectPalette, SetMiterLimit и SetArtDirection не изменяют содержимого 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, достаточно вызвать функцию CreateFile, ReadFile, W r i t e F i l e или менее известную функцию Dev-iceloControl. Например, при помощи файловых операций можно работать с драйверами последовательного порта, параллельного порта и файловой системы. Кроме того, в 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 PUNICODE_STRING RegistryPath) • Функция DriverEntry предназначена для тех же целей, что и _DllMainStartCRTStartup, точка входа в DLL пользовательского режима. Однако в отличие от DLL пользовательского режима, драйверы режима ядра обычно не экспортируют функций. Вместо этого DriverEntry сообщает системе адреса функций, которые должны экспортироваться системными средствами, с использованием структуры DRIVERJ3BJECT. Простой драйвер ввода-вывода может ограничиться реализацией минимального подмножества функций. Например, приведенный ниже фрагмент DriverEntry экспортирует две функции. Функция Drvllnload вызывается при выгрузке драйвера. Функция DrvDispatch вызывается при создании, закрытии и вызове DeviceloControl. Driver->DriverUnload = 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 — «перископа», через который мы будем наблюдать за работой ядра. finclude "kernelopt.h" finclude "periscope.h" const WCHAR DeviceName[] const WCHAR DeviceLinkt]
L"\\Device\\Periscope": L"\\DosDevices\\PERISCOPE":
// Обработка CreateFile. CloseHandle NTSTATUS DrvCreateClosedN PDEVICEJ3BJECT DeviceObject. IN PIRP Irp) Irp->IoStatus. Information 0; Irp->IoStatus. Status = STATUS_SUCCESS: loCompleteRequestdrp.
IO_NO_INCREMENT):
return STATUS SUCCESS: // Обработка DeviceloControl NTSTATUS DrvDeviceControKIN PDEVICEJ3BOECT DeviceObject. IN PIRP Irp)
Обращение к адресному пространству режима ядра
NTSTATUS nStatus = STATUS_INVALID_PARAMETER; Irp->IoStatus.Information - 0; // Получить указатель на текущую позицию стека, // в которой находятся коды функций и параметры PIO_STACK_LOCATION irpStack loGetCurrentlrpStackLocation (Irp): unsigned * ioBuffer = (unsigned *) Irp->AssociatedIrp.SystemBuffer; if ( (irpStack->Parameters.DeviceloControl.loControlCode — IOCTL_PERISCOPE) && (ioBuffer!"NULL) && (irpStack->Parameters.DeviceloControl. InputBufferLength >- 8) ) unsigned leng - ioBuffer[l]; if ( irpStack->Parameters.DeviceloControl. OutputBufferLength >- leng ) Irp->IoStatus.Information = leng: nStatus = STATUS_SUCCESS;
_try memcpy(ioBuffer. (void *) ioBuffer[0]. leng): _except ( EXCEPTIONJXECUTE_HANDLER ) Irp->IoStatus.Information = 0: nStatus = STATUS INVALID PARAMETER:
Irp->loStatus.Status = nStatus: loCompleteRequestdrp. IO_NO_INCREMENT): return nStatus:
// Обработка выгрузки драйвера void DrvUnloaddN PDRIVERJBJECT DriverObject) UNICODE_STRING deviceLinkUnicodeString; RtlInitUnicodeString(&deviceLinkUnicodeString. DeviceLink); loDeleteSymboli cLi nk(Sdevi ceLi nkUni codeStri ng): IoDeleteDevice(DhverObject->DeviceObject);
179
180
Глава 3. Внутренние структуры данных GDI/DirectDraw
// Инициализационная точка входа // для устанавливаемых (installable) драйверов NTSTATUS DriverEntrydN PDRIVERJDBJECT 'Driver. IN PUNICODE_STRING RegistryPath) UNICODE_STRING deviceNameUnicodeString; RtllnitUnicodeStringC &deviceNameUnicodeString. DeviceName ); // Создать устройство PDEVICE_OBJECT deviceObject = NULL; NTSTATUS ntStatus = loCreateDevice (Driver, sizeof(KDeviceExtension). & deviceNameUnicodeString. FILE_DEVICE_PERISCOPE. 0. TRUE. & deviceObject); if ( NT_SUCCESS(ntStatus) ) // Создать символическую ссылку, по которой приложения Win32 // будут получать доступ к драйверу/устройству UNICODE_STRING deviceLinkUnicodeString; RtllnitUnicodeString (SdeviceLinkUnicodeSthng, DeviceLink): ntStatus = IoCreateSymbolicLink( &deviceLinkUnicodeString. Sdevi ceNameUni codeSth ng); // Создать диспетчерскую таблицу драйвера if ( NT_SUCCESS(ntStatus) ) Driver->DriverUnload = DrvUnload; Driver->MajorFunction[IRP_MJ_CREATE] = DrvCreateClose; Driver->MajorFunction[IRP_MJ_CLOSE] = DrvCreateClose; Driver->MajorFunction[IRP MJ DEVICE CONTROL] = DrvDeviceControl:
if ( !NT_SUCCESS(ntStatus) && deviceObject!=NULL ) loDeleteDevice(deviceObject): return ntStatus:
Основная часть кода составляет «скелет» базового драйвера режима ядра. Каждому устройству, поддерживаемому драйвером, должно соответствовать некоторое имя; в нашем примере используется имя Periscope. Это имя заносится в каталог Device пространства имен объектов Windows. В DDK входит небольшая утилита objdir, которая, помимо прочего, выводит список драйверов устройств, установленных в вашей системе. Функция DrvCreateClose обрабатывает создание и закрытие экземпляров устройства, инициируемые при вызове функций API CreateFile и CloseHandle. Функция DrvDeviceControl решает главную задачу — чтение блока памяти при вызове DeviceloControl в пользовательском приложении. Весь «интересный» код сосредоточен в нескольких строках функции DrvDeviceControl . Убедившись в том, что при вызове был передан правильный код, буфер
Обращение к адресному пространству режима ядра
181
ввода-вывода не пуст, а длина переданного параметра не менее 8 байт, программа получает начальный адрес и размер читаемого блока, после чего просто копирует запрашиваемые данные в выходной буфер. Обратите внимание: процесс чтения защищен механизмом обработки исключений — на тот случай, если какие-нибудь адреса окажутся недействительными. Функция DrvUnload обрабатывает выгрузку драйвера, а функция DriverEntry является главной точкой входа в драйвер. Чтобы приведенный код правильно откомпилировался в драйвер режима ядра, следует изменить некоторые параметры компилятора и компоновщика, используемые по умолчанию. Например, компилятор должен придерживаться соглашения о вызове stdcal 1 вместо принятого по умолчанию соглашения cdecl. Флаг подсистемы Windows в драйвере должен быть равен «native,4.00» вместо Windows GUI. Эти изменения обеспечиваются включением соответствующих параметров в файл проекта и заголовочный файл kernelopth. При правильных настройках драйвер режима ядра будет успешно откомпилирован в Visual C++. Программа Periscope компилируется в крошечную библиотеку DLL режима ядра, Periscope.sys. В дальнейших программах предполагается, что эта DLL скопирована в корневой каталог диска С:. Драйвер написан для систем Windows NT 4.0/Windows 2000 и был в них протестирован. Динамическая загрузка, запуск, остановка и выгрузка драйверов устройств режима ядра хорошо поддерживаются на уровне Win32 API. Для выполнения этих функций мы определили класс KDevice C++. Конструктор KDevice устанавливает соединение с диспетчером управления службами, вызывая функцию OpenSCMManager. Открытая функция KDevice::Load загружает драйвер функцией CreateService, запускает драйвер функцией StartService, после чего получает манипулятор объекта устройства при помощи функции CreateFi I e. В качестве имени файла при вызове CreateFile указывается строка \\.\Periscope — стандартное обозначение открываемого устройства в Windows. После этого можно вызывать функцию DeviceloControl для полученного манипулятора и общаться с драйвером Periscope, работающем в режиме ядра. KDevi се представляет собой обобщенный класс для работы с драйверами устройств Windows NT/2000. С таким же успехом можно воспользоваться им по отношению к другому драйверу. Класс KDevice устроен просто, поэтому мы не будем рассматривать его полный исходный текст и сразу перейдем к небольшой тестовой программе для работы с драйвером Periscope. // TestPeriscope.cpp fdefine STRICT finclude <windows.h> finclude <winioctl .h> finclude finclude "device.h" finclude "..\Periscope\\Periscope.h" class KPeriscopedient : public KDevice { public:
KPerlscopeClient(const TCHAR * DeviceName) : KDevice(DeviceName)
182
Глава 3. Внутренние структуры данных GDI/DirectDraw
bool ReacKvoid * dst. const void * src, unsigned Ten): bool KPeri scoped lent: :Read(void * dst, const void * src. unsigned Ten) {
unsigned cmd[2] - { (unsigned) src. len }: unsigned long dwRead;
}
return IoControl(IOCTL_PERISCOPE, and. sizeof(cmd). dst. len. SdwRead) && (dwRead==len);
int WINAPI WinMain(HINSTANCE. HINSTANCE. LPSTR. int) { KPeri scoped i ent scope(" Peri Scope"):
183
WinDbg и расширение отладчика GDI
шаговом режиме пройдите от функции WinMain в TestPeriscope.cpp до функции DrvDeviceControl в Periscope.срр. Вы придете к состоянию стека, приведенному в табл. 3.3. Обратите внимание — между входом в функцию DeviceloControl в kernel32.dll и достижением DrvDeviceControl в Periscope работает системный код Windows, поэтому вам придется пройти через ассемблер. Также следует заметить, что ntdll.dll является библиотекой DLL пользовательского режима, а для переключения процессора в режим адресации ядра вызывается прерывание 2Eh. Другими словами, прерывание 2 Eh обслуживается кодом режима ядра. Таблица 3.3. Состояние стека при переходе от программы пользовательского режима к драйверу режима ядра Уровень
Функция
Модуль/файл
1
WinMain
TestPeriscope.cpp
if С scope.Load("c:\\periscope.sys")==ERROR_SUCCESS ) { unsigned char buf[256];
2
KPeri scoped lent:: Read
TestPeriscope.cpp
3
KDevice: iloControl
Device.h
4
DeviceloControl
Kernel32.dll
scope.Read(buf, (void *) OxaOOOOOAE. sizeof(buf)):
5
NTDeviceloControlFile
Ntdll.dll
scope.CloseO;
6
Int 2Eh
MessageBox(NULL. (char *) buf, "Mem[0xa000004e]", MB_OK):
7
NTDeviceloControlFile
Ntoskrnl.exe
8
lofCallDriver
Ntoskrnl.exe
9
DrvDeviceControl
Periscope. cpp
} else MessageBoxtNULL, full name. "Unable to load c:\\periscope.sys". NULL. MB_OK); return 0: } Программа создает класс KPeriscopeCllent, производный от KDevice, и включает в него дополнительный метод Read — оболочку для вызова DeviceloControl. Этот метод приказывает Periscope прочитать блок памяти при помощи специального управляющего кода IOCTL_PERISCOPE. Основная программа создает экземпляр KPeri scoped lent в стеке, загружает драйвер режима ядра и читает 256 байт, начиная с адреса Оха000004е. Адрес принадлежит графическому механизму win.32k.sys, обеспечивающему работу gdi32.dll и user32.dll. Базовый адрес win32k.sys равен ОхаООООООО. При чтении по смещению Ох4е от начала модуля Win32 обычно возвращается предупреждение, выводимое в DOS при выполнении Windows-программы: «This program cannot be run in DOS mode $». Если вы впервые работаете с текстом простого драйвера режима ядра Windows NT/2000 и элементарными средствами для работы с драйвером из пользовательского режима, вероятно, у вас возникнет искушение выполнить код в пошаговом режиме и посмотреть, как же все в действительности рзаботает. Вооружитесь отладчиком уровня ядра (таким, как Softlce/W), загрузите необходимые символы или таблицу экспортируемых функций для системных DLL, в по-
•
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
Глава З. Внутренние структуры данных 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
GDI, режим ядра
Kdextx86.dll
Исполнительная часть/HAL, режим ядра
Ntsdexts.dll
Стандартное расширение пользовательского режима
Rpcexts.dll
Удаленный вызов процедур (RPC)
Userexts.dll
USER, пользовательский режим
Userkdx.dll
USER, режим ядра
Vdmexts.dll
NT DOS/WOW (Window in Window)
WinDbg и расширение отладчика GDI
185
Интерфейс WinDbg с расширениями отладчика организован очень просто, он полностью определяется в заголовочном файле DDK WDBGEXTS.h. Расширения отладчика должны экспортировать три обязательные функции CheckVersion, ExtensionApiExtension и WinDbgExtensionDllInit, выполняющие проверку версии и инициализацию. Функция CheckVersion убеждается в том, что версия ОС на ведомом компьютере совпадает с версией, для которой написано расширение. Не надейтесь получить правильные результаты при загрузке FREE-версии DLL расширения для отладки в CHECKED-версии ОС. Функция ExtensionApi Version проверяет, используют ли DLL расширения и хост WinDbg одну и ту же версию API. Функция WinDbgExtensionDTlInit, самая важная из этих трех функций, передает структуру WINDBG_EXTENSION_APIS от WinDbg к DLL расширения. В настоящее время структура WINDBG_EXTENSION_APIS определяет 11 функций косвенного вызова, которые могут вызываться из DLL расширения. В реализации функций косвенного вызова задействованы DLL imagehlp, отладочные файлы символических имен и ведомая система, подключенная через нуль-кабель. typedef struct _WINDBG_EXTENSION_APIS { ULONG nSize: PWINDBG_OUTPUT_ROUTINE IpOutputRoutine: PWINDBG_GET_EXPRESSION 1pGetExpressi onRouti ne: PWINDBG_GET_SYMBOL 1pGetSymbolRoutine; PWINDBG_DISASM IpDisasmRoutine; PWINDBG_CHECK_CONTROL_C IpCheckControlCRoutine: PWINDBG_READ_PROCESS_MEMORY_ROUTINE IpReadProcessMemoryRoutlne: PWINDBG_WRITE_PROCESS_MEMORY_ROUTINE 1pWri teProcessMemoryRoutine; PWINDBG_GET_THREAD_CONTEXT_ROUTINE 1pGetThreadContextRouti ne; PWINDBG_SET_THREAD_CONTEXT_ROUTINE 1pSetThreadContextRoutlne; PWINDBG_IOCTL_ROUTINE IploctlRoutine: PWINDBG STACKTRACE ROUTINE IpStackTraceRoutine: } WINDBG_EXTENSION_APIS. *PWINOBG_EXTENSION_APIS: Как видно из этого определения, DLL расширения могут обращаться к управляющей программе WinDbg с запросами на вывод строки, вычисление выражения, поиск символического имени, дезассемблирование кода, проверку аварийного завершения, чтение/запись содержимого памяти, чтение/запись контекста потока, вызов функций ввода-вывода и даже трассировку стека. Другими словами, вся информация об устройстве внутренних структур данных операционной системы находится у DLL расширения, a WinDbg обеспечивает интерфейс пользователя с отлаживаемой системой. Помимо трех обязательных функций DLL расширения может экспортировать и другие функции, которые могут использоваться в качестве команд в командной строке WnDbg. Имя экспортируемой функции совпадает с именем команды. Все экспортируемые функции имеют одинаковый прототип, определяемый следующим макросом: #define DECLARE_API(s)s CPPMOD VOID sC HANDLE hCurrentProcess, HANDLE hCurrentThread. ULONG dwCurrentPc.
Глава 3. Внутренние структуры данных GDI/DirectDraw
186 dwProcessor, args
ULONG PCSTR
Поскольку книга посвящена программированию графики в Windows NT/2000, нас в первую очередь интересует Gdikdx.dll — DLL расширения для отладки GDI в режиме ядра. После настройки WinDbg расширение Gdikdx.dll загружается командой 1 oad в командной строке WinDbg: > load gdikdx.dll Debugger extension library [...\system32\gdikdx] loaded Все команды расширений отладчика начинаются с символа !, чтобы их можно было отличить от стандартных команд WinDbg. Команда he! p выводит краткую сводку десятков команд, поддерживаемых расширением отладчика GDI. Как и следовало ожидать от внутреннего отладочного инструмента, для gdikdx.dll эта команда выводит устаревшую информацию. В частности, команды brush, cliserv, gdicall и proxymsg приведены в справке, но в действительности не поддерживаются; команда di fi была заменена командой 1 f i, а новые команды dbl i и ddib вообще не упоминаются. К счастью, у каждой команды имеется параметр -?, при помощи которого можно получить обновленную информацию. Обратившись к списку функций, экспортируемых gdikdx.dll, вы найдете имена новых команд, отсутствующие в справке. Команды расширения отладчика для отладки GDI в режиме ядра перечислены в табл. 3.5. Таблица 3.5. Команды расширения отладчика для GDI в режиме ядра Команда
Параметры
Использование
dumphmgr
[?]
Сводка объектов GDI по типам
dumpobj
[?] [-p pid] [-1] [-s] object_type
Все объекты GDI заданного типа Объекты диспетчера манипуляторов DirectDraw
dumpdd dumpddobj
[-P pid] [type]
Объекты DirectDraw заданного типа
dh
[-?] object handle
Запись HMGR для объекта GDI
dht
[-?] object handle
Тип/уникальность/индекс для манипулятора GDI
ddib
[-?] [-h [-b [-p
Дамп растра
[-1 LPBITMAPINFO] [-w Width] Height] [-f filename] Bits] [-y Byte_Width] palbits palsize] pbits
dbli
[-?] BLTINFO *
ddc
[-?adeghrstuvx] hdc
Контекст устройства
dpdev
[-TabdfghmnprRw] ppdev
Объект физического устройства
dldev
[-?] C-f] C-F#] Idev
Объект логического устройства
187
WinDbg и расширение отладчика GDI Команда
Параметры
Использование
dgdev
[-?m] dgdevptr
GRAPHICS_DEVICE
dco
[-?] clipobj
CLIPOBJ
dpo
[-?] pathobj
PATHOBJ
dppal
[-?] pal
EPALOBJ
dpw32
[-?] [process]
dpbrush
[-?] pbrush | hbrush
HBRUSH или PBRUSH
dfloat
[-?] [-1 num] Value
Дамп вещественного числа или массива в формате IEEE
ebrush
[-?] pbrush | hbrush
HBRUSH
dpso
[•?] [-f filename] surfobj
Структура SURFACE из SURFOBJ
dblt
[-?] BURECORD_PTR
BLTRECORD
dr
[-?] hrgn|prgn
REGION
cr
[-?] hrgn|prgn
Проверка REGION
dddsurface
[•?haruln]ddsurface
EDDJURFACE
dddlocal
[-?ha]
EDD_DIRECTDRAW_LOCAL
dddglobal
[-?ha]
EDD_DIRECTDRAW_GLOBAL
dsprite
[-?ha]
SPRITE
dspri testate
[•?ha]
SPRITE_STATE
rgnl og
[-?] nnn[sl][s2][s3][s4]
Последние nnn записей rgnlog
stats
[•?]
Накапливаемая статистика
verifier
[-?hds]
Вывод информации верификатора
hdc
[-?gltf] handle
del
[•?] DCLEVEL*
dca
[•?] DC_ATTR*
ca
[•?]COLORADJUSTMENT*
mix
[-?]MATRIX*
la
[•?]LINEATTRS*
ef
[-?]address [count]
'dteb dpeb
или PBRUSH
Вывод структуры данных HDC пользовательского режима Вывод MATRIX из DC_ATTR
[•?] TEB
Вывод команд из очереди ТЕВ
[-?] [-w]
Вывод каптированных объектов РЕВ Продолжение
188
Глава 3. Внутренние структуры данных GDI/DirectDraw
Таблица 3.5. Продолжение Команда с
Параметры
Использование
[-?] address [count]
хо
[-?] EXFORMOBJ*
Шрифтовые расширения tstats [-?] [1..50] gs
[-?] FD_GLYPHSET*
gdata
[-?] GLYPHDATA *elf
tm
[•?] TEXTETRICW*
trnwi
[-?]TMW_INTERNAL*
fo
[-?acfhwxy] FONTOBJ*
pfe
[-?] PFE*
pff
[-?] PFF*
pft
[-?] PFT*
stro
[-?phe] STROBJ*
gb
[-?hmg] GLYPHBITS*
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 обладает полным доступом к отладочной информации ведомого компьютера.
gdf
[-?] GLYPHDEF*
gp
[-?] GLYPHPOS*
cache
[-?] CACHE*
fh
[-?] FONTHASH*
hb
[-?] HACHBUCKET*
fv
[-?] FILEVIEW*
ffv
[-?] FONTFILEVIEW*
helf
[-?] font handle
ifi
[-?] IFIMETRICS*
pubft
[-?]
Дамп всех открытых шрифтов
pvtft
[-?]
Дамп всех закрытых или внедренных шрифтов
devft
[-?]
Дамп всех шрифтов устройств
dispcache
[-?]
Дамп кэша глифов для вывода структуры PDEV
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
WinDbg и расширение отладчика GDI
за ними следуют пять функций, соответствующие пяти функциям косвенного вызова, которые мы собираемся реализовать. В следующем листинге приведена реализация функций 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; sscanftexpr. "%x". & number): return number;
public: KImageModule KPeriscopeClient HWND HWND HANDLE
}
pWin32k; pScope; hwndOutput: hwndLog: hProcess;
if С pWin32k ) { const IMAGEHLP_SYMBOL * pis; if ( expr[0]=='&' ) // Пропустить первый & pis = pWin32k->ImageGetSymbol(expr+l); else pis = pWin32k->ImageGetSymbol(expr);
KHostО pWin32k pScope hwndOutput hwndLog hProcess
= = = =
NULL: NULL: NULL: NULL: NULL:
void WndOutpuUHWND hWnd. const char void Log(const char * format. ...):
if ( pis ) { Log("GetExpressionUs)-*081x\n", expr, pis->Address);
format, vajist argptr);
void ExtOutput(const char * format, ...): unsigned ExtGetExpression(const char * expr): bool ExtCheckControlC(void); boo! 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, содержит манипулятор исследуемого процесса. Первые две функции решают вспомогательные задачи;
191
return pis->Address:
ExtOutput("Unknown GetExpression(""£s"")\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 ~ ReadProcessMemory(hProcess. address, buffer, count. & dwRead);
192
Глава 3. Внутренние структуры данных GDI/DirectDraw
if ( bytesread ) * bytesread = dwRead; if ( (unsigned) address >= 0x80000000 ) Log("ReadKRamU08x. &])=", address, count): else
LogC'ReadURamUx, Шх. ld)=". hProcess, address, count):
int len = min(4, count/4): for (int 1=0: iImageGetSymbol для получения адреса по полученному имени вида win32k!gcMaxHmgr. Указатель pWin32k ссылается на объект KImageModule, в который была предварительно загружена информация о символических именах для файла win32k.sys. Функция KImageModule: :ImageGetSymbo1, не приведенная в книге, вызывает функцию SymGetSymFromName для преобразования символического имени в адрес. Интересная подробность: при вызове функция SymGetSymFromName получает указатель на неконстантный указатель на строку, тогда как ExtGetExpression в качестве параметра принимает только константный указатель на строку. Возникает естественное желание — преобразовать константный указатель в неконстантный, обмануть компилятор и добиться своего. Ничего не выйдет; вызов SymGetSymFromName завершится неудачей, и вы получите сообщение об ошибке доступа. Обе стороны настроены серьезно. Функция ExtGetExpression вызывается из библиотеки gdikdx.dll, которая компилируется в Visual C++ с параметром, перемещающим все строки в секцию, доступную только для чтения. Следовательно, строки, передаваемые ExtGetExpression, должны быть доступны только для чтения. Функция SymGetSymFromName ищет символ !, отделяющий имя модуля от имени функции, и заменяет его нуль-символом, чтобы обеспечить правильное завершение имени модуля. В результате для константной строки будет генерироваться ошибка. Проблема решается просто: перед вызовом SymGetSymFromName функция ImageGetSymbol копирует параметр в локальную переменную.
WinDbg и расширение отладчика GDI
193
Функция KHost: :ReadProcessMemory отвечает за чтение блоков памяти. Сначала она убеждается в том, что адрес принадлежит пространству ядра. Если проверка дает положительный результат, функция использует класс KPeriscoped lent (см. предыдущий раздел), который, в свою очередь, использует наш маленький драйвер режима ядра Periscope.sys; в противном случае просто вызывается функция Win32 API ReadProcessMemory с манипулятором процесса. Обратите внимание: при правильно заданном манипуляторе функция ReadProcessMemory позволяет читать содержимое адресного пространства пользовательского режима другого процесса. Однако KHost является классом C++, тогда как API расширения отладчика WinDbg определяется только с использованием средств С. Нам придется немного потрудиться, чтобы состыковать их. Ниже приведена часть оставшегося кода.
KHost theHost: void WDBGAPI ExtOutputRoutine(PCSTR format { vajist ap: va_start(ap. format);
)
theHost.WndOutputCtheHost.hwndOutput. format, ap); va_end(ap);
ULONG WDBGAPI ExtGetExpressionCPCSTR 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, ExtCheckControlj:, ExtReadProcessMemory. ExtWriteProcessMemory. 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
Структуры данных режима ядра
уф
20с
command
1 \_ Do
j jdumphmgr
Periscope loaded D:\WINNT50\symbols\sys\.\win32k.dbg loaded. Windows 03 vS.O, build 2031 "D:\WINNT50\System32\gdikdx.dll" loaded.
i -:
««« Extension DLL (2013 Free) does not match target system(2031 Free)
:
dumphmgr Max handles out so far 1130 Total Hragr: Reserved memory 2097152 Committed 36864 ulLoop-1130 gcMaxHmgr-1130 handles, (objects) TYPE DEF TYPE DC TYPE RON TYPE SURF TYPE CLIOBJ TYPE PAL TYPE ICMLCS TYPE LFONT TYPE PFE TYPE BRUSH TYPE TOTALS
current 132, 0 102, 0 -
37,
492,
3, 34, 1, 95,
102, 132, 998,
0 0 0 0 0 0 0 0 0
-
maximum 0, 0 0, 0 0, 0 0, 0 0, 0 0 0, 0, 0 0, 0 0, 0 0, 0 0, 0
-
-
-
allocated 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0
:; j i s
-
LookAside 0 0 0 0 0 0 0 0 0 0 0 -
LAB Cur
0 0 0 0 0 0
0 0
LAB Max
0 0
§ ;
0 0 0 0 0
0
0 0
cUnused objects 132 cUnknown objects 0 0
0
0 0
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, равно 16384. Обычно таблица используется лишь частично, поэтому многие элементы таблицы остаются пустыми. В 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
197
Структуры данных режима ядра
Тип
Идентификатор типа
Описание
SURFJYPE
0x05
Аппаратно-зависимый растр
CLIOBJJYPE
0x06
Клиентский объект
PATHJYPE
0x07
Траектория
PALJYPE
0x08
Палитра
ICMCSJYPE
0x09
LFONTJYPE
ОхОа
RFONTJYPE
ОхОЬ
PFEJYPE
ОхОс
PFTJYPE
OxOd
ICMCXFJYPE
ОхОе
ICMDLLJTYPE
OxOf
BRUSHJYPE
0x10, 0x30
D3D_HANDLE_TYPE
0x11
DD_VPORT_TYPE
0x12
SPACEJYPE
0x13
DD_MOTION_TYPE
0x14
METAJYPE
0x15
EFSTATE_TYPE
0x16
BMFDJYPE
0x17
VTFDJTYPE
0x18
TTFD_TYPE
0x19
RCJTYPE
Oxla
TEMPJYPE
Oxlb
Тип
Идентификатор типа
Описание
ORVOBJ_TYPE
Oxlc
DEFJYPE
0x00
Удаленные объекты GDI
DCIOBJJTYPE
Oxld
DCJYPE
0x01,0x21
Контекст устройства, метафайл
SPOOL TYPE
Oxle
DD DRAW TYPE
0x02
Объект DirectDraw (теперь обрабатывается отдельно)
DD SURF TYPE
0x03
Поверхность DirectDraw (теперь обрабатывается отдельно)
RGN TYPE
0x04
Регион
Логический шрифт
Кисть, перо
Из более чем 30 типов объектов, перечисленных в табл. 3.6, программистам Win32 известны лишь некоторые — например, объекты DC_TYPE, BRUSH_TYPE и LFONT_TYPE, соответствующие контексту устройства, кисти/перу и логическому Шрифту. Интересный факт: кисти и перья относятся к одному типу BRUSH_TYPE, хотя их идентификаторы типов несколько отличаются. Win32 API не содержит
198
Глава 3. Внутренние структуры данных GDI/DirectDraw
функций для непосредственного создания объектов траекторий (PATH_TYPE), хотя логика подсказывает, что какой-то объект в памяти все же создается. Построение траектории начинается с вызова функции BeginPath. При помощи расширения отладчика GDI мы исследуем структуры данных ядра, создаваемые для объектов GDI.
Контекст устройства в механизме GDI Контекст устройства является одним из основных объектов GDI. Его многочисленные атрибуты определяют различные аспекты взаимодействия Win32 API с графическим устройством, будь то видеоадаптер, принтер, плоттер или фотонаборная машина. GDI хранит данные контекста устройства в двух местах. Структура пользовательского режима OC_ATTR содержит такие атрибуты, как текущее перо, текущая кисть, цвета фона и текста. Определение структуры DC_ATTR приведено в разделе «Отслеживание СОМ-интерфейсов DirectDraw» главы 4. Механизм GDI также поддерживает структуру DCOBJ в адресном пространстве ядра; эта структура содержит полную информацию об объекте контекста устройства, включая копию DC_ATTR. Для манипулятора контекста устройства поле pKernel элемента таблицы объектов GDI ссылается на экземпляр DCOBJ, а поле pUser — на экземпляр DC_ATTR. Расширение отладчика GDI поддерживает несколько команд, предназначенных для расшифровки структуры данных, соответствующих манипуляторам устройств. Команда ddc расшифровывает НОС и выводит в основном содержимое DCOBJ; команда del выводит содержимое структуры DCLEVEL со структурой DCOBJ; команда dca выводит содержимое структуры DC_ATTR, присутствующей как в адресном пространстве ядра, так и в пользовательском адресном пространстве. Ниже показано то, что мы знаем об этих структурах. // dcobj.h // Windows 2000, 440(Ох1В8) байт typedef struct
HPALETTE hpal ; ppal: void * pColorSpace: void * lIcmMode: unsigned ISaveDeptn: unsigned unklJOOOOOOO: unsigned hdcSave; HGDIOBJ unk2 00000000[2]; unsigned pbrFill; void * pbrLine; void * unk3_ela28d88; void * hpath; // HGDIOBJ flPath; // unsigned lapath; // LINEATTRS prgnClip; void * prgnMeta : void * COLORADJUSTMENT ca: // flFontState: unsigned
HPATH PathFlags 0x20 байт 0x18 байт
199
Структуры данных режима ядра
unsigned unsigned unsigned unsigned MATRIX MATRIX FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ FLOATOBJ void * SIZE } DCLEVEL;
ufi;
unk4 00000000[12]; fl:
fl brush; mxWorldToDevice; mxDeviceToWorld; efMllPtoD; efM22PtoD: efDxPtoD; efDyPtoD: efMll TWIPS: efM22 TWIPS: efPrll; efPr22; pSurface; sizl;
// Windows 2000. 1548(0x600 байт typedef struct i( HGOIOBJ hHmgr; pEntry: void * cExcLock; ULONG ULONG Tid; OHPDEV 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 *
// // // //
000 004 008 OOc
dhpdev; // 0x010 dctype; fs: // Флаги ppdev ; hsem; // 0x020 flGraphics: f!Graphics2; pdcattr; // Указатель на DC ATTR dcLevel ; // 0x030 OxlB8(440) байт dcAttr: // OxlC8(456) байт hdcNext; // ОхЗВО hdcPrev: erclClip: unk4JOOOOOOO[2]; ercl Window: ercl Bounds: unk5_00000000[4]: prgnAPI : prgnVis: prgnRao; FillOrigin: unk6JOOOOOOO[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 DCOBJ:
unka_00000000[10]: hlfntCur;
unkb_00000000[2]: prfnt:
unkc_00000000[33]: unkdJOOOffff: unke_ffffffff: unkf 00000000[3];
В последний раз подробные описания структур вроде DCOBJ встречались в книге Шульмана (Schulman), Макси (Махеу) и Питрека (Pietrek) «Undocumented Windows», опубликованной в 1992 году. Эта книга помогла нам разобраться в некоторых полях, унаследованных от Windows 3.0/3.1, — как расшифрованных, так и не расшифрованных командами расширения отладчика. Для каждого объекта GDI данные режима ядра начинаются с 16-байтовой структуры. В первом поле хранится манипулятор GDI объекта; второе поле содержит неизвестный указатель; третье поле — счетчик блокировок, а последнее поле — идентификатор программного потока, создавшего объект. По манипулятору механизм GDI может обратиться к таблице объектов GDI и определить, какому процессу принадлежит манипулятор, а также получить доступ к структурам пользовательского режима (таким, как DC_ATTR). Первое поле после заголовка, dhpdev, содержит манипулятор структуры PDEV, находящейся под управлением драйвера графического устройства. Драйвер графического устройства должен обеспечивать управление несколькими физическими устройствами. Для этого драйвер устройства определяет структуру данных, необходимую для управления этими устройствами. В документации Windows DDI эти структуры упоминаются под именем POEV; они определяются и используются только драйвером устройства. Чтобы драйвер создал и инициализировал структуру PDEV, механизм GDI вызывает функцию драйвера DrvEnablePDEV. Поскольку структура PDEV управляется исключительно драйвером устройства, механизм GDI не интересуют подробности ее строения, поэтому DDI (интерфейс драйвера устройства) позволяет DrvEnablePDEV вернуть манипулятор PDEV вместо указателя на PDEV. Механизм GDI действует честно — он позволяет разработчику драйвера скрыть реализацию за манипулятором по аналогии с тем, как сам механизм GDI скрывает свою реализацию за манипуляторами GDI. Манипулятор, полученный при вызове DrvEnablePDEV, используется механизмом GDI при последующих обращениях к физическому устройству для создания графической поверхности. Чтобы освободить память и ресурсы, занимаемые физическим устройством, механизм GDI вызывает функцию DrvDisablePDEV. В Windows NT/2000 DDK включены примеры исходных текстов нескольких драйверов экрана, причем все они используют разные структуры PDEV. Функция DrvEnabl ePDEV, как правило, возвращает в качестве манипулятора обычный указатель на PDEV. Как мы знаем из Win32 API, существует несколько разных типов контекстов устройств. В структуре DCOBJ эти различия обозначаются в поле dctype. В настоящее время выделяются три типа контекстов: typedef enum
201
Структуры данных режима ядра
DCTYPE_DIRECT =0. // обычный контекст устройства DCTYPE_MEMORY = 1. // совместимый контекст DCTYPE_INFO = 2 // информационный контекст
}: Поле fs структуры DCOBJ содержит флаги, относящиеся к контексту устройства. Ниже перечислены некоторые из флагов, выводимых расширением GDI. typedef enum DC DC DC DC DC DC DC DC DC DC DC DC DC DC
DISPLAY DIRECT CANCELED PERMANENT DIRTY RAO ACCUM WMGR ACCUM APP RESET SYNCHRONIZEACCESS EPSPRINTINGESCAPE TEMPINFODC FULLSCREEN IN CLONEPDEV REDIRECTION
= = = = = = = = = = -
0x0001. 0x0002. 0x0004. 0x0008, 0x0010. 0x0020. 0x0040. 0x0080. 0x0100. 0x0200, 0x0400. 0x0800. 0x1000. 0x2000
} DCFLAGS: Следующее поле DCOBJ выводится расширением GDI под именем ppdev. Вполне естественно предположить, что это сокращение означает «Pointer to Physical DEVice», то есть «указатель на физическое устройство». В расширении GDI даже предусмотрена команда dpdev для расшифровки указателя на PDEV. Но согласно DDK, структура данных физического устройства находится под управлением драйвера устройства, и механизму GDI о ней знать ничего не положено. Функция драйвера DrvEnablePDEV возвращает манипулятор физического устройства вместо указателя на него. Одно из возможных объяснений заключается в том, что механизм GDI создает для физического устройства свою собственную структуру данных, которую мы назовем PDEV_WIN32K, чтобы избежать путаницы со структурой PDEV драйвера. Структура PDEV_WIN32K устроена чрезвычайно сложно. Мы поближе познакомимся с ней в следующем подразделе. Поле hsem ссылается на структуру семафора. Очевидно, семафор предназначен для синхронизации обращений к полям. В полях flGraphics и flGraphics2 хранятся флаги возможностей устройства. Состав этих флагов документируется в DDK; к их числу принадлежат флаги GCAPS_ALTERNATEFILL, GCAPS_WINDINGFILL, GCAPS_COLOR_DITHER и т. д. Флаги flGraphics2 и HGraphics2 берутся из структуры DEVINFO, заполняемой функцией DrvEnablePDEV драйвера устройства. Поле pdcattr ссылается на структуру DC_ATTR данного контекста устройства в адресном пространстве пользовательского режима, содержащую большую часть атрибутов контекста. Структура DCOBJ содержит копию этой структуры в поле /dcAttr. Вероятно, разработчики GDI хотели оптимизировать процесс присваивания значений атрибутам DC, сведя к минимуму использование кода режима ядра; для этого структура DC_ATTR должна размещаться в адресном пространстве Пользовательского режима. Однако разработчики также хотели упростить дос-
202
Глава 3. Внутренние структуры данных GDI/DirectDraw
туп к атрибутам в режиме ядра, для чего копия DC_ATTR должна находиться и в режиме ядра. Синхронизация двух копий DC_ATTR осуществляется с помощью специальных флагов. В процессе анализа структуры DC_ATTR выяснилось, что выполнение некоторых функций с манипулятором контекста устройства (например, выбор HBITMAP в совместимом контексте устройства или выбор палитры в DC) никак не влияет на содержимое таблицы объектов. Если вас интересует, эти атрибуты хранятся в структуре DCLEVEL, содержащейся в DCOBJ. Структура DCLEVEL содержит информацию о палитре, цветовой глубине, регулировке цвета, атрибутах линий, области отсечения, преобразованиях, траекториях и т. д.
Структура PDEV в механизме GDI Все графические драйверы поддерживают базовую точку входа DrvEnableDriver. При загрузке драйвера механизм GDI вызывает функцию DrvEnableDriver, которая заполняет структуру DRVENABLEDATA. DrvEnableDriver передает механизму GDI таблицу реализованных функций, тем самым сообщая ему, какие функции поддерживаются драйвером. В DirectDraw также создаются некоторые таблицы функций косвенного вызова. Конечно, механизм GDI должен хранить полученную информацию, относящуюся к конкретному драйверу, в некоторой структуре данных. Ниже приведено описание структуры PDEV механизма GDI. // Windows 20000 3304 (OxCES) байт typedef struct unsigned
header[4]:
void * int int void * unsigned unsigned void * void * POINT unsigned SPRITESTATE
ppdevNext: cPdevRefs; cPdevOpenRefs: ppdevParent: flags: flAccelerated: hsemDevLock; hseinPointer; ptl Pointer: unk 0038[2]; SpnteState:
// 0010 // 0014 // 0018
HFONT HFONT HFONT HGDIOBJ unsigned void * void * unsigned unsigned void * void * void * void *
hlfntDefault: hlfntAnsiVariable: hi fntAnsi Fixed; ahsurf[6]: unk_0240[2]: prfntActive: prfntlnactive; clnactive; unk_0254[27]:
// 021с
pf nDrvSetPoi nterShaoe ; pfnDrvMovePointer: pfnMovePointer: pfnSync :
// // // //
// OOlc // 0020 // 0024 // 0028 // 002c // 0030 // 0038 // 0040. 476(ldc) байт // 0220 // 0224 // 0228 // 0240 // 0248 // 024c // 0250 // 0254 02cO 02c4 02c8 02cc
Структуры данных режима ядра
203
unsigned void * unsigned
unk_02dO; pfnDrvSetPalette; unk_02d8[2J;
// 02dO // 02d4 // 02d8
void * DHPDEV void *
pldev; dhpdev: ppalSurf ;
// 02eO // 02e4 // 02e8
DEVINFO devinfo; GDIINFO gdiinfo; void * pSurface; void * hSpooler; unsigned pDesktopId: unk 0054: unsigned EDDJDIRECTDRAW_GLOBAL eDirectDrawGlobal : void * POINT DEVMODEW * unsigned void * } PDEV_WIN32K;
pGraphicsDevice; ptlOrigin; pdevmode : unk Ob78[3]: apfn[89];
// 02ec-417 // 0418-547 // 0548 // 054c // 0550 // 0554 // 1552 (0x610) байт // // // // //
Ob68 ОЬбс ОЬ74 0578 ОЬ84
Структура PDEV_WIN32K имеет довольно большой размер (3304 байта) и содержит большое количество информации, относящейся к драйверу устройства. PDEV_WIN32K в первую очередь используется механизмом GDI при обращениях к драйверу графического устройства для выполнения различных запросов пользователя. Структура начинается с неизвестного заголовка, состоящего из 16 байт. Разные структуры PDEV_WIN32K, существующие в системе, объединяются в иерархическое дерево. Поле ppdevNext содержит ссылку на следующую структуру, а поле ppdevParent указывает на родительскую структуру. В расширении отладчика GDI поддерживается команда dpdev для расшифровки структуры PDEV_ WIN32K. У этой команды имеется параметр -R, предназначенный для рекурсивного вывода всех структур, на которые ссылается родительская структура. Если воспользоваться параметром -R для структуры PDEV_WIN32K, соответствующей экранному контексту устройства, вы увидите, что поле ppdevNext связывается со структурами PDEV_WIN32K нескольких шрифтовых драйверов. В Windows GDI существует несколько типов графических драйверов, каждый из которых обладает специфическими особенностями. Классификация драйверов осуществляется на основании поля флагов flags. В табл. 3.7 перечислены некоторые флаги, поддерживаемые расширением GDI. Таблица 3.7. Флаги структуры PDEV_WIN32K
Флаг
Интерпретация
PDEV_DISPLAY
Экранный вывод
PDEV_HARDWARE_POINTER
Аппаратная поддержка курсора
PDEV_GOTFONTS
Наличие шрифтового драйвера
PDEV_DRIVER_PUNTED_CALL
Драйвер возвращает запросы механизму GDI
PDEV_FONTDRIVER
Шрифтовой драйвер
204
Глава 3. Внутренние структуры данных GDI/DirectDraw
Следующие несколько полей предназначены для управления курсором мыши в драйверах экрана. Поле hsemPointer представляет собой семафор, синхронизирующий операции с курсором мыши.'Драйвер устройства обеспечивает несколько функций косвенного вызова для вывода курсора мыши; адреса этих функций хранятся в полях pfnDrvSetPoi nterShape и pfnDrvMovePointer. В системе автора эти поля ссылаются на mga64!DrvSetPoi nterShape и mga64!DrvMovePointer. Структуры SPRITESTATE и DIRECTDRAWGLOBAL, внедренные в PDEV_WIN32K, относятся к реализации DirectDraw. Мы рассмотрим эти структуры в следующем разделе. В PDEV_WIN32K хранятся манипуляторы трех шрифтов. В системе автора поле hlfntDefault ссылается на гарнитуру «System», поле h i f n t A n s i V a r i a b l e — на гарнитуру «MS Sans Serif», а поле hlfntAnsiFixed — на гарнитуру «Courier». Хотя Windows GDI старается соответствовать принципу WYSIWYG, со штриховыми кистями возникают проблемы. В GDI штриховая кисть определяется монохромным растром размером 8 х 8. На экранах с разрешением от 72 dpi до 120 dpi горизонтальные, вертикальные, диагональные или решетчатые узоры выглядят вполне нормально. Однако на принтерах с разрешением от 180 до 2400 dpi и даже выше узор из растров, определяемых матрицами 8 x 8 пикселов, превращается в сплошную серую рябь. Чтобы штриховые кисти были лучше видны на устройствах высокого разрешения, механизм GDI позволяет драйверу устройства передать свои собственные растры для реализации шести стандартных типов штриховых кистей Windows GDI. Функция EnablePDEV драйвера устройства может передать массив из шести указателей на поверхности (растры), а манипуляторы соответствующих объектов GDI сохраняются в массиве ahsurf. Хотя драйверам экрана рекомендуется передавать эти манипуляторы, в структуру PDEV_WIN32K экранного DC включается шесть манипуляторов стандартных растров 8 x 8 , передаваемых GDI по умолчанию. Структуры GDI обычно существуют в виде пар; одна структура относится к логическому описанию, а другая — к физической реализации. Следовательно, для структуры физического устройства PDEV_WIN32K следует поискать парную структуру с логическим описанием. Указатель на такую структуру хранится в поле pldev, а сама структура расшифровывается командой расширения GDI dldev. Структуры LDEV_WIN32K образуют двусвязный список. Начиная с экранного контекста устройства, список открывается драйвером экрана (например, «\SystemRoot \System32\mga64.dll»), за которым следует несколько шрифтовых драйверов. Последовательность завершается шрифтовым драйвером ATM («\SystemRoot\ System32\atmfd.dll»).
// Windows 2000. 384 (0x180) байт typedef struct LDEV_WIN32K * nextldev; LDEV_WIN32K * prevldev: ULONG 1evtype; ULONG cRefs: ULONG unk_010: void * pGdiFriverlnfo: ULONG ulDriverVersion; PFN apfn[89]: LDEV WIN32K:
Структуры данных режима ядра
205
По данным протокола, сгенерированного программой Fosterer, для получения первой логической структуры графического устройства расширение отладчика GDI читает значение глобальной переменной win32k!gpldevDrivers. Структура PDEV_WIN32K содержит копию структуры DEVINFO, заполняемой точкой входа DrvEnablePDEV графического драйвера. Структура DEVINFO документирована в DDK. В основном она описывает возможности драйвера устройства по обработке кривых, шрифтов, графических форматов, работе с цветом и т. д. Еще структура PDEV_WIN32K содержит копию структуры GDI INFO, также заполняемой точкой входа DrvEnablePDEV и документированной в DDK. Структура GDI INFO в основном содержит информацию о размере, формате и разрешении графической поверхности. Многие поля структуры GDIINFO можно получить при помощи функции Win32 API GetDeviceCaps. Например, вызов GetDeviceCapsChDC, TECHNOLOGY) связан с полем ulTechnology структуры GDIINFO, а значение GetDeviceCapsChDC, RASTERCAPS) берется из поля flRaster структуры GDIINFO. Поле pSurface ссылается на структуру поверхности SURFACE, где фактически выполняются все графические операции. Структура поверхности рассматривается ниже в этом разделе. Поле pdevmode ссылается на структуру DEVMODEW, Unicode-версию DEVMODE. Структура DEVMODE обычно инициализируется графическим драйвером и модифицируется пользовательским приложением, что позволяет не только получать информацию от драйвера устройства, но и менять значения параметров, настройка которых допускается драйвером. При выводе на экран структура DEVMODE особой пользы не приносит, однако драйвер принтера получает из нее важную информацию о качестве печати, размере бумаги, типе носителя, разрешении и т. д. Последнее и самое важное поле структуры PDEV_WIN32K, apfn, представляет собой таблицу из 89 указателей на функции. В Windows 2000 интерфейс DDI определяет 89 функций, которые могут реализовываться драйвером графического устройства; каждой функции соответствует заранее определенный индекс. Например, индекс INDEX_DrvEnablePDEV равен 0, а индекс INDEX_DrvSynchronizeSurface равен 88. Одни из этих 89 индексов не используются, другие зарезервированы, третьи предназначены только для драйверов экрана^ а четвертые — только для драйверов принтеров. Лишь небольшая часть этих функций обязательно должна реализовываться драйверами устройств; остальные функции необязательны. При загрузке драйвера устройства система вызывает функцию DrvEnableDriver, которая заполняет структуру DRVENABLEDATA. В сущности, структура DRVENABLEDATA представляет собой сжатую таблицу с 89 указателями на функции. Присваивание значений 89 указателям на функции — утомительная работа, чреватая ошибками и плохо расширяемая. По этой причине механизм GDI позволяет драйверу передать список поддерживаемых им функций вместе с индексами, по которому механизм GDI строит расширенную таблицу. Таблица функций хранится в двух местах. Структура логического устройства LDEV_WIN32K содержит исходную таблиЦу функций, сконструированную по данным DRVENABLEDATA и полученную от драйвера устройства. Структура физического устройства PDEV_WIN32K содержит таблицу, фактически используемую механизмом GDI; эта таблица содержит функции из LDEV_WIN32K, а также точки входа механизма GDI для реализации функций, не поддерживаемых драйвером устройства. Например, если драйвер устройства не поддерживает DrvBitBlt, он фактически обращается к механизму GDI с
Глава 3. Внутренние структуры данных GDI/DirectDraw
207
Структуры данных режима ядра
просьбой предоставить реализацию этой функции. По этой причине таблица функций PDEV_WIN32K вместо указателя NULL содержит указатель на функцию win32k.sys — Win32 ISpBitBlt. В табл. 3.8 приведено содержимое таблицы функций DDI на компьютере автора.
Индекс
Адрес
Имя функции
41
fdd69950
mga64!DrvGetModes
43
fdd5bf60
mga64!DrvDestroyFont
59
fdd6eflO
mga64 ! DrvGetDi rectDrawI nf о
Таблица 3.8. Пример таблицы функций PDEV_WIN32K
60
fdd6f!90
mga64 ! DrvEnabl eDi rectDraw
'
Индекс
Адрес
Имя функции
61
fdda0410
mga64 ! DrvOi sabl eDi rectDraw
00
fdd691fO
mga64! DrvEnabl ePDEV
67
fdd68dcO
mga64 ! Drvl cmSetDevi ceGammRamp
01
fdd69310
mga64!Dn/CompletePDEV
68
a0036184
win32k!SpGradientFill
02
fdd69350
raga64!DrvDisablePDEV
69
a0073180
win32k!SpStretchBHROP
03
fdd693bO
mga64 ! DrvEnabl eSurf ace
70
a010d!93
win32k!SpPlgBlt
04
fdd69640
mga64 ! DrvDi sabl ePDEV
71
aOlOdOfl
win32k!SpALphaBlend
05
fdd69760
mga64 ! DrvAssertMode
74
a010d049
wi n32k! SpTransparentBl t
06
fdd69710
mga64!DrvOffset
07
fdd68faO
mga64!DrvResetPDEV
10
fdd4fcOO
mga64 ! DrvCreateDevi ceBi tmap
11
fdd4fdOO
mga64 ! DrvDel eteDevi ceBi tmap
12
fddSOfbO
mga64 ! DrvReal i zeBrush
13
fddSOccO
mga64 ! DrvDi therCol or
14
aOOa8fe3
win32k!SpStrokePath
15
aOOaaafc
win32k!SpFillPath
17
fdd68890
mga64!DrvPaint
18
a001834b
win32k!SpBitBlt
19
a001d26d
win32k!SpCopyBits
20
a0064500
win32k!SpStretchBlt
22
fdd68clO
mga64!DrvSetPalette
23
a001d72e
win32k!SpTextOut
24
fdd70fcc
mga64!DrvEscape
29
fddSdelO
mga64 ! DevSetPoi nterShape
30
fdd5df90
mga64 ! DrvMovePoi nter
31
аООаабЗЬ
win32k!SpLineTo
40
a003c327
wi n32k ! SpSaveScreenBl t
Как видно из таблицы, в случае с драйвером экрана механизм GDI выполняет основную работу по выводу кривых, заливок, текста и растров, а драйвер экрана выполняет инициализацию, операции с курсором мыши, реализацию оф>ектов и т. д.
Поверхности в механизме GDI На уровне механизма GDI графические функции работают с поверхностями, связанными с драйвером устройства, на котором осуществляется вывод. При работе с поверхностями устройств используется координатная система, напоминающая режим отображения MMJTEXT GDI API. Пикселы поверхности адресуются парами 28-разрядных целых чисел со знаком; в левом верхнем углу расположено начало координат — точка (0, 0). Поверхность устройства лежит в правом нижнем квадранте этой системы координат, а обе координаты принимают только неотрицательные значения. Хотя координаты в Win32 API хранятся и передаются в виде 32-разрядных целых чисел со знаком, в некоторых графических операциях механизм GDI использует младшие 4 бита 32-разрядного целого для представления дополнительных координат (субпикселов), повышающих точность вычислений. В механизме GDI определены два основных типа поверхностей. Поверхности первого типа, управляемые механизмом GDI, в документации DDK обычно именуются аппаратно-независимыми растрами (Device-Independent Bitmap, DIB). Поверхности, управляемые GDI, состоят из одной цветовой плоскости с упакованными пикселами и выравниванием строк развертки по границам двойных слов. Если драйвер устройства работает с поверхностью, управляемой графическим механизмом, весь вывод на этой поверхности может быть выполнен средствами GDI. Поддержка со стороны механизма GDI в несложных драйверах экрана или драйверах растровых принтеров заметно упрощает сами драйверы
Глава 3. Внутренние структуры данных GDI/DirectDraw
и их сопровождение. Пример драйвера кадрового буфера, входящий в Windows 2000 DDK, создает поверхность, управляемую графическим механизмом, в качестве основной поверхности и поручает выполнение вывода GDI. Драйвер принтера UniDrv в Windows 2000 также использует поверхности, управляемые графическим механизмом, после деления физической страницы на серию прямоугольных полос. • Ко второму типу относятся поверхности, управляемые устройством; то есть драйверам устройств дозволяется организовать самостоятельное управление своими поверхностями. Во внутреннем представлении формат поверхности, управляемой устройством, может совпадать с форматом поверхности, управляемой графическим механизмом, или отличаться от него. Если форматы совпадают, драйвер устройства все равно может выполнять графические операции средствами GDI. Растры в формате устройства (device-format bitmap) составляют особую категорию специализированных форматов поверхностей, управляемых устройствами. Данная возможность поддерживается для того, чтобы некоторые драйверы экрана могли реализовать ускоренное копирование растров на экран. Кроме того, это позволяет драйверам осуществлять вывод в видеопамяти, поделенной на банки, или работать с растрами в нестандартных форматах. Главной структурой данных, предназначенной для представления различных поверхностей GDI, является структура SURFOBJ. Структура SURFOBJ документирована в Windows NT/2000 DDK. Она занимает одно из центральных мест в интерфейсе DDI и используется для представления как растров, так и графических поверхностей. Поскольку структура SURFOBJ очень важна для работы механизма GDI, ниже приведено ее определение, позаимствованное из документации DDK.
typedef struct _SURFOBJ { DHSURF dhsurf; HSURF hsurf; DHPDEV dhpdev; HDEV hdev: SIZEL sizlBitmap: ULONG cjBits; PVOID pvBits; PVOID pvScanO; LONG 1 Delta; ULONG iUniq; ULONG iBitmapFormat; USHORT iType; USHORT 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 положительно, а для перевернутых — отрицательно. Поле iUniq предназначено для целей оптимизации. Оно содержит текущее состояние поверхности, управляемой графическим механизмом, и обновляется при каждом изменении поверхности. Это позволяет драйверу устройства организовать кэширование поверхностей. Например, если драйвер принтера PostScript получает два запроса на вывод растра с одним исходным растром и одинаковыми значениями iUniq, драйверу достаточно сохранить исходный растр при обработке первого запроса и воспользоваться им при получении второго запроса. В поле iBitmapFormat структуры SURFOBJ задается стандартный формат поверхности, управляемой графическим механизмом, наиболее близко подходящий к формату данной поверхности. Это может быть изображение с 1, 4, 8, 16, 24 и 32 битами на пиксел, несжатое или сжатое по алгоритму RLE. В Windows 2000 GDI драйвер устройства также может поддерживать сжатые изображения в формате JPEG и PNG, для чего полю iBitmapFormat присваиваются соответственно значения BMF_JPEG и BMF_PNG. Однако ни Windows GDI, ни графический механизм не поддерживают работу с изображениями в формате JPEG или PNG; эти изображения просто передаются драйверу устройства, если последний заявляет о своей поддержке этих форматов. В поле iType задается тип поверхности. Допустимые значения перечислены в табл. 3.9. Таблица 3.9. Типы поверхностей SURFOBJ.iType
Описание
STYPE_BITMAP
Растр, управляемый механизмом GDI
STYPE_DEVICE
Поверхность, управляемая драйвером
STYPE DEVBITMAP
Растр, управляемый драйвером, в формате устройства
В последнем поле f jBitmap хранятся некоторые флаги поверхностей, управляемых графическим механизмом. Эти флаги сообщают, хранится ли растр в прямом или перевернутом виде, инициализируется ли он нулями, является ли он транзитивным или отсутствующим в системной памяти. Если предполагается, что структура SURFOBJ представляет все графические поверхности механизма GDI, где же хранятся сведения о цветах — например, палитра? В механизме GDI управление цветом отделено от SURFOBJ. Для каждого графического вызова, использующего SURFOBJ, передается указатель на струк-
Глава 3. Внутренние структуры данных GDI/DirectDraw
туру XLATEOBJ, которая при необходимости обеспечивает преобразование цветов между исходной и целевой поверхностью. Например, функции DrvStretchBlt и DrvPlgBlt используют параметр pxlo, содержащий указатель на объект XLATEOBJ.
Аппаратно-зависимые растры в механизме GDI Аппаратно-зависимые растры (Device-Dependent Bitmaps, DDB) управляются драйверами графических устройств с поддержкой со стороны Windows GDI. Прежде чем использовать поверхность DDB, необходимо создать для нее объект GDI, при этом возвращается манипулятор типа HBITMAP. Хотя предполагается, что аппаратно-зависимые растры поддерживаются драйвером устройства в собственном формате, все большее количество драйверов устройств Windows NT/ 2000 поручает выполнение большинства графических операций механизму GDI. Для этого формат их растров должен соответствовать формату, поддерживаемому механизмом GDI. Манипуляторы HBITMAP также находятся под управлением диспетчера манипуляторов GDI. Следовательно, с каждым манипулятором в таблице объектов GDI связано 16 байт информации, включая указатель на структуру в адресном пространстве ядра. В расширении отладчика GDI эта структура называется SURFACE. Главной частью структуры SURFACE является структура SURFOBJ. Определение структуры SURFACE выглядит следующим образом: // Windows 2000, 128 (0x80) байт typedef struct HGDIOBJ void * ULONG ULONG
hHmgr; pEntry; cExcLock; Tid;
// // // //
SURFOBJ
surfobj:
// 010, документируется в DDK
pdcoAA: flags; ppal: unk_050[2]: sizlDim[2]; hdc: cRef: hpalHint; unk_06c[5]:
// // // //
XDCOBJ * FLONG PPALETTE unsigned SIZEL HOC ULONG HPALETTE unsigned SURFACE:
000 004 008 OOc 044, выводится gdikdx 048 04c 050
// 058
// 060 // 064
// 068 // Обе
Структура SURFACE, как и структуры ядра других объектов GDI, начинается с 16-байтового заголовка. После заголовка следует структура SURFOBJ с информацией о формате, размере, графическими данными и т. д. Структура SURFACE должна полностью описывать растр GDI — либо DDB, либо DIB-секцию. Поэтому в структуре SURFACE после структуры SURFOBJ хранится манипулятор палитры и указатель на структуру PALETTE. PALETTE является структурой режима ядра для объекта логической палитры GDI. Мы рассмотрим структуру PALETTE в одном из следующих разделов этой главы.
Структуры данных режима ядра
211
Поле флагов flags в структуре SURFACE содержит флаги АР1_В1ТМАР (растр, созданный средствами Win32 API) и DDB_SURFACE (аппаратно-зависимый растр Win32
API). Аппаратно-зависимые растры Win32 API и DIB-секции могут выбираться в контексте устройства. В этом случае поле hdc содержит манипулятор контекста устройства, а в поле cRef хранится счетчик выборов объекта в DC. Поле sizlDim обеспечивает поддержку функций Win32 API SetBitmapDimensionEx и GetBitmapDimensionEx, предоставляя место для хранения физических размеров растра. Терминология 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, обычно равно STYPEJITMAP; поле fjBitmap обычно равно BMFJTOPDOWN; поле flags обычно равно API_BITMAP | DDB_SURFACE, а поле pvBits указывает на адресное пространство ядра. Таким образом, память для графических данных DDB выделяется в общем адресном пространстве ядра из выгружаемого пула.
DIB-секции в механизме GDI В терминологии Win32 API DIB-секцией (DIB section) называется растр, который находится под управлением GDI, но доступен для пользовательских программ непосредственно через указатель. DIB-секции создаются функцией CreateDIBSection с передачей описания в структуре BITMAPINFO. GDI возвращает манипулятор HBITMAP, с которым можно выполнять те же операции, что и с манипуляторами DDB, а также указатель на графические данные, которые можно читать и записывать через указатель, как содержимое обычного блока памяти. В таблице объектов GDI DIB-секция почти эквивалентна DDB. У нее тоже имеется манипулятор и структура SURFACE в адресном пространстве ядра. Главное отличие заключается в том, что память для графических данных DIB-секЦии выделяется в адресном пространстве пользовательского режима вместо
Глава 3. Внутренние структуры данных GDI/DirectDraw
адресного пространства режима ядра. Благодаря этому обстоятельству графические данные становятся доступными для пользовательских программ; кроме того, вывод средствами механизма GDI может происходить лишь в том случае, если процесс-владелец является текущим процессом. Поле fjBitmap структуры SURFOBJ для DIB-секций равно BMF_DONTCACHE. Это означает, что графический драйвер не должен кэшировать графические данные на основании содержимого поля iUniq, поскольку графические данные могут быть изменены пользовательской программой без ведома GDI через указатель, полученный при вызове CreateDIBSection. Другое, второстепенное отличие заключается в том, что DIB-секции, как и DIB, обычно хранятся в памяти в перевернутом виде, если только их высота не задается отрицательной величиной. Мы знаем, что аппаратно-независимые растры (DIB) не находятся под управлением GDI. В частности, для них нельзя создать манипуляторы GDI. Однако при передаче DIB драйверу устройства в интерфейсе DDI применяется все та же структура SURFOBJ вместо структуры BITMAPINFO, используемой для представления DIB в Win32 API. Видимо, механизм GDI создает временную структуру SURFOBJ для представления DIB перед обращением к точкам входа механизма GDI или драйвера устройства.
Кисти в механизме GDI Кисти задают цвет и узор заполнения некоторой области. Средства Win32 API позволяют создавать однородные (solid) кисти, штриховые (hatched) кисти, узорные (pattern) кисти DDB, а также узорные кисти DIB. Из раздела «Структуры данных пользовательского режима» мы знаем, что для однородных кистей в адресном пространстве пользовательского режима создается небольшая структура .для хранения цвета кисти, что повышает эффективность использования однородных кистей. Для всех остальных типов кистей GDI хранит всю информацию в структуре BRUSH ядра.
213
Структуры данных режима ядра BRUSHATTR * pbrushattr:
unsigned
unsigned COLORREF COLORREF ULONG ULONG ULONG
unsigned
ULONG
unsigned
ULONG DWORD * ULONG
unsigned
unk 030: bCacheGrabbed; crBack; crFore: ulPalTime: ul Surf Time: ulRealization; unk_04c[3]: ulPenWidth: unk 05c: ulPenStyle; pStyle: dwStyleCount: unk_06c ;
// 028 // // // // // // // // // // // // //
030 034 038 03c 040 044 048 04c 058 05c 060 064 068
BRUSH; Структура BRUSH начинается со стандартного 16-байтового заголовка объектов GDI ядра. За ним следует поле ul Style стиля кисти, значение которого отличается от значения аналогичного поля структуры LOGBRUSH. В расширении отладчика GDI оно кодируется константами HS_CROSS, HS_PAT, HS_DITHEREDCLR и т. д. В полях crBack и crFore хранится основной и фоновый цвет контекста устройства, а в поле brushAttr.lbColor — настоящий цвет кисти. В поле f l A t t r s хранятся дополнительные флаги (табл. 3.10). Таблица 3.10. Атрибуты кисти в структуре BRUSH
BRUSH.flAttrs
Описание
BR_NEED_BK_CLR (0x0002)
Необходим фоновый цвет
BR_DITHER_OK (0x0004)
Разрешить смешивание цветов
BRJSJOLID (0x0010)
Однородная кисть
BR_IS_HATCH (0x0020)
Штриховая кисть
BR_IS_BITMAP (0x0040)
Узорная кисть DDB
} BRUSHHATTR;
BR_IS_OIB (0x0080)
Узорная кисть DIB
// Windows 2000. 112 (0x70) байт (?) typedef struct { HGDIOBJ hHmgr: // 000. 000. заголовок объектов GDI режима ядра void * pentry: // 004 ULONG cExcLock: // 008 Tid: ULONG // OOc
BR_IS_NULL (0x0100)
Пустая кисть
BR_IS_GLOBAL (0x0200)
Стандартные объекты
BR_IS_PEN (0x0400)
Перо
BR_IS_OLDSTYLEPEN (0x0800)
Геометрическое перо
BR_IS_MASKIN6 (0x8000)
Растр узора используется как маска прозрачности
typedef struct {
unsigned AttrFlags; COLORREF IbColor;
ULONG HBITMAP HANDLE ULONG
ul Style: hbmPattern: hbmClient;
flAttrs:
// // // //
010 014 018 Olc
ULONG ulBrushUnique: // 020 BRUSHATTR * pbrushhttr; // 024
_BR_CACHED_IS_SOLID (0x80000000)
, При работе с узорными кистями DIB механизм GDI создает объект для растра кисти, манипулятор которого хранится в поле hbmPattern, при этом в поле
214
Глава 3. Внутренние структуры данных GDI/DirectDraw
hbmClient остается манипулятор HGLOBAL, передаваемый при вызове CreateDIBPatternBrush. Для узорных кистей DDB механизм GDI копирует исходную поверхность DDB, сохраняя манипулятор копии в поле hbmPattern, а манипулятор исходной поверхности — в поле hbmClient. Копирование исходного растра позволяет программисту удалить его после создания объекта кисти. Объект узорной кисти никогда не существует в одиночку; для него всегда создается парный объект растра узора. К этому моменту вы должны уже достаточно хорошо понимать, как различные типы кистей представляются в механизме GDI.
Перья в механизме GDI Перо определяет цвет и стиль линий, дуг и кривых. Win32 API позволяет создавать косметические и геометрические перья с разными стилями, разной толщиной и атрибутами. Как ни странно, механизм GDI не определяет специальной структуры данных для представления перьев — для них используется та же структура BRUSH, что и для кистей. Впрочем, это выглядит вполне логично, если заметить, что расширенные перья, создаваемые функцией ExtCreatePen, определяются с помощью структуры LOGBRUSH. Механизм GDI различает перья и кисти по флагу BR_IS_PEN в поле flAttrs. Другой флаг, BR_IS_OLDSTYLEPEN, указывает, было ли перо создано при помощи «старомодной» функции CreatePen (или CreatePenlndirect) вместо «новой» функции ExtCreatePen. Поля ulPenWidth, ulPenStyle, pStyle и dwStyl eCount имеют тот же смысл, что и аналогичные поля структуры EXTLOGPEN, определяемой в Win32 API. В расширении отладчика GDI существует команда dpbrush для расшифровки структуры BRUSH, однако эта команда работает лишь с полями, относящимися к «настоящим» кистям. Для перьев, созданных функцией ExtCreatePen, эта команда возвращает неполную информацию.
Палитры в механизме GDI Палитра представляет собой цветовую таблицу, по которой цветовые индексы преобразуются в значения RGB или, наоборот, значения RGB преобразуются в исходный цветовой индекс. Чтобы работать с палитрой в контексте устройства, необходимо создать логическую палитру функцией CreatePalette или CreateHalftonePalette. Эти функции возвращают манипулятор логической палитры (тип 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 2000. 84+4n байт typedef struct _EPALOBJ HGDIOBJ void * ULONG ULONG
hHmgr; pentry; cExcLock: Tid;
// // // //
000. заголовок объею 004 008 OOc
FLONG ULONG ULONG HOC HDEVPPAL ULONG ULONG PTRANSLATE
flPal; cEntries; ulTime; hdcHead: hSelected: cRefhpal : cRef Regular; ptransFore:
// // // // // //
010 014 018 Olc 020 024
// 028 // 02c
PTRANSLATE ptransCurrent; // 030 // 034 PTRANSLATE ptransOld: // 038 unsigned unk_038: pGetNearer: // 03c PFN // 040 pGetMatch: PEN // 044 ulRGBTime; ULONG // 048 PRGB555XL pRGBClate: // 04c, this EPALOBJ * pPalette; // 050. this->apa1Color PAL ULONG * papal Col or; PAL ULONG apalColor[l]; // 054 EPALOBJ ; Структура EPALOBJ представляет объект логической палитры в режиме ядра, поэтому она, как и структуры всех объектов GDI, начинается со стандартного заголовка. Тип таблицы трансляции цветов определяется содержимым поля flPal. В табл. 3.11 приведены значения, полученные из выходных данных расширения отладчика GDI и частично — из заголовочного файла winddi.h. Таблица 3.11. Флаги EPALOBJ EPALOBJ.flPal
Значение
Описание
PAL_INDEXED
0x0001
Индексируемая палитра
PAL_BITFIELDS
0x0002
Используются битовые маски
PAL_RGB
0x0004
Красный, зеленый, синий
PAL_B6R
0x0008
Синий, зеленый, красный Продолжение
216
Глава 3. Внутренние структуры данных GDI/DirectDraw
Таблица 3.11. Продолжение EPALOBJ.flPal
Значение
Описание
PAL_CMYK
0x0010
Голубой, малиновый, желтый, черный
PAL_DC
0x0100
PAL_FIXED
0x0200
PAL_FREE
0x0400
PALJ10NOCHROME
0x2000
Только два цвета
PAL_DIBSECTION
0x8000
Используется для DIB-секции
PAL_HT
0x100000
Полутоновая палитра
PAL_PGB16_555
0x200000
16-битный RGB-цвет в формате 555
PAL_RGB16_565
0x400000
16-битный RGB-цвет в формате 565
Не может изменяться
В поле cEntries хранится количество элементов в цветовой таблице ара!Color. Эти два поля аналогичны полям структуры PALOBJ. Механизм GDI сохраняет в структуре EPALOBJ адреса двух функций, pGetNearest и pGetMatch. На компьютере автора поле pGetNearest ссылается на win32k!u1IndexedGetNearestFromPa1 Entry, а поле pGetMarch — на ulIndexedGetMatchFromPalEntry (хотя в других системах они могут ссылаться на что-нибудь другое). Драйверы устройств не работают со структурой EPALOBJ напрямую. В файле winddi.h определяется структура PALOBJ, содержащая единственное поле ulReserved. Чтобы обратиться к цветовой таблице, драйвер устройства должен вызвать функцию графического механизма PALOBJ_cGetColors. Прослеживается аналогия со структурой XLATEOBJ, для обращения к которой также определяется специальная функция XLATEOBJ_cGetPalette.
Регионы в механизме GDI Регион (region) определяется как совокупность точек на поверхности графического устройства. Он может иметь форму прямоугольника, многоугольника, эллипса или произвольной комбинации этих фигур. Для регионов определены операции заливки, инвертирования и обводки; кроме того, они используются при отсечении или проверке принадлежности (hit testing). Вероятно, чаще всего регионы применяются при отсечении. Регионы принадлежат к числу объектов, управляемых GDI. Новые регионы создаются такими функциями, как CreateRectRgn, CreateRoundRgn и CreateEllipticRgn. Объединение существующих регионов осуществляется посредством логических операций. Все эти функции возвращают манипулятор объекта GDI, HRGN, который используется при последующем вызове функций GDI. Как было показано в разделе «Структуры данных пользовательского режима», координаты прямоугольных регионов GDI хранит в структурах данных пользовательского режима. Для других регионов информация хранится в адресном пространстве ядра.
217
Структуры данных режима ядра
В расширении отладчика GDI предусмотрена команда dr, предназначенная для расшифровки HRGN или указателя на структуру данных REGION режима ядра. Команда даже перечисляет все прямоугольники, из которых состоит заданный регион. Ниже приведена информация о структуре REGION, полученная при помощи этой команды. // Windows 2000. переменный размер // Не используйте непосредственные ссылки на scnPntCntToo! typedef struct LONG LONG LONG LONG LONG } SCAN;
scnPntCnt; scnPntTop; scnPntBottom; scnPntX[2]; scnPntCntToo:
// // // // //
Количество координат х Верхняя граница (включается) Нижняя граница (не включается) Массив переменной длины, содержащий х пар To же. что и scnPntCnt:
// Windows 2000, переменный размер struct REGION HGDIOBJ hHmgr: void * pentry; ULONG cExcLock; ULONG Tid;
// // // //
000, заголовок объектов GDI режима ядра 004 008 00с
unsigned sizeObj; unsigned unk_014[2]; SCAN * pscnTail; unsigned sizeRgn; unsigned cScans; RECTL rcl; SCAN scnHead[l];
// // // // // // //
010 014 Olc 020 024 028 038
Структура REGION начинается со стандартного 16-байтового заголовка. Как упоминалось в разделе «Структуры данных пользовательского режима», GDI оптимизирует процедуру создания регионов, состоящих из одного прямоугольника, за счет связывания с манипулятором GDI структуры RECT пользовательского режима. Тем самым GDI привязывает объект региона к процессу-создателю. Для поддержания этой связи механизм GDI сохраняет в заголовке идентификатор программного потока, создавшего объект. Структура REGION имеет переменный размер. Она содержит всю информацию о регионе, объем которой может увеличиваться или уменьшаться в результате применения операций к региону. Например, если регион объединяется с другим регионом операцией RGNJ3R, размер структуры обычно увеличивается, а при использовании операции RGN_AND он обычно уменьшается. Чтобы уменьшить количество операций выделения/освобождения, механизм GDI не выделяет блок Памяти именно того размера, который необходим для представления региона; вместо этого он выделяет несколько больший блок, позволяющий увеличивать размеры региона без повторного выделения памяти. Вероятно, размер структуры REGION при выделении памяти хранится в поле sizeObj, а фактически используемый размер — в поле sizeRgn.
Глава 3. Внутренние структуры данных GDI/DirectDraw
В поле гс! хранятся данные прямоугольника, ограничивающего регион. Важнейшими данными в структуре 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 есть веские причины. Регионы обычно передаются функциям графических драйверов в виде структур CLIPOBJ. Интерфейс DDI не предоставляет доступа к внутренней структуре данных CLIPOBJ; вместо этого он позволяет графическим драйверам перечислить все прямоугольники, образующие регион, при помощи функции CLIPOBJ_bEnum. Драйвер может указать порядок перечисления прямоугольников при помощи функции CLIPOBJ_cEnumStart. Механизм GDI позволяет производить перечисление слева направо, сверху вниз; справа налево, сверху вниз; слева направо, снизу вверх и т. д. — в любом порядке, удобном для GDI. Поле pscanTail структуры REGION позволяет механизму GDI быстро перейти к последней структуре SCAN. Поле scnPntCount позволяет быстро переходить слева направо или к следующей структуре SCAN при перечислении сверху вниз. Поле scnPntCountToo обеспечивает быстрый переход справа налево или к следующей структуре SCAN при перечислении снизу вверх. В следующем примере демонстрируется связь структуры REGION с регионами, знакомыми нам по Win32 API. При создании региона функцией CreateEllipticRgn(0,0,100,100) вы получаете манипулятор региона. Укажите его при вызове команды dr расширения GDI; в отладчике выводится адрес структуры REGION и список всех прямоугольников, образующих регион. Структура REGION содержит 63 структуры SCAN с ограничивающим прямоугольником [О, О, 99, 99]. В табл. 3.12 приведен сокращенный список элементов массива структур SCAN.
219
Структуры данных режима ядра
Таблица 3.12. Массив структур SCAN в структуре REGION (для круга)
CntToo
Cnt
Тор
Bottom
0
-maxint - 1
0
2
0
1
47,52
2
2
1
2
39,60
2
2
39
47
1,98
2
2
47
52
0,99
2
2
52
60
1,98
2
2
97
98
39,60
2
2
98
99
47,52
2
0
99
maxint
Х[]
0
0
Из приведенного примера видно, что структура REGION содержит аппроксимацию исходной фигуры в виде комбинации прямоугольников, задаваемых целочисленными координатами. Следовательно, если создать для региона манипулятор GDI и потом масштабировать его (при помощи функций GetRegionData и ExtCreateRegion с параметром XFORM), результат будет отличаться от того, который получится при обратной процедуре (предварительном масштабировании математическими методами и последующем создании манипулятора GDI). Структура REGION описывает регион слева направо, сверху вниз. Верхние и левые координаты включаются в регион, а нижние и правые — не включаются. При создании структур REGION GDI старается действовать как можно точнее, поэтому многие структуры SCAN имеют высоту всего в один пиксел. Например, несколько первых и последних структур SCAN для круга соответствуют прямоугольникам высотой в 1 пиксел. Но там, где это возможно, GDI с целью экономии памяти увеличивает SCAN до максимально возможной высоты. Например, центральная часть круга аппроксимируется прямоугольником высоты 5, соответствующей средней структуре SCAN в массиве (координаты от 47 до 52). Для точного представления регионов, не имеющих ярко выраженного прямоугольного строения, размер структуры REGION обычно прямо пропорционален высоте региона и в меньшей степени зависит от ширины региона. Например, при удвоении высоты эллипса размер REGION может вырасти вдвое, а при удвоении ширины размер REGION может вообще не измениться. Количество структур SCAN и размеры REGION напрямую влияют на работу механизма GDI, на использование памяти драйверами устройств и на общее быстродействие, особенно в режимах печати с высоким разрешением на качественных принтерах. В примере из табл. 13.12 следует обратить внимание на первую и последнюю структуры SCAN. Они не соответствуют фрагментам региона, то есть не содержат
Глава 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 для создания круглого региона с координатами О, О, 100, 100 вам возвращается регион с ограничивающим прямоугольником О, О, 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 используется абстрактная структура CLIPOBJ. Для получения прямоугольников, образующих регион, необходимо вызвать функцию CLIPOBJ_bEnum.
Траектории в механизме GDI Траектория (path) представляет собой совокупность фигур (или геометрических форм), к которой применяются операции заливки, обводки или заливки с одновременной обводкой. Для создания траектории можно воспользоваться средствами Win32 API, однако вы даже не получите манипулятора созданного объекта. Любой нормальный программист понимает, что для представления траектории в процессе построения и при последующем использовании в GDI требуется какая-то внутренняя структура данных. Графический механизм Windows (win32k.sys) даже экспортирует довольно большую группу функций для выполнения операций с объектами траекторий в драйверах устройств. По данным расширения отладчика GDI, объекты траекторий присутствуют в таблице объектов GDI. Команда dumpobj PATH выводит информацию обо всех объектах траекторий в системе. Расширение отладчика GDI не содержит команд для расшифровки манипулятора объекта траектории или соответствующей структуры данных режима ядра (в этом отношении траектории также отличаются от других типов объектов GDI). Команда dpo расшифровывает только структуру PATHOBJ, передаваемую функциям механизма GDI или функциям драйверов устройств — например, EngStrokePath или DevStrokeAndFillPath. Приведенная ниже информация о структурах данных, представляющих траектории в механизме GDI, была получена с использованием нескольких тесто-
221
Структуры данных режима ядра
вых объектов траекторий, а также документации Win32 API и DDK. Основной структуре было присвоено имя PATH. // Windows 2000. переменный размер typedef struct _PATHDT _PATHDT * pNext; _PATHDT * pLast: unsigned flags; unsigned pointno: POINTFIX point[1]: } PATHDT:
// // // // //
000 004 008 OOc 010
// // // //
000 004 008 010
// Windows 2000. переменный размер typedef struct unsigned void * unsigned PATHDT PATHDEF:
unk_00: pTail: nAllocSize: pathdt[l]:
// Windows 2000. ? байт typedef struct HGDIOBJ void * ULONG ULONG
hHmgr: pentry; cExcLock; Tid;
// 000, заголовок объектов GDI режима ядра // 004 // 008 // OOc
PATHDEF * SEGMENT * PATHDT * PATHDT * RECTFX POINTFX ULONG unsigned
ppachain: pFirst: ppfirst: pplast: rcfxBoundBox; ptfxSubPathStart: nCurves; unk_38[10];
// 010 // 014 // 014 // 018 // Olc
// 02c // 034 // 038
} PATH;
Структура 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 для первой структуры в списке. В поле fI ags хранятся общие атрибуты точек. Флаги, используемые в этом поле, документируются в Windows NT/2000 DDK при описании структуры PATHDATA (табл. 3.13). Таблица 3.13. Флаги PATHDT PATH DT.f lags
Значение
Описание
PD_BEGINSUBPATH
0x0001
Первая точка начинает новую субтраекторию (фигуру)
PD_ENDSUBPATH
0x0002
Последняя точка завершает субтраекторию (фигуру)
PD_RESETSTYLE
0x0004
Сбросить стиль в начале новой субтраектории
PD_CLOSEFIGURE
0x0008
Добавить линию, соединяющую последнюю точку субтраектории (фигуры) с первой точкой
PD_BEZIERS
0x0010
Группы из трех точек описывают кривую Безье, а не сегмент линии
Итак, траектория является объектом GDI, как регион или DDB. Перед использованием объектов GDI при вызове графических функций их необходимо выбрать в контексте устройства. Траектории, в отличие от других объектов GDI, не имеют специальной функции выбора — они неявно выбираются при создании. В момент создания новой траектории старая траектория в контексте устройства уничтожается. Впрочем, механизм GDI все равно должен хранить манипуляторы траекторий для разных контекстов, поэтому манипулятор объекта траектории для заданного контекста устройства хранится в поле hpath структуры DEVLEVEL. За этим полем также следует поле флагов, flPath, и структура LINEATTRS для описания атрибутов линии.
Структуры данных режима ядра
223
Рассмотрим пример — небольшой фрагмент кода Win32, в котором создается траектория: const POINT Points[3] = { {200.50}. (250. 150}. {300, 50} }: BeginPath(hDC): MoveToExfhDC. 100, 100. NULL); LineTo(hDC. 150. 150); PolyBezierTo(hDC. & POINTS[0]. 3); EndPath(hDC):
При помощи расширения отладчика GDI можно провести поиск всех объектов траекторий в системе. Воспользуйтесь командой dumpobj PATH, а затем введите команду dt <манипулятор_СО1>, чтобы вывести элемент таблицы объектов GDI, соответствующий конкретному манипулятору. Из выходных данных команды берется указатель на структуру PATH, содержащую указатель на структуру PATHDEF. Структура PATHDEF определяется следующим образом: // Пример структуры PATHDEF 0000; unkJO OxOOOOOOOO' 0004: pTail & pathdt[2] 0008: nAllocSize Oxfc OOOc: 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 состоит из двух точек с флагами PD_BEGINSUBPATH|PD_RESETSTYLE. Итак, перед нами две точки, образующие отрезок. Вторая структура PATHDT состоит из трех точек с флагами PD_ENDSUBPATH|PD_BEZIERS. Она описывает одну кривую Безье, которая продолжается из предыдущей точки и завершает субтраекторию. Структура PATHDEF точно воспроизводит все параметры, указанные в коде Win32. Теперь мы знаем, что структура PATH позволяет представить отрезки и кривые Безье, а также их произвольные комбинации. Например, вызовы функций CloseFigure, LineTo, MoveToEx, PolyBezier, PolyBezierTo, Polygon, PolylineTo, PolyPolygon и PolyPolyline легко преобразуются в последовательности отрезков и кривых Безье. С другой стороны, Windows 95/98 позволяет включать в построение траекторий вызовы TextOut и ExtTextOut. Как в структуре PATH представляется текст? Оказывается, при построении траекторий можно использовать только шрифты TrueType, и в траектории записываются только контуры текстовых строк, которые фактически представляют собой кривые Безье. Кроме перечисленных функций Windows NT/2000 позволяет включать в траекторию эллиптические кривые. Например, при построении траектории можно использовать функции AngleArc, Arc, ArcTo, Ellipse, Pie и т. д. Как механизм GDI решает эту задачу? Эллиптические кривые разбиваются на последовательности кривых Безье по аналогии с тем, как непрямоугольные регионы разбиваются на
Глава 3. Внутренние структуры данных GDI/DirectDraw
группы строк развертки. Предположим, перед вызовом EndPathO в приведенный выше фрагмент включаются две дополнительные команды: CloseFigure(hDC); E111pse(hDC. -100. -100. 100. 100);
' Функция CloseFlgureO завершает вторую структуру PATHDT (см. выше). Функция E l l i p s e O добавляет в список еще одну структуру PATHDT — группу кривых Безье из 13 точек. Первая точка начинает новую фигуру, а остальные 12 точек образуют 4 кривых Безье. Механизм GDI аппроксимирует эллипс при помощи 4 кривых Безье. Определения 13 контрольных точек выглядят следующим образом: { 99. -0.5 }, { 99. -55.4375 { -55.5. -100 {-100, 54.4375 { 54.5, 99
54.5, -100 }. -100, -55.4375 }. -55.5. 99 }. 99, 54.4375 },
{ -0.5. -100 }, { -100. -0.5 }, { -0.5. 99 }. { 99. -0.5 }.
Становится понятно, почему в структуре PATH используются FIX-координаты. Округление координат до целых чисел приведет к искажению формы эллипса. Структура PATH используется не только для хранения траекторий в Win32 GDI. Она также играет очень важную роль в DDI (интерфейсе между механизмом GDI и драйверами графических устройств). В частности, вызовы функций рисования линий (такие, как LineTo и PolyBezier) преобразуются в вызовы функции DrvStrokePath, которой передается указатель на структуру PATHOBJ. Функции с заливкой областей (например, Ellipse и Polygon) преобразуются в вызовы функции DrvStrokeAndFi 11 Path, которой также передается указатель на PATHOBJ. По сравнению с Windows NT 4.0 в Windows 2000 добавилась новая точка входа DrvLineTo, повышающая быстродействие для вызовов LineTo с целочисленными координатами конечных точек. Структура PATHOBJ также относится к числу «замаскированных» структур DDI и содержит только два открытых поля. Вы можете воспользоваться функциями GDI для получения информации о компонентах траектории, построения новых траекторий или расширения траектории посредством включения новых кривых. Например, функция EngCreatePath создает новый объект PATHOBJ; функция PATHOBJ_ bPolyBezier включает в траекторию кривые Безье; функция PATHOBJ_bEnum перечисляет записи компонентов траектории в структуре PATHDATA, очень похожей на описанную выше структуру PATHDT.
Шрифты в механизме GDI То что в Win32 API обычно именуется шрифтами (fonts), правильнее было бы называть «логическими шрифтами». Логические шрифты создаются функциями CreateFont, CreateFontlndirect и CreateFontDi rectEx. При вызове функции указываются характеристики, которыми должен обладать шрифт. GDI (а точнее — система подстановки шрифтов, font mapper) находит физический шрифт, в наибольшей степени соответствующий предъявленным требованиям.
225
Структуры данных режима ядра
Для ссылок на логические шрифты, создаваемые GDI, используются манипуляторы типа HFONT. В расширении отладчика GDI объекты шрифтов обозначаются типом LFONT. Например, команда dumpobj LFONT выводит список манипуляторов всех логических шрифтов в системе. Передавая манипулятор логического шрифта команде hel f, вы получите информацию о структуре данных, ассоциированной с этим манипулятором в адресном пространстве ядра. Команда просто выводит дамп соответствующей структуры LOGFONTW. // Windows 2000. ? байт typedef struct { HGDIOBJ hHmgr; // 000 000, заголовок объектов GDI void * pentry; // 004 ULONG cExcLock; // 008 ULONG Tid: // OOc unsigned unk_010[3]; PDEVJJIN32K * ppdev ; unsigned unk 020[8]: HGDIOBJ hPFE; unsigned unk 020[39]; WCHAR Face[32]: unsigned nSize: ENUMLOGFONTEXW enumlogfontex; } LFONT:
// // // // // // // //
010 Olc 020 040 044 OdO 110 114
На самом деле данные, хранимые в пространстве ядра для логических шрифтов, отнюдь не ограничиваются структурой LOGFONTW, показываемой расширением GDI. Даже функция GetObject возвращает для манипулятора логического шрифта структуру из 356 байт, больше напоминающую структуру ENUMLOGFONTEXW. Ее первым полем действительно является структура LOGFONTW. Другое поле, заслуживающее внимания, — указатель на структуру физического устройства механизма GDI. Следовательно, в механизме GDI структура LFONT, поддерживаемая для логического шрифта, фактически представляет собой структуру LOGFONTW с несколькими дополнительными полями, образующими структуру ENUMLOGFONTEXW, что вполне разумно. Но где же хранится информация о соответствии между логическими и физическими шрифтами? И как информация о шрифтах передается функциям драйверов графических устройств — например, DrvTextOut? Расширение отладчика GDI показывает еще одну недокументированную структуру данмых GDI — PFE. Манипулятор структуры PFE хранится в поле hPFE каждой структуры LFONT. Вы можете получить список всех манипуляторов PFE при помощи команды dunpobj PFE расширения отладчика GDI, а затем воспользоваться командой pfe Для получения информации о структуре ядра PFE. Структура ядра PFE выгля' следующим образом: // Windows 2000. 108 (Охбс) байт struct PPF: typedef struct HGDIOBJ
hHmgr;
// 000. заголовок объектов GDI режима ядра
226
Глава 3. Внутренние структуры данных GDI/DirectDraw
void * ULONG ULONG
pentry; cExcLock : Tid:
PFF * pFFF: iFont; ULONG ЛРРЕ: ULONG FD_GLYPHSET * pfdg: void * unk 020: IFIMETRICS * pifi: unsigned idifi; pkp: void * unsigned idkp: ckp: unsigned iOrientation: unsigned unsigned cjEfdwPFE:
void * unsigned unsigned unsigned unsigned unsigned unsigned void * unsigned unsigned unsigned
pgiset:
ulTimeStamp; ufi: unk 04c: pid; ql: unk_058: pFl Entry; cAlt; cPfdgRef;
// 004 // 008 // OOc // 010. pff // 014
// 018 // Olc.
gs
// 020 f8ddef60 // 024. ifi
// 028 // 02c
// // // // // // // //
030 034 038 03c 040 044 048 04c
// 050
// // // // // a i Family Name; //
054 058 05c 060 064 068
} PFE;
Структура PFE начинается со стандартного заголовка объектов GDI длиной 16 байт. Поле pPFF ссылается на структуру PFF, содержащую информацию о физическом файле шрифта. Структура PFF описывается ниже в этом разделе. В поле pfdg хранится указатель на структуру FD_GLYPHSET, документированную в SDK. Структура FD_GLYPHSET определяет отображение символов Unicode на внутренние манипуляторы глифов. Символы Unicode представляются 16-разрядными значениями. Кодировка Unicode поддерживает тысячи разнообразных символов, а шрифты могут ограничиваться небольшим подмножеством этой кодировки. Шрифт представляет собой совокупность глифов, каждому из которых присвоен уникальный манипулятор. При помощи структуры FD_GLYPHSET механизм GDI устанавливает соответствие между символами Unicode и манипуляторами глифов. В расширении отладчика GDI предусмотрена команда gs для расшифровки структуры FD_GLYPHSET. Например, для шрифта Small Fonts (smallf.fon) эта команда показывает, что шрифт состоит из 224 глифов. Глифу символа «пробел» соответствует манипулятор 0, глифу символа «А» — манипулятор 0x21 и т. д. Структура FD_GLYPHSET создается точкой входа шрифтового драйвера DrvQueryFontTree, когда параметр iMode равен QFT_GLYPHSET. Также обратите внимание на поле p i f i , в котором хранится указатель на структуру IFIMETRICS, также документированную в DDK. Структура IFIMETRICS содержит сведения о гарнитуре, используемые GDI. В частности, в ней хранятся имена семейства, стиля и гарнитуры, уникальное имя, возможности эмуляции, идентификатор внедрения и, наконец, 10-байтовый массив panose с описанием визуальных характеристик шрифта. Структура IFIMETRICS заполняется функци-
227
Структуры данных режима ядра
ей DrvQueryFont. В расширении отладчика GDI предусмотрена команда i f i , предназначенная для расшифровки структуры IFIMETRICS. Например, для шрифта Small Fonts команда возвращает информацию о растровом формате 1 бит/пиксел, о возможности масштабирования с целочисленным коэффициентом и поворотах на 90°, а также об эмуляции полужирного, курсивного и полужирного курсивного начертаний. Логический шрифт связывается с конкретным процессом Win32. При уничтожении процесса все его манипуляторы GDI уничтожаются, а элементы таблицы объектов освобождаются для повторного использования. Однако манипуляторы PFE существуют на уровне системы и не ассоциируются с конкретными процессами. С одной структурой PFE может быть связано несколько логических шрифтов. Структура PFF описывает файл физического шрифта. Как нетрудно предположить, в расширении GDI также имеется команда pff для расшифровки этой структуры. Определение структуры PFF выглядит так: struct RFONT;
typedef struct _PFF sizeofThis; pPFFNext ; pPFFPrev; pwazPathName cwc; cFiles; unk_018[2]: fl State; cLoaded; cNotEnum; cRFONT: prfntList: hff; void * hdev; unsigned dhpdev;
ULONG PFF * PFF * WCHAR * ULONG ULONG unsigned ULONG ULONG ULONG ULONG RFONT * void *
void * void * void * void *
pfhFace: pfhFamily; pfhUFI; pPFT;
// 000 // 004. pff // 008. pff
// // // // // // // // // // // //
OOc 010 014 018 020 024 028 02c 030. fo 034 038 03c
// 040 // 044
// 048 // 04c.
pft
ULONG ulCheckSuml : // 050 // 054 unsigned unk 054; ULONG cFonts; // 058 void * ppfv; // 05c void * pPvtDataHead; // 060 unsigned unk 064; // 064 PFE * pPFE; // 068. pfe WCHAR wszStrings[l]; // Обе } PFF; 'i 'Структуры PFF в механизме GDI объединяются в двусвязные списки. Ссылки "И следующий и предыдущий элементы хранятся соответственно в полях pPFFNext * pPFFPrev. Следующее поле содержит указатель на имя файла шрифта на дис-
228
Глава 3. Внутренние структуры данных GDI/DirectDraw
ке - например, «\??\C:\WIN2000\FONTS\SMALLF.FON», для которого в поле fl State установлен флаг PFF_STATE_PERMANENT_FONT. В поле cLoaded хранится признак, который показывает, был ли файл загружен в память; в поле cRFONT хранится количество реализованных шрифтов, созданных на основании физического шрифта, а поле prfntList ссылается на первый элемент списка реализованных шрифтов. •Поле pPFT содержит указатель на структуру PFT, которая представляет собой таблицу структур PFF. Структура PFT расшифровывается командой pft и выглядит следующим образом: typedef struct
void * void * void * ULONG ULONG PFF * PFT;
pfhFamily: pfhFace; pfhUFI: cBuckets; cFiles; apPFF[l]:
// // // // //
000 004 008 OOc 010
// 014
В первых трех полях структуры PFT хранятся указатели на три хэш-таблицы, расшифровываемые командой fh. Хэш-таблицы предназначены для быстрого установления соответствия между логическими и физическими шрифтами. В структуре 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.TIF" "\??\С:\WIN2000\FONTS\CGA80WOA.FON" apPFF[3] "\??\C:\WIN2000\FONTS\MICROSS.TTF" apPFF[5] "\??\C:\WIN2000\FONTS\PALA.TIF" apPFF[98] "\??\C:\WIN2000\FONTS\TIMES1.TIF" Настало время описать самую важную шрифтовую структуру графического драйвера, FONTOBJ, и ее расширенную версию RFONT. Выше говорилось о том, что структура LFONT описывает логический шрифт, то есть запрос на получение шрифта с заданным размером и углом поворота, особыми характеристиками (например, насыщенностью) и т. д. С другой стороны, шрифтовой файл, описываемый структурой PFF, представляет собой общий шаблон, который может масштабироваться для разных размеров, поворачиваться на разные углы и дополняться другими специфическими возможностями. В простейшем варианте для каждого символа текстовой строки механизм GDI обращается к шрифтовому драйверу за описа-
229
Структуры данных режима ядра
нием общего контура символа, масштабирует его до нужного размера, поворачивает на нужный угол, преобразует в растр, использует и забывает о его существовании. Однако в общем случае такая схема крайне неэффективна, особенно если учесть, что для однобайтовых шрифтов с небольшим количеством символов легко организуется кэширование, экономящее массу времени по многократному построению растров для каждого шрифта. Для этого в механизме GDI используется структура RFONT. Структура RFONT описывает конкретную реализацию или, если хотите, — конкретный экземпляр шрифта. Это не логический и не физический шрифт, а набор глифов, созданных в соответствии с требованиями логического шрифта на основании общего описания, взятого из шрифтового файла. Первая часть структуры RFONT документируется в DDL как структура FONTOBJ, это сделано для ускорения обращений со стороны графических драйверов. К остальным полям структуры RFONT можно обращаться только посредством специальных методов структуры FONTOBJ — таких, как FONTOBJ_cGetG1 yphHandl es и FONTOBJ_cGetGlyphs. В расширении отладчика GDI расшифровка структуры RFONT выполняется при помощи команды fo. Ниже приведено объемистое определение структуры RFONT. typedef struct 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; bSmall Metrics; iMax: i First: cBits;
Struct RFONT FONTOBJ ULONG ULONG ULONG PVOID ULONG PVOID DHPDEV PFE *
f Ob j: iUnique; flType: ulContent: hdevProducer: hDeviceFont; hdevConsumer; dhpdev; ppfe:
// 000
// 02c // 030 // 034 // 038
// 03c // 040 // 044 // 048
230
Глава 3. Внутренние структуры данных GDI/DirectDraw
PFF * FD XFORM 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: // 04c fdx; // 050 cBitsPerPel; //'060 mxWorldToDevice; // 064 iGraphicsMode: // OaO eptflNtoWScale x i; // Oa4 eptflNtoWScale у i; // Oac bNtoWIdent; // Ob4 xoForDDI_pmx; // Ob8 xoForDDI ulMode; // Obc unkJOO; // OcO mxForDDI: // Oc4 flRealizedType; // 100 ptlUnderlinel: // 104 ptl StrikeOut: // Юс ptlULThickness; // 104 ptlSOThickness: // Юс ICharlnc: f xMaxAscent : fxMaxDescent; fxMaxExtent ; ptfxMaxAscent: ptf xMaxDescent ; cxMax: IMaxAscent: IMaxHeight: cyMax; cjGlyphMax; fdxQuantized; INonLinearExtLeading; 1 NonLi nearlntLeadi ng ; 1 NonLi nearMaxCharWi dth ; 1 NonLi nearAvgCharWi dth ; ul Orientation; pteUnitBase x; pteUnitBase y: efWtoBase; 1 Ascent; pteUnitAscent x; pteUnitAscent_y; efWtoDAscent ; ef DtoWAscent ; efWtoDEsc: efDtoWEsc: efEscToBase: efEscToAscent; fllnfo; hgBreak; fxBreak ; pfdg; wcgp; cSelected; rflPDEV prfntPrev: rflPDEV_prfntNext: rf 1 PFF_prf ntPrev ;
Структуры данных DirectDraw
RFONT * void * CACHE POINT ULONG FLOATOBJ FLOATOBJ TEXTMETRICW LONG LONG LONG ULONG ULONG RFONT * RFONT * RFONT * void * void * ULONG ULONG ULONG ULONG ULONG ULONG
231
rf!PFF_prfntNext; hsemCache: cache; ptlSim; bNeededPaths; efDtoWBase_31: ef DtoWAscentJl: 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_DRAW_TYPE, CLIOBJJYPE и SPOOLJYPE. Объекты, относящиеся к DirectDraw, описаны в следующем разделе. Другие объекты в этой главе не рассматриваются, поскольку они либо не играют особой роли для программирования Win32, либо устарели с развитием ОС Windows, либо мы не располагаем средствами для создания их экземпляров. Вскоре вы убедитесь, что знание внутренних структур данных GDI помогает глубже понять программирование для Win32 GDI API.
Структуры данных DirectDraw «Дайте мне манипулятор, и я покажу вам структуру данных». Собственно, именно эта задача и решалась в данной главе применительно к объектам GDI. Мы выяснили, что в системе существует глобальная таблица объектов GDI, что GDI
232
Глава 3. Внутренние структуры данных GDI/DirectDraw
создает для некоторых объектов структуры данных в адресном пространстве пользовательского режима, и для всех объектов создаются структуры данных, которые механизм GDI хранит в адресном пространстве режима ядра. При помощи расширения отладчика GDI мы постепенно исследуем недокументированные связи между GDI и DDL А теперь перейдем к 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_OK) { lpdd->SetCooperativeLevel(hWnd. DDSCLJORMAL): DDSURFACEDESC ddsd; ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps - DDSCAPS_PRIMARYSURFACE: LPDIRECTDRAWSURFACE 1pddspri тагу: hr = lpdd->CreateSurface(&ddsd. &lpddsprimary. NULL): if ( hr == DD_OK) { char mess[MAX_PATH]: wsprintf(mess. "DirectDraw object at *x. vtable at *x\n". "DirectDraw surface object at £x. vtable at %x". Ipdd. * (unsigned *) Ipdd, Ipddsprimary. * (unsigned *) Ipddsprimary): MessageBox(NULL. mess. "DirectDrawTest". MB_OK); 1pddsprimary->Release(): } lpdd->Release();
} return hr:
233
Структуры данных DirectDraw
Приведенный фрагмент создает объект DirectDraw и объект поверхности DirectDraw, а затем выводит их адреса и указатели на таблицы виртуальных функций. Если вставить фрагмент в программу и выполнить его, на экране появляется окно сообщения с текстом следующего вида: DirectDraw object at 7e2alO. vtable at 728405aO 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 7298ЕС16: ddraw.dll!DD_Release 72980C5A: ddraw.dl1!DD_Compact 7297C82B: ddraw.dll!DD_CreateClipper Уловили? Принцип использования адресов и таблиц функций в СОМ очень похож на интерфейс DDI между механизмом GDI и графическими драйверами, хотя он в значительно большей степени формализован. Теперь давайте посмотрим, как DirectDraw отражается в таблице объектов GDI. Для этого мы воспользуемся верным расширением отладчика GDI под управлением нашей собственной программы Fosterer. Дважды выполните команду dumpdd — перед выполнением приведенного выше фрагмента и когда окно сообщения находится на экране (то есть когда объекты DirectDraw еще не освобождены). Результат предугадать нетрудно — мы обнаруживаем два новых типа объектов, DD_DIRECTDRAW_TYPE и DD_SURFACE_TYPE. При реализации DirectDraw в GDI все равно используются манипуляторы, хотя и скрытые интерфейсными указателями. Очевидно, DD_DIRECTDRAW_TYPE соответствует объекту DirectDraw, a DD_SURFACE_ TYPE — объекту поверхности DirectDraw. Начнем с рассмотрения объекта DirectDraw. Список всех объектов DirectDraw выводится командой dumpddobj DDRAW. Структура данных режима ядра расшифровывается командой dddlocal, которая выводит имя структуры — EDD_DIRECTDRAW_LOCAL. Механизм GDI различает глобальную структуру данных DirectDraw и структуру данных DirectDraw, существующую на уровне процесса. Ниже приведено определение структуры EDD_DIRECTDRAW_LOCAL. // Windows 2000. 72 байта typedef struct HGDIOBJ void * ULONG ULONG
hHmgr: pentry: cExcLock; Tid:
EDD_DIRECTDRAW_GLOBAL * peDirectDrawGlobal: EDO DIRECTDRAW GLOBAL * peDirectDrawGlobal2:
// // // //
000. заголовок GDI 004 008 OOc
// 010 // 014
234
Глава 3. Внутренние структуры данных GDI/DirectDraw
EDD_SURFACE * peSurface Ddlist; unsigned unk_01c[2]; EDD DIRECTDRAW LOCAL * peDi rectDrawLocal Next ; FLATPTR fpProcess: FLONG fl; HANDLE UniqueProcess; PEPROCESS Process; unsigned unk_038[2]: void * unk_040 ; unsigned unk_044; } EDD_DIRECTDRAW_LOCAL;
// // // // // // // // //
018 Olc 024 028 02c 030 034 038 040
// 044
В поле UniqueProcess структуры EDD_DIRECTDRAW_LOCAL хранится идентификатор процесса. Поле Process содержит указатель на объект ядра, связанный с процессом. Более того, объект DirectDraw связывается с создавшим его потоком через поле Tid (в отличие от большинства объектов GDI, у которых поле Tid обычно равно 0). Механизм GDI также поддерживает один экземпляр глобальной структуры данных EDD_DIRECTDRAW_GLOBAL, управляющей глобальной информацией состояния DirectDraw. В EDD_DIRECTDRAW_LOCAL указатели на эту структуру встречаются дважды. Как правило, один процесс DirectDraw создает несколько поверхностей DirectDraw. Объекты ядра этих поверхностей объединяются в односвязный список, начинающийся с поля peSurface_DdList. Все объекты DirectDraw, в данный момент существующие в системе, также связываются в список при помощи поля peDirectDrawLocalNext. Структура EDD_DIRECTDRAW_LOCAL стоит во главе иерархии всех объектов процесса, относящихся к DirectDraw, а также содержит ссылки на другие глобальные объекты из семейства DirectDraw. Сетевая иерархия структур DirectDraw позволяет координировать их работу. Структура EDD_DIRECTDRAW_GLOBAL расшифровывается командой 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: i Original Type; unk_040[5]; pRange: pRangeLimit; psoCoinposite: unk_060[66]; prgnUnlocked: unk_16c[28] :
// Windows 2000. 1552 (0x610) байт
// 0x000 // 0x004 // 0x008 // OxOOc // 0x010 // 0x014 // 0x038 // ОхОЗс // 0x040 // 0x054 // 0x058 // Ox05c // 0x060 // 0x168 // Oxl6c
235
Структуры данных DirectDraw
typedef struct
/ t
void * DWORD DWORD unsigned LONG unsigned LONGLONG DWORD VIDEOMEMORY * DWORD DWORD *
dhpdev ; dwReservedl ; dwReserved2 ; unk_00c[3] ; cDri verReferences ; unk_01c[3]; 1 1 As sertModeTi meout : dwNumHeaps : pvntist; dwNumFourCC; pwdFourCC ;
ddhalinfo; unk_leO[44]: ddcall backs; ddsurfacecall backs; DD'SURFACECALLBACKS ddpalettecallbacks: DD'PALETTECALLBACKS unk_314[48] : unsigned dSdnthalcallbacks; D3DNTHAL_CALLBACKS unk_460[7] ; unsigned d3dnthalcallbacks2; D3DNTHAL_CALLBACKS2 unk_498[18]; unsigned DD_MISCELLANEOUSCALLBACKS ddmi seel 1 aneouscal 1 backs ; unk_4ec[18]; unsigned d3dnthalcallbacks3; D3DNTHAL_CALLBACKS3 unk_54c[23]; unsigned
DD_HALINFO unsigned DD CALLBACKS
// 0x000 // 0x004 // 0x008 // OxOOc // 0x018 // OxOlc // 0x028 // 0x030 // 0x034 // 0x038 // ОхОЗс // 0x040 // OxleO // 0x290 // Ox2c4 // 0x304 ? // 0x314 // Ox3d4 // 0x460 // Ox47c // 0x498 // Ox4eO // Ox4ec // 0x534 // Ox54c
// Ox5a8 peDi rectOrawLocal Li st ; EDO DIRECTDRAWLOCAL * // OxSac peSurface LockList; EDO SURFACE * // Ox5bO FLONG fl: // Ox5b4 ULONG cSurfaceLocks; // Ox5b8 pAssertModeEvent; PKEVENT // OxSbc peSurfaceCurrent; EDD SURFACE * // Ox5cO peSurfacePrimary; EDD SURFACE * // 6x5c4 BOOL bSuspended; // Ox5c8 unk_5c8[12]; unsigned // 0x5 f 8 RECTL rcBounds; HDEV // 0x608 hdev; // ОхбОс unsigned unk_60c : } EDD_DIRECTDRAW_GLOBAL; Структура EOD_DIRECTDRAW_GLOBAL содержит практически всю информацию о поддержке DirectDraw, которую должен знать механизм GDI. В поле dhpdev хранится манипулятор структуры PDEV драйвера устройства, возвращаемый при вызове DrvEnablePDEV. Обычно он представляет собой указатель на закрытую структуру данных физического устройства. В структуру EDO_DIRECTDRAW_GLOBAL включается несколько других структур, полученных механизмом GDI от драйвера экрана. Поле d d h a l i n f o содержит структуру DD_HALINFO, возвращаемую функцией DrvGetDirectDrawInfo и описывающую возможности оборудования и драйвера. В полях ddcall backs, ddsurfacecall • ,b«*s и ddpalettecall backs хранятся структуры DD_CALLBACKS, DD_SURFACECALLBACKS и •«^.PALETTECALLBACKS, возвращаемые функцией DrvInableDirectDraw. Другая группа
236
Глава 3. Внутренние структуры данных GDI/DirectDraw
структур относится к функциям трехмерной графики DirectDraw. Они передают механизму GDI информацию о точках входа DirectDraw, поддерживаемых драйвером. Таким образом, механизм GDI знает, какие функции следует вызывать при создании поверхности, назначении цветовых ключей, отображении адресов видеопамяти, переключении поверхностей и т. д. В структуре EDD_DIRECTDRAW_GLOBAL хранится немало другой интересной информации — например, список объектов DirectDraw, список заблокированных поверхностей, указатель на текущую поверхность и т. д. Функция EDD_DIRECTDRAW_GLOBAL является частью структуры PDEV_WIN32K, описанной в разделе «WinDbg и расширение отладчика GDI». Структура PDEV_WIN32K также включает структуру SPRITESTATE. Разобравшись с тем, как в механизме GDI организовано хранение общих данных DirectDraw (как глобальных, так и данных уровня процесса), давайте посмотрим, что же скрывается за поверхностями DirectDraw. С каждым объектом поверхности DirectDraw связывается соответствующая структура данных, скрытая от пользователя. В выходных данных команды dumpddobj эти структуры обозначаются типом DD_SURF_TYPE. Команда dumpddobj SURF расширения GDI выводит все манипуляторы поверхностей DirectDraw. При вызове команды dddsurface для конкретного манипулятора поверхности выводится структура данных режима ядра EDD_SURFACE. typedef struct
HGDIOBJ void * ULONG ULONG
hHmgr; pentry: cExcLock: Tid;
// // // //
000, 004 008 OOc
DO_SURFACE_LOCAL DD SURFACE MORE DD SURFACE GLOBAL DD_SURFACE_INT
ddsurfacelocal :
// // // //
010 04c 068 Ob4
EDD_SURFACE * EDD_SURFACE * unsigned EDD_DIRECTDRAWGLOBAL * EDO DIRECTDRAWLOCAL * FLONG unsigned ULONG
peSurface DdNext: peSurface_LockNext: unk_OcO: peDirectDrawGlobal : peDirectDrawLocal : fl: unk_OdO ; iVisRgnUniqueness: unk_0d8: hSecure; unk_OeO : hbmGdi : unk_0e8: rclLock; unk Ofc[3]:
unsigned
HANDLE unsigned HBITMAP unsigned ERECTL
unsigned
EDD SURFACE:
ddsurfacemore:
ddsurfaceglobal : ddsurfaceint:
// Ob8 // OcO
// OdO
// Oe4 // Dec: // Ofc
За стандартным заголовком объектов GDI режима ядра в структуре EDD_ SURFACE следуют четыре структуры, документированные в Windows 2000 DDK:
Структуры данных DirectDraw
237
DD_SURFACE_LOCAL, DD_SURFACE_MORE, DD_SURFACE_GLOBAL и DD_SURFACE_INT. Структура DD_ SURFACE_GLOBAL содержит информацию, общую для нескольких поверхностей — шаг (pitch), высота, ширина и координаты х/у. Структура DD_SURFACE_LOCAL содержит данные, относящиеся к конкретному объекту поверхности — первичный и вторичный буферы, цветовые ключи, формат пикселов, присоединенные поверхности и т. д. Структура DD_SURFACE_MORE содержит дополнительные данные уровня поверхности — такие, как сведения о видеопорте и флаги оверлеев. Последняя структура, DD_SURFACE_INT, содержит указатель на структуру DD_SURFACE_LOCAL. За документированными структурами поверхностей DirectDraw следуют указатели на следующую поверхность в списке, глобальные и локальные данные DirectDraw. В поле hbmGdi иногда хранится манипулятор DDB. Мы знаем, как устроены некоторые структуры данных DirectDraw режима ядра; но как они используются? Обработка графических команд DirectDraw (например, переключения поверхностей) обычно начинается с интерфейсного указателя на поверхность DirectDraw. По интерфейсному указателю на поверхность определяется манипулятор DD_SURF_TYPE объекта GDI и передается механизму GDI. Механизм находит структуру EDD_SURFACE и получает указатель на структуру EDD_DIRECTDRAW_GLOBAL, в которую входит структура DD_SURFACECALLBACKS. В структуре DD_SURFACECALLBACKS хранится указатель на точку входа драйвера экрана, обрабатывающую переключение поверхностей и вызываемую механизмом DirectDraw. Функции переключения передается структура DD_FLIPDATA, которая собирается по данным из исходной и целевой структур EDD_SURFACE. За подробностями обращайтесь к описанию 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 режима ядра
Отслеживание вызовов функций Win32 API
241
Отслеживание вызовов функций Win32 API
Глава 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
Методика перехвата и отслеживания не так уж редко встречается в 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-программировании типы данных обновляются очень часто. Желательно, чтобы утилита отслеживания умела преобразовывать коды бинарных растровых операций в имена типа SCRCOPY, сохранять растры в файлах или, скажем, сообщать о том, что манипулятор GDI соответствует объекту логического пера. О Хронометраж. Возможность измерения времени, потраченного на обработку вызова Win32 API, поможет оптимизировать программу и исключить из нее нежелательные вызовы. О Недокументированные функции API, внутримодулъные вызовы, вызовы системных функций, вызовы кода режима ядра. Отсутствие поддержки этих возможностей является одной из слабостей готовых программ. Если вы хотите действительно глубоко разобраться в какой-либо области Windows-программирования (например, в графическом программировании), обойтись без хорошей программы мониторинга практически невозможно.
Построение программы мониторинга Программа мониторинга обычно состоит из двух частей: управляющей программы и разведчика (DLL или драйвера). Управляющая программа засылает разведчика в нужное место, отдает ему команды и, возможно, получает информацию. Разведчик проникает «в тыл» пользовательского процесса, закрепляется .в нужном месте, собирает мельчайшие обрывки информации из интересующей области, действует в соответствии с поставленной задачей или передает информацию управляющей программе. На рис. 4.1 изображена схема работы такой программы. Конечно, у этой общей модели существует немало разновидностей. Если вы найдете надежный способ внедрения разведчика, чтобы он мог действовать самостоятельно, возможно, управляющая программа вам и не понадобится. Например, некоторые среды с двухбайтовой кодировкой символов существуют «поверх» обычной системы Windows. Вместо внедрения DLL во все приложения, обладающие графическим интерфейсом, они просто переименовывают системные DLL и заменяют их собственными реализациями, обеспечивающими поддержку двухбайтовой кодировки в однобайтовой системе. Если вы хотите проследить за операциями, происходящими в адресном пространстве режима ядра, вам наверняка понадобится драйвер устройства (то есть разведывательная DLL) режима ядра. В этом случае управляющая программа устанавливает драйвер и управляет его работой. Например, в программе Fosterer из главы 3 драйвер Periscope режима ядра использовался для чтения данных из адресного пространства ядра и последующего анализа структур данных графической системы, хранящихся в режиме ядра. SoftICE/W, отладчик системного уровня от компании Numega, также использует драйвер режима ядра для обеспечения возможностей отладки общесистемного уровня на одном компьютере.
243
Отслеживание вызовов функций Win32 API
Процесс 1
Процесс 2
Управляющая программа
Программа под наблюдением
Информация
Наблюдатель DLL/Драйвер
Команды Системная DLL
Рис. 4.1. Компоненты программы мониторинга
При написании программы-разведчика необходимо решить несколько задач: О внедрение разведчика в процесс; О подключение к цепочкам вызовов функций API; О получение параметров, возвращаемых значений и данных хронометража; О сохранение данных в удобном формате; О создание пользовательского интерфейса для выбора программ и модулей, за которыми вы хотите наблюдать, а также перехватываемых функций Win32 API и методов СОМ. В этом разделе мы создадим программу Pogy, предназначенную для общего мониторинга вызовов Win32 API. Программа названа в честь подводной лодки, участвовавшей в подводных научных исследованиях. Мы будем использовать Pogy для исследований глубин операционной системы Windows. Пользовательский интерфейс управляющей программы Роду.ехе оформлен в виде диалогового окна, состоящего из нескольких страниц. Наблюдением занимается DLL Diver.dll. А теперь давайте кратко рассмотрим строение этой программы.
Внедрение DLL-разведчика В Win32 API существует возможность установки перехватчиков (hooks) на системном уровне или на уровне программного потока. Перехватчики отслеживают сообщения или изменяют стандартные действия, выполняемые при их обработке. Установка перехватчиков выполняется функцией API SetWi ndowsHooksEx. В Windows 2000 количество классов перехватчиков даже увеличилось до 15. Скажем, при установке перехватчика класса WM_GETMESSAGE отслеживаются сообщения, поставленные в очереди сообщений, а перехватчик класса WH_SHELL получает оповещения о создании и уничтожении окон верхнего уровня.
244
Глава 4. Мониторинг графической системы Windows
Функции-перехватчики обычно реализуются в DLL — для перехватчиков системного уровня это является обязательным требованием. Причина заключается в том, что для работы перехватчика в других процессах его код должен загружаться в адресное пространство целевого процесса. Исполняемый файл может загружаться другим процессом только в виде данных, поэтому перехватчик системного уровня должен быть реализован в DLL. • После загрузки DLL в адресное пространство процесса перехватчик может вытворять практически все, что захочет. На этом факте основаны некоторые приемы отслеживания вызовов API. Впрочем, вы должны позаботиться о том, чтобы DLL оказалась в нужном месте. Функция SetWindowsHookEx является лишь одним из возможных способов внедрения DLL в исследуемый процесс. Впрочем, этот способ прост и хорошо документирован. Чтобы DLL внедрялась в каждый процесс, ее можно включить в следующий ключ реестра Windows NT/2000: HKEY_LOCAL_MACHINE\Software\Microsoft\ Windows NTACurrent Version\Windows\AppInit_DLLs
Знание нетривиальных способов внедрения DLL во внешние процессы является неплохим показателем квалификации в области Windows-программирования. В классической книге Мэтта Питрека (Matt Pietrek), «Windows 95 System Programming Secrets», продемонстрирован механизм внедрения DLL через API отладчика Win32 и динамическую модификацию кода исследуемого процесса. В книге Джеффри Рихтера (Jeffery Richter), «Programming Applications for Microsoft Windows» (5 издание), показано, как сделать то же самое с использованием удаленного программного потока. В нашей программе Pogy функция SetWindowsHookEx устанавливает перехватчик системного уровня, который представляет собой функцию косвенного вызова, определяемую приложением. После регистрации в системе перехватчик системного уровня вызывается при наступлении некоторых событий в системе, тогда как перехватчик уровня программного потока отвечает лишь за один поток. Функция-перехватчик ShellРгос реализуется в DLL Diver.dll, как это требуется для перехватчика системного уровня. Модуль Diver экспортирует функцию SetupDiver, вызываемую из управляющей программы Pogy.exe для выполнения установки, удаления и настройки взаимодействия между компонентами. Ниже приведена часть кода перехватчика, работающая на стороне DLL-разведчика. #pragma data_seg("Shared") HWND HHOOK fpragma fpragma
h_Controller = NULL: h_ShellHook = NULL: data_seg() comment(linker. "/section:Shared,rws")
LRESULT CALLBACK ShellProc( int nCode, LPARAM IParam ) { if ( nCode==HSHELL_WINDOWCREATED ) if ( . . . ) StartSpyO; assert(h Shell Hook);
WPARAM wParam.
Отслеживание вызовов функций Win32 API
245
if (h_ShellHook) return CallNextHookEx(h_ShellHook. nCode, wParam. IParam); else return FALSE;
void _declspec(dllexport) SetupDiver(int nOpt. HWND hWnd)
{
switch (nOpt) { case Diver_Install: assert(h_ShellHook==NULL);
h_ShellHook = SetWindowsHookEx(WH_SHELL. (HOOKPROC) ShellProc. hlnstance. 0): h_Contro1ler = hWnd: break;
case DiverJJnlnstall: assert(h_ShellHook!=NULL); UnhookWindowsHookEx(h Shel1 Hook); h_ShellHook break;
= NULL;
Перехватчик системного уровня регистрируется в системе (диспетчере окон) только один раз. Функция SetWindowsHookEx возвращает манипулятор, который используется функцией-перехватчиком и по которому в итоге перехватчик удаляется вызовом UnhookWi ndowsHookEx. Возникает проблема: если перехватчик системного уровня может загружаться в адресные пространства разных процессов, обычно изолированные друг от друга, где же тогда хранится манипулятор? Ответ: в секции общих данных той DLL, в которой определена функция перехвата. Обычная секция данных ЕХЕ-файла Win32 является закрытой для процесса, загрузившего DLL; иначе говоря, каждый процесс работает со своей собственной копией этой секции. Однако секция общих данных совместно используется всеми процессами, загрузившими DLL. В приведенном выше фрагменте начало и конец этой секции отмечены двумя директивами data_seg, а директива comment (linker) сообщает компоновщику о том, что эта секция доступна для чтения/записи и является общей («rws»). Мы сохраняем в общей секции манипуляторы перехватчика и окна. Пожалуйста, обратите внимание на необходимость инициализации данных общей секции. Управляющая программа Pogy.exe связана с той же DLL Diver.dll. При загрузке Pogy создает окно для взаимодействия с DLL-разведчиком. Далее Pogy вызывает функцию SetupDiver(Diver_Install,...), сообщая разведчику манипулятор своего окна и позволяя создать перехватчик. При вызове функции SetWindowsHookEx возвращается манипулятор перехватчика, необходимый для вызова следующего перехватчика в цепочке перехватов. Манипуляторы окна управляющей программы и перехватчика хранятся в DLL и поэтому доступны для всех пользовательских процессов. Таким образом, после присваивания значений h_Shel I Hook и h_Control 1 ег любой процесс может обратиться к этим переменным.
246
Глава 4. Мониторинг графической системы Windows
Однако к этому моменту библиотека Diver.dll загружена еще только в процесс управляющей программы. Функция перехвата вызывается лишь при создании или уничтожении окна верхнего уровня. Если это происходит в каком-то процессе, отличном от процесса управляющей программы, операционная система видит, что вызываемый перехватчик отсутствует в текущем процессе, и загружает DLL с перехватчиком. После загрузки DLL вызывается функция ShellРгос с кодом HSHELL_WINDOWCREATED. Функция Shell Ргос связывается с управляющей программой и определяет, следует ли начать отслеживание вызовов API. Главное, что требует операционная система от функции-перехватчика — чтобы она не забыла вызвать следующий перехватчик в цепочке функцией CallNextHookEx. В функции SetupDiver также предусмотрена возможность отключения перехватчика.
Подключение к цепочке вызовов функций API Получив от управляющей программы приказ о начале работы, DLL-разведчик инициализируется и создает скрытое окно. Манипулятор этого окна передается управляющей программе. С этого момента управляющая программа и разведчик могут обмениваться сообщениями посредством манипуляторов окон. В операционной системе Windows для обмена простыми сообщениями с двумя 32-разрядными параметрами задействуются коды пользовательских сообщений, начинающиеся с префикса WMJJSER. Но если вы захотите передать блок данных за границы процесса, обычный указатель не подойдет — указатель, относящийся к одному адресному пространству, в общем случае не работает в другом адресном пространстве. К счастью, для отправки блоков данных можно воспользоваться функцией WM_COPYDATA. Операционная система Windows специально обеспечивает правильность копирования блоков данных в сообщениях типа WM_ SETTEXT, WM_GETTEXT и WM_COPYDATA за границами процесса. Получив информацию о том, что DLL-разведчик создал коммуникационное окно, управляющая программа отправляет список отслеживаемых функций. Для каждой функции задается имя вызывающего модуля, имя вызываемого модуля, имя функции, количество параметров, типы параметров и тип возвращаемого значения. Например, если пользователь хочет отслеживать вызовы функции GDI SetTextColor из программы CLOCK.EXE, задаются следующие значения: О имя вызывающего модуля — CLOCK.EXE; О имя вызываемого модуля — GDI32.DLL; О имя функции — SetTextColor; О количество параметров — два; О типы параметров — НОС и COLORREF; О тип возвращаемого значения — COLORREF. По полученным данным DLL строит внутреннюю таблицу отслеживаемых модулей и функций. В главе 1 кратко рассматривался формат РЕ-файлов, используемых для представления модулей Win32 (находящихся как на диске, так и в памяти). При этом упоминалось, что при статической или динамической компоновке модулей используются каталоги экспорта и импорта, с хранением адреса каждой импор-
247
Отслеживание вызовов функций Win32 API
тируемой функции во внутренней переменной. Следовательно, чтобы подключиться к цепочке вызова функции Win32 API, необходимо лишь найти в каталоге импорта модуля тот адрес, по которому хранится адрес импортируемой функции, и заменить его адресом функции-перехватчика. Конечно, чтобы программа могла нормально работать, перед заменой исходный адрес следует сохранить. При мониторинге сразу нескольких функций вы не сможете просто заменить несколько импортируемых адресов одним адресом функции-перехватчика. Функция-перехватчик по крайней мере должна знать, для какой отслеживаемой функции она вызывается. В нашей реализации для каждого элемента таблицы отслеживаемых функций создается небольшая функция-заглушка, которая заносит индекс функции в стек перед вызовом универсальной функции ProxyProlog. Таким образом, при модификации каталога импорта модуля используются адреса заглушек. Заглушки выглядят следующим образом: push index // 68 хх хх хх хх jmp ProxyProlog // Е9 уу уу уу уу Функции ProxyProlog остается лишь извлечь индекс из стека, а затем воспользоваться им при обращении к таблице функций для получения полной информации. На рис. 4.2 показано, как происходит вызов функции Win32 до и после модификации каталога импорта адресом заглушки. В левой части изображена ситуация до перехвата; значение переменной каталога импорта используется для косвенного вызова функции Win32 API. В правой части показано, что происходит после модификации. Теперь приложение осуществляет косвенный вызов заглушки, передающей управление универсальной функции ProxyProlog библиотеки Diver.dll. Функция ProxyProlog, а также сопутствующие функции и структуры данных Diver.dll отвечают за то, чтобы после обработки была вызвана исходная функция Win32 API, а затем управление было возвращено вызывающей стороне. Application
Application call [
imp_SetTextCoior]
call r
stub SetTextColor]
& SetTextColor Diver.DLL push id SetTextColor jmp ProxyProlog 4
GDI32.DLL
GDI32.DLL
Рис. 4.2. Перехват вызова функции API с использованием заглушки
1
248
Глава 4. Мониторинг графической системы Windows
ПРИМЕЧАНИЕ Чтобы решение было по возможности универсальным, следует избегать модификации содержимого регистров. Если бы индекс передавался не в стеке, а в регистре, наше*решение не работало бы для функций, использующих регистры для передачи параметров.
Сбор информации Для тех функций, за которыми мы следим, вызов ProxyProlog предшествует вызову настоящей функции Win32 API. Однако ProxyProlog и связанные с ней функции должны выполнить очень непростую работу — собрать информацию обо всех параметрах, сохранить время входа в функцию, вызвать исходную функцию API, сохранить время возвращения из функции, сохранить возвращаемое значение и, наконец, вернуть управление вызывающей стороне. Программаразведчик должна восстановить в прежнем виде все, к чему она прикасалась, — все регистры и флаги процессора (кроме счетчика тактов). Из-за своей сложности эта задача разделена между несколькими функциями, написанными на ассемблере, С и даже на C++ с применением виртуальных функций. О Функция ProxyProlog написана на «голом» ассемблере — в том смысле, что компилятор не должен включать в нее стандартный код входа и выхода из функции. Функция сохраняет содержимое регистров, текущее время (время 1), вызывает функцию ProxyEntry, снова сохраняет время (время 2), восстанавливает регистры и, наконец, возвращает управление исходной функции Win32 API, вызываемой приложением. О Функция ProxyEntry написана на языке С. Она создает в программном стеке структуру KRoutinelnfo, сохраняет основную информацию о вызове, вызывает виртуальную функцию C++ KFuncTable: :FuncEntryCa"llBack, модифицирует стек процессора, чтобы при выходе из исходной функции Win32 API управление сначала передавалось функции ProxyEpilog, а затем снова модифицирует стек процессора, чтобы функция ProxyProlog передала управление исходной функции Win32 API. О Функция KFuncTable: :FuncEntryCallBack реализована как виртуальная функция C++. В минимальной реализации она не делает ничего. Впрочем, эта функция располагает всей информацией о параметрах и времени входа-выхода, поэтому при желании она может выполнить хронометраж, сохранить параметры, проверить и даже изменить их значения. О Функция ProxyEpilog, написанная на «голом» ассемблере, вызывается сразу же после возврата из функции Win32 API. Она сохраняет регистры, сохраняет время (время 3), вызывает функцию ProxyExit, снова сохраняет время (время 4), восстанавливает регистры и, наконец, возвращает управление вызывающей стороне, тем самым завершая мониторинг одного вызова функции API. О Функция ProxyExit написана на языке С. Она извлекает из программного стека структуру KRoutinelnfo, вызывает виртуальную функцию KFuncTable: :FuncExitCallBack и модифицирует стек процессора, чтобы функция ProxyEpilog вернула управление исходной вызывающей стороне.
249
Отслеживание вызовов функций Win32 API
О Функция KFuncTable::FuncExitCallBack реализована как виртуальная функция C++. В минимальной реализации она не делает ничего. Функция располагает всеми данными о времени входа и выхода, а также о возвращаемом значении функции API. При необходимости она может вернуть эту информацию управляющей программе. Ниже приведен код важнейших входных функций, ProxyProlog и ProxyEntry. typedef struct {
unsigned unsigned unsigned unsigned unsigned
m_flag: m_edx; m_ecx; m_ebx; m_eax;
unsigned m_funcid: unsigned m_rtnads: unsigned m_para[32]: } EntryInfo: _declspec(naked) void ProxyProlog(void)
{
// funcid. rtadr, pi..pn // funcid резервирует в стеке место. // в которое позднее заносится адрес вызывающей стороны // Сохранить общие регистры и флаги asm push eax asm push ebx asm push ecx // edx. ecx. ebx. eax asm push edx // 4 байта EFLAGS asm pushfd // Время 1
asm asm asm
_emit _emit shrd
OxOF 0x31 eax, edx. 8
asm asm asm
push sub push
eax // Время входа eax. Overhead eax // Время входа - затраты
_asm lea eax. [esp+8] _ asm push eax asm call ProxyEntry
asm asm asm asm asm asm
pop _emit _emit shrd sub add
ecx OxOF 0x31 eax. edx. 8 eax. ecx OverHead. eax
// EAX = EDX:EAX » 8
// Смещение флага в стеке // Функция С // ecx = время входа // Время 2 // EAX = EDX:EAX » 8
// 'Новые затраты после ProxyEntry
// Восстановить общие регистры и флаги asm popfd
250
Глава 4. Мониторинг графической системы Windows
_asm _asm _asm _asm
pop pop pop pop
edx ecx ebx 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; 'pFuncTab1e->FuncEntryCal1 Back(routine, info); // Модифицировать адрес возврата, чтобы перед возвращением // к исходной вызывающей стороне управление было передано // нашей функции ProxyEpilog info->m_rtnads = (unsigned) ProxyEpilog;
// Обеспечить возврат управления исходной функции // при выходе из ProxyProlog info->m_funcid = (unsigned) pFuncTable->ra func[id].f oldaddress;
Измерение времени осуществляется самым точным и эффективным способом, существующим на процессорах Intel благодаря инструкции RDTSC. Эта инструкция возвращает в регистрах EDX:EAX 64-разрядное количество тактов процессора, прошедших с момента последнего запуска. На процессоре Pentium 200 МГц один такт занимает 5 не. Работать с 64-разрядными величинами неудобно, поэтому программа сдвигает пару EDX:EAX на 8 разрядов вправо и использует только младшее 32-разряд8 ное значение. Минимальный интервал времени увеличивается до 5 х 2 = 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 включает в стек несколько стандартных регистров и копию регистра флагов процессора. Все эти значения отображаются на структуру Entrylnfo уровня С, указатель на которую передается ProxyEntry. Функция ProxyEntry использует указатель на Entrylnfo для получения идентификатора функции и модификации адресов возврата в стеке. Дальше происходит самое интересное. После вызова ProxyEntry функция ProxyProlog восстанавливает общие регистры и регистр флагов, после чего выполняет инструкцию ret. Куда при этом возвращается управление? Когда-то на вершине стека процессора находился индекс функции, занесенный туда заглушкой, но позднее функция ProxyEntry записывает на это место адрес исходной функции Win32 API. Следовательно, последняя инструкция ret в ProxyProlog фактически возвращает управление исходной реализации API. Например, если мы включаемся в цепочку перехвата функции GDI Del eteObject, код заглушки заносит в стек индекс функции (например, 5) и вызывает ProxyProlog. Функция ProxyProlog вызывает функцию ProxyEntry, чтобы та сохранила параметры и записала на место индекса адрес GDI-реализации Del eteOb ject. Таким образом, последняя инструкция ProxyProlog передает управление функции GDI Del eteOb ject. Выходная часть представляет собой зеркальное отражение входной части. Функции ProxyEpilog и ProxyExit приведены ниже для полноты картины. typedef struct {
unsigned m_rslt: } Exitlnfo:
_declspec(naked) void ProxyEpilog(void) { // Результат вызова функции API. asm push eax // Также резервирует место // для адреса возврата // Сохранить общие регистры asm push eax asm push ebx asm push ecx asm push edx // 4 байта флагов asm pushfd
asm asm asm
emit emit shrd
// Время 3 OxOF 0x31 eax. edx. 8 // EAX - EDX:EAX » 8
asm asm
push sub
eax // Время выхода eax. Overhead
252
Глава 4. Мониторинг графической системы Windows
_asm
push
_asm _asm _asm
lea push call
_asm _asm _asm _asm _asm asm
pop _emit _emit shrd sub add
_asm _asm _asm _asm _asm
asm
eax eax. [esp+28] eax ProxyExit
popfd pop pop pop pop
// Время выхода - затраты // Адрес зарезервированного участка
// ecx // OxOF 0x31 eax, edx. 8 // eax. ecx // OverHead. eax
ecx = время выхода Время 4 EAX = EDX:EAX » 8 Новые затраты после ProxyEpilog
Отслеживание вызовов функций Win32 API
253
го значения в стеке адрес возврата, который используется функцией ProxyEpilog для передачи управления исходной вызывающей стороне посредством функции ret. На рис. 4.3 изображен процесс перехвата функции API вместе со всеми изменениями, происходящими в стеке процессора. В нижней части показана передача управления от приложения к заглушке, функции ProxyProlog, функции Win32 API, ProxyEpilog и обратно к приложению (функции ProxyProlog и ProxyEpilog являются вспомогательными). В верхней части рисунка показаны изменения в стеке. Стек
// Восстановить флаги и регистры
edx ecx ebx eax
ret
// Вернуть управление // исходной вызывающей стороне
void _stdcall ProxyExit(ExitInfo *info, unsigned leavetime) { int depth: assert(pStack); KRoutinelnfo * routine = pStack->Lookup(depth); if ( routine ) { pFuncTable->FuncExitCallBack(routine. info, leavetime. depth): info->m_rslt = routine->rtnaddr: pStack->Pop():
При выходе из перехватываемой функции Win32 API управление не возвращается непосредственно вызывающей стороне. Вместо этого вызывается наша функция ProxyEpilog. Дело в том, что функция ProxyProlog изменяет адрес возврата в стеке так, чтобы он указывал на ProxyEpi I og (посредством простого присваивания info->m_rtnads = (unsigned)ProxyEpilog). Мы предусмотрительно сохранили этот адрес возврата в программном стеке для последующего использования. Теперь особое внимание уделяется регистру ЕАХ; в нем хранится скалярное возвращаемое значение функции (например, манипулятор GDI, возвращаемый функцией CreateSolidPen). Функция ProxyEpilog сохраняет его в стеке и передает информацию ProxyExit в виде указателя на структуру Exitlnfo. Структура Exitlnfo состоит из единственного поля, в котором хранится возвращаемое значение функции. Функция ProxyExit находит структуру KRoutinelnfo в программном стеке, вызывает функцию KFuncTable: :FuncExitCallback, а затем заносит на место возвращаемо-
Рис. 4.3. Передача управления и изменения в стеке при перехвате функций 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 выводит данные в текстовый файл, разделяя поля запятыми. Файлам присваиваются имена с последовательной нумерацией pogyOOOO.txt, pogy0001.txt и т. д. Программный код создания файла находит следующий свободный номер в последовательности, чтобы предотвратить стирание старых файлов. В простейшем случае вывод данных организуется просто. Параметры функций Win32 API обычно состоят из 4 байт; такой же размер имеет возвращаемое значение скалярной функции, передаваемое в регистре ЕАХ. Самое «тупое» решение — выводить все значения в виде 8 шестнадцатеричных цифр. Таким образом, TRUE будет выводиться в виде «0x00000001», FALSE - в виде «0x00000000», код растровой операции SRCCOPY — в виде «ОхООСС0020», а для текстовой строки будет выводиться только адрес. В общем, для хакера сойдет, но для простых пользователей очень неудобно. В Win32 API определяется очень богатый ассортимент типов (или по крайней мере макросов типов). Мы работаем со знаковыми и беззнаковыми числами разного размера, всевозможными указателями, бесчисленными манипуляторами и типами высокого уровня, например BITMAPINFO, LOGFONT, DEVMODE и т. д. Архитектура DLL-разведчика позволяет вам выбрать специальную интерпретацию для каждого из этих типов. Для каждой функции Win32 API типы параметров и возвращаемых значений также могут задаваться по именам. Значения, относящиеся к одному типу, расшифровываются одинаково. Вы можете настроить
Отслеживание вызовов функций Win32 API
255
процесс преобразования «сырых» данных в текстовый формат и добавлять поддержку новых типов данных при помощи подключаемых DLL. Чтобы упростить работу с сотнями имен типов, функций и модулей, мы воспользуемся таблицей атомов и преобразуем имена из текстового формата в целочисленные индексы. Например, вместо имени COLORREF программа передает целочисленный атом _COLORREF, значение которого получается на стадии инициализации при включении строки COLORREF в таблицу атомов. Все компоненты системы работают с одной таблицей атомов, поэтому если другой компонент вдруг захочет снова включить COLORREF в таблицу атомов, повторного включения не произойдет; вместо этого будет возвращено исходное целочисленное значение. Происходящее очень похоже на API работы с атомами в Win32. Программа реализует таблицу атомов без использования функций атомов Win32 API по соображениям быстродействия и переносимости. Таблица атомов преобразуется в базовый класс C++ lAtomTable, который сильно напоминает интерфейс СОМ (правда, в данном случае интерфейс Illnknown нам не нужен): struct lAtomTable
{
virtual ATOM AddAtom(const char * name) = 0: virtual const char * GetAtomName(ATOM atom) - 0;
}: Наряду с таблицей атомов также определяется базовый класс C++ IDecoder, преобразующий некоторые типы данных в текстовый формат: struct IDecoder { virtual bool Initialize(lAtomTable * pAtomTable) = 0 ; virtial int Decode(ATOM 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_COLORREF; boo! KBasicDecoder:: InitializedAtomTable * pAtomTable) { if ( pAtomTable==NULL ) return false: atom_char = pAtomTable->AddAtoin("char"): atom_BYTE = pAtomTab1e->AddAtom("BYTE"); atom_COLORREF = 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:
} if ( typ==atom_BYTE ) wsprintf(szBuffer. "%d", data & OxFF): return 4;
} if ( typ==atom_COLORREF ) if ( data==0 ) strcpy(szBuffer, "BLACK"); else if ( data==OxFFFFFF ) strcpytszBuffer. "WHITE"): else wsprintf (szBuffer. "Ибх". data); return 4; return 0:
// Необработанные типы
На стадии инициализации DLL-разведчик создает таблицу атомов, инициализирует экземпляр KBasicDecoder, загружает ini-файл с информацией о специальных настройках IDecoder, загружает и инициализирует каждую из них.
Отслеживание вызовов функций Win32 API
257
Статическая функция MainDecoder управляет всем процессом расшифровки блока данных. Она проходит по цепочке реализаций IDecoder и находит ту, которая позволяет расшифровать определенные типы данных. Реализации KFuncTabl e:: FuncEntryCallBack и KFuncTabl e: :FuncExitCallBack просто вызывают MainDecoder. Итак, в нашем распоряжении имеется расширяемый декодер для расшифровки типов данных Win32. Как видите, знакомство с архитектурой расширения отладчика WinDbg нас кое-чему научило.
Управляющая программа Мы разобрались с процессом внедрения DLL-разведчика, перехватом функций Win32 API, сбором информации и выводом данных... Чего еще не хватает в нашем решении? Очевидно, управляющей программы, при помощи которой выбираются атакуемые программы, отслеживаемые модули и функции, а также точное определение Win32 API. Конфигурация управляющей программы Pogy определяется несколькими стандартными ini-файлами Windows. Эти файлы хранятся в текстовом формате, их структура понятна без лишних объяснений, а в Win32 API предусмотрены средства для их обработки. Управляющая программа является приложением Win32, поэтому ничто не мешает нам использовать все имеющиеся возможности Win32. Главный файл данных, Pogy.ini, состоит из двух секций. В секции Target перечисляются приложения, за которыми вы хотите следить, с указанием конфигурационных файлов для каждого приложения. В секции Option хранятся общие параметры работы программы (например, флаги регистрации вызовов API и отображения информации о вызовах в окне). Здесь же указываются DLL для расшифровки дополнительных типов данных. Пример файла Pogy.ini: [Target] 1=CLOCK.EXE (pclock.ini) 2=NOTEPAD.EXE (pnotepad.ini)
[Notepad]
LogCalM DispCall=0 Decoderl=pogygdi.dll!_Create_GOI_Decoder@0
Decoder2=pogygdi.dll!_Create_DDRAW_Oecoder
В соответствии с этим ini-файлом мы хотим регистрировать вызовы API, но без отображения информации о них. К программе подключаются два декодера Для дополнительных типов данных: один предназначен для типов GDI, а другой — для типов, относящихся к DirectDraw. Пользователь может отслеживать работу одной из двух программ, для каждой из которых существует отдельный ini-файл. В ini-файле уровня приложения перечисляются модули прикладного процес• са> за которыми вы собираетесь следить. Пользователь должен указать имя вызывающего модуля, имя вызываемого модуля и имя ini-файла для группы функций API. Пример: [Module]
CIOCK.EXE, Gdi32.DLL. wingdi CLOCK.EXE. User32.DLL. winuser
258
Мониторинг графической системы Windows
Это означает, что нас интересуют обращения к GDI32.DLL и USER32.DLL; им соответствуют отдельные ini-файлы wingdi.ini и winuser.ini. Имейте в виду, что ini-файлы групп функций API играют'в нашей программе такую же роль, как заголовочные файлы Windows в компиляторе C/C++; другими словами, они содержат описание API, используемое программой во время мониторинга. Конечно, очень хотелось бы изобрести автоматизированный способ построения этих ini-файлов по содержимому заголовочных файлов Windows, библиотечных файлов или каких-нибудь файлов с символическими именами...' Но пока не будем отвлекаться и просто введем вручную всю информацию имя модуля, имя функции, список типов параметров и тип возвращаемого значения. Ниже приведен небольшой фрагмент файла для GDI API.
;T*!V'-- •''*"* '' V x' £ vents | Setup >м
Пользовательский интерфейс управляющей программы Pogy представляет собой диалоговое окно, состоящее из трех страниц-вкладок. На странице Events регистрируются такие события, как создание и уничтожение окон, перехват вызовов функций API DLL-разведчиком, а также выводится подробная информация о вызовах API (если в mi-файле установлен соответствующий флаг). На странице Setup устанавливаются флаги регистрации данных. На странице API выводятся данные, прочитанные программой из ini-файлов. Здесь же выбирается приложение, за которым вы собираетесь наблюдать (из перечисленных в Pogy.ini). В таблице выводится информация о загруженных описаниях функций API. Страница API управляющей программы изображена на рис. 4.4. После запуска программа Pogy устанавливает общесистемный перехватчик, реализованный в Diver.dll. При создании или уничтожении окна верхнего уровня любого приложения DLL-разведчик загружается в его адресное пространство. Diver.dll получает имя главного исполняемого файла приложения и отправляет Pogy сообщение, чтобы узнать, нужно ли следить за данным процессом. Если приказ будет отдан, DLL-разведчик создает скрытое окно для получения информации об отслеживаемых функциях, включается в цепочку перехвата заданных функций и начинает записывать полученную информацию в текстовый файл Наблюдение прекращается с завершением целевого приложения.
,. ШШШШШШ H
das* • - - Interface
t?
:'•
' •"'* - ?| x|'
1
Tatget
[wingdi]
int SelectClipRgn(HDC. HRGN) int SetROP2(HDC,int) BOOL SetWindowExtEx(HDC. int. int. LPSIZE) BOOL SetBrushOrgEx(HDC. int. Int. LPPOINT) BOOL LPtoDP(HDC. LPPPOINT. int) HBRUSH CreateBrushlndirect(LPLOGBRUSH) HBRUSH CreateDIBPatternBrushPttLPVOID. UINT) BOOL DeleteDC(HDC) HBITMAP CreateBitmap(int. int. UINT. UINT. LPVOID) HOC CreateCompatibleDC(HDC) HBRUSH OeateSol idBrush(COLORREF) HRGN CreateRectRgnlndi rect(LPRECT) INT SetBoundsRecUHDC. LPRECT. UINT) BOOL PatBlt(HDC. int. int. int. int, DWORD) BOOL SetViewportOrgEx(HDC. int. int. LPPOINT) BOOL SetWindowOrgEx(HDC. int. int, LPPOINT) Int SetMapMode(HDC. int)
259
Отслеживание вызовов функций Win32 API
Funcfen
' ; ' ^\ ,
-
~"**
iil9Gdi32.dll
Gdi32.dll
AddFontResourceA
lift Gdi32.dll
Gdi32.dll
AddFontResourceW
lift Gdi32.dll
Gdi32.dll
AnimatePalette
ilft Gdi32.dll
Gdi32.dll
Arc
\- '
ii!5 Gdi32.dll
Gdi32.dll
BilBIt
i* '
ijl9Gdi32.dll
Gdi32.dll
CancelDC
;;Я Gdi32.dll
Gdi32.dll
Chord
ii£ Gdi32.dll
Gdi32.dll
ChoosePixelFormat
!i£ Gdi32.dll
Gdi32.dll
CloseMetaFile
!';Й Gdi32.dll
Gdi32.dll
CombineRgn
<т
'Я
\.
"
„« , $* ••• ./, ;
| , -,
>
Г
" '
'"•
',
1
^' ' :-?t
&i*\
:
,-. ...........
_ . ._ . ,,„
1!,!^'
_
Рис. 4.4. Пользовательский интерфейс программы мониторинга Win32 API
После всех потраченных усилий нас ожидает награда — протокол вызовов функций Win32 API, сгенерированный DLL-разведчиком под управлением главной программы. На рис. 4.5 изображен один из таких файлов, импортированный в Microsoft Excel. *№>'!'"1K~-s^ "*'-/ '(" '""• p
xK'A •_,_ ,& Depth 1 1 1 1 1 1 t 1 1
Enter 183,258.00 183.486,00 192,405.00 192,482.00 192.552.00 192,608.00 19265500 762,636,00 762.649.00
Leave 183,262.00 183,496.00 192,409.00 192,488.00 19260600 192.653.00 762,630.00 762,648.00
762 658 00
1 762 663 00
762,710.00
1
762,712.00
762 753 00
1
762 754 00
1
762,758.00
762,755.00 791 .667.00
&МХ1°™/
Return
2 TRUE
2 TRUE 1bOa0132 18a0021 TRUE TRUE 1bOa0132 TRUE 1cOa0132 18a0021 TRUE
"" '~ ~ ', " '
^
Caller ! CLOCK.EXE+16bd : CLOCK.EXE+166 : CLOCK. EXE +16bd CLOCK.EXE+16S ; CLOCK.EXE+355b CLOCK EXE+3571 ; CLOCK.EXE+3589 : CLOCK. ЕХЕ+35Ы i CLOCK. EXE+35bd CLOCK EXE+3616 ; CLOCK.EXE+3626 CLOCK EXE+3637 CLOCK EXE+365a
.' ^^л<*
'
f
i
i
«1.
.Jtt *^ ^
Parameter 1 ^1 I Calee 1010056 i Gdi32.dll!SetBkMode 601004de . Gdi32.dll!DeleteObject :1010058 ! Gdi32.dll!SetBI<Mode 611004de ; Gdi32.dllIDeleleObject LOGFONTW*(135a3cO ; Gdi32.dll!Cre3teFontlndirectW : 11010058 Gdi32.dll!SelectObjecl :1010058 ; Gdi32.dll!GetTexlExtenlPointW ;1010058 i Gdi32.dll!GelTextExtentPointW i1010058 ; Gdi32.dll!SelectObject I 1bOa0132 Gdi32.dlllDeleteObject LOGFONTW*(135a3cO , : Gdi32.dll!CreateFontlndirectW 1010058 Gdi32 dlHSelectObject Gdi32 dlHGetTe»tEK(entExPomtWl1010058 ,.(
kt
„i. ,...:„ ,.„:
Рис. 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 вместе с информацией, которая нас совершенно не интересует. Конечно, нам хотелось бы иметь автоматизированные средства для выборки из заголовочных файлов основных прототипов функций и приведения их к упрощенному формату. Простой, но специализированный анализатор заголовочных файлов С — неплохая тема курсовой работы для студентов, изучающих построение компиляторов. В результате получилась небольшая консольная 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]
Отслеживание вызовов Win32 GDI В любой области Windows-программирования действует общее правило: справиться с работой легко, а выполнить ее блестяще — трудно. Конечно, наша программа мониторинга Win32 API приносит реальную пользу, но и она оставляет желать лучшего. Чтобы добиться главной цели — понимания всех аспектов работы GDI, — нам предстоит еще изрядно потрудиться. В действительности мы хотим ориентировать программу на мониторинг функций GDI и DirectDraw, которым, собственно, и посвящена эта книга. Это позволит нам использовать функции Win32 API, реализованные в KERNEL32.DLL и USER32.DLL, не беспокоясь о том, что они сами могут быть предметом мониторинга.
Файл определения GDI API Прежде всего нам понадобится полный или почти полный ini-файл, который может читаться DLL-разведчиком и который описывает как можно большее количество функций GDI API.
261
HANDLE BOOL DWORD HOC HANDLE' BOOL BOOL BOOL BOOL BOOL BOOL
GdiGetSpoolFileHandle(LPWSTR,LPOEVMOOEW.LPWSTR) GdiDeleteSpoolFileHandle(HANDLE) GdiGetPageCount(HANDLE) GdiGetDC(HANDLE) GdiGetPageHandletHANDLE.DWORD.LPDWORD) GdiStartDocEMF(HANDLE.DOCINFOW*) GdiPlayPageEMF(HANDLE,HANDLE.RECT*.RECT*.RECT*) GdiEndPageEMF(HANDLE.DWORD) GdiEndDocEMF(HANDLE) GdiGetDevmodeFor Page (HANDLE, DWORD. PDEVMODEW*. PDEVMODEW*) Gdi ResetDCEMF(HANDLE.PDEVMODEW)
[Types] HANDLE LPWSTR LPDEVMODEW BOOL DWORD HOC LPDWORD DOCINFOW* RECT* PDEVMODEW* PDEVMODEW
262
Глава 4. Мониторинг графической системы Windows
Отслеживание вызовов Win32 GDI
unsigned data - * (unsigned *) pValue;
Как видно из приведенного листинга, файл определения API состоит из двух секций. В первой секции перечисляются упрощенные прототипы функций, а во второй — уникальные типы данных, используемые этими функциями. Для создания условий, в которых происходит большая часть графического вывода, Win32 GDI пользуется услугами модуля управления окнами (USER32.DLL). Модуль USER32 содержит ряд интересных функций API, за которыми тоже было бы полезно проследить, — BeginPaint, EndPaint, GetDC и т. д. Исходя из этого, мы воспользуемся программой Skimmer и сгенерируем файл winuser.ini из файла winuser.h.
1f (
{
TCHAR temp[32];
if ( ! Lookup( objtyp & Ox7F. Dic_GdiObjectType, temp) ) _tcscpy(temp. "HGDIOBJ"); if ( objtyp & 0x80 ) // Стандартный объект wsphntf(szBuffer. "(S*s)*x". temp, data & OxFFFF);
Программа Skimmer перечисляет все типы данных, задействованные в некоторой группе функций API; эта информация используется DLL-разведчиком. Чтобы сохраненные данные лучше читались, нам понадобится специальный декодер для работы со специфическими типами данных GDI — такими, как HGDIOBJ, LOGFONTW и даже DEVMODEW, если эта информация кого-нибудь заинтересует. Благодаря знанию структур данных GDI, полученному из главы 3, задача построения DLL декодера данных GDI сводится к обычному кодированию. Ниже приведена базовая структура DLL декодера GDI. class KGDIDecoder : public IDecoder
else wsphntftszBuffer. "UsUx". temp, data & OxFFFF); return 4:
if ( typ==atom_PLOGFONTW ) LOGFONTW * pLogFont = (LOGFONTW *) data: if ( ! IsBadReadPtr(pLogFont. sizeof(LOGFONTW)) )
ATOM atom_HGDIOBJ;
wsprintftszBuffer. "& LOGFONTW{*d,*d pLogFont->lfHeight. pLogFont->lfWidth, pLogFont->lfFaceName);
public: KGDIDecoderO
{
(typ==atom_HDC) || (typ==atom_HGDIOBJ) || (typ==atom_HPEN) || (typ==atom_HBRUSH) || (typ==atom_HPALETTE) || (typ==atom_HRGN) || (typ==atom_HFONT) )
unsigned objtyp = (data » 16) & OxFF;
Декодер данных GDI
{
263
pNextDecoder = NULL:
return 4;
virtual bool InitializedAtomTable * pAtomTable); virtual int Decode(ATOM typ. const void * pValue. char * szBuffer, int nBufferSize);
// Необработанные типы return 0:
bool KGDIDecoder::InitializedAtomTable * pAtomTable)
{
if ( pAtomTable--NULL ) return false: atom_HGDIOBJ
= pAtomTable->AddAtom("HGDIOBJ");
KGDIDecoder GDIDecoder: extern "C" declspec(dllexport) IDecoder * WINAPI Create_GDI_Decoder(void)
{ return true; // Поиск типов объектов GDI int KGDIDecoder::Decode(ATOM typ. const void * pValue. char * szBuffer, int nBufferSize)
return & GDIDecoder;
}
Класс KGDIDecoder представляет собой простой декодер для типов данных GDI, созданный на основе базового класса IDecoder (по аналогии с реализацией СОМинтерфейсов в классах СОМ). Функция Create_GDI_Decoder возвращает указатель на глобальный экземпляр KGDIDecoder (примерно то же самое делает фабрика класса для класса СОМ). Разведчик загружает DLL декодера GDI во время
264
Глава 4. Мониторинг графической системы Windows
работы, получает адрес функции-создателя, вызывает ее, а затем инициализирует декодер методом KGDIDecoder->Initialize. Затем новые декодеры подключаются поверх старых и последовательно вызываются для преобразования полученных данных в текстовый формат до тех пор, пока запрос не будет обработан. Как было показано в главе 3, манипулятор объекта GDI в Windows NT/2000 состоит из трех частей: 8-разрядного признака уникальности, 8-разрядного идентификатора типа и 16-разрядного индекса. В нашей программе эта информация используется для выделения из манипулятора имени типа и индекса. Для указателей на структуру LOGFONTW программа выводит данные логического шрифта: высоту, ширину и название гарнитуры. Перевод 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 каталог импорта вообще не используется. В этом случае происходит прямой внутримодульный (intramodule) вызов; конструкции типа call [ imp_SelectObj] в нем не участвуют. Для полного мониторинга вызовов API на уровне процесса (то есть перехвата всех обращений изнутри и извне модуля, в котором находится реализация; из модулей уже загруженных и тех, которые будут загружены потом) приходится модифицировать саму реализацию API. Например, если найти адрес функции SelectObject в GDI32.DLL и модифицировать саму функцию, все вызовы SelectObject будут проходить через ваш код.
Отслеживание вызовов Win32 GDI
265
Модифицировать программу нетрудно. Значительно труднее сделать так, чтобы после модификации приложение работало так же, как раньше. Как было показано в разделе «Отслеживание вызовов функций Win32 API», мы хотим вставить в функцию несколько строк ассемблерного кода, чтобы вызов функции API приводил к автоматической передаче управления нашему входному обработчику. После выхода из обработчика исходная функция API должна выполняться в точности с теми же значениями регистров. После завершения функции API перед возвращением управления вызывающей стороне должен быть вызван наш выходной обработчик. Главная проблема заключается в том, что из-за модификации точки входа в функцию API при завершении входного обработчика управление нельзя передать модифицированному входу функции. У этой проблемы существует два основных решения. В первом случае входной обработчик восстанавливает прежнее состояние точки входа, чтобы при возвращении из него управление передавалось исходной реализации API. Такое решение идеально подходит для одноразовой регистрации, но где же вносить исправления для последующих вызовов? Наиболее естественно было бы делать это в выходном обработчике, но это означает, что на время выполнения функции API мы не сможем обрабатывать рекурсивные вызовы, а также вызовы из других программных потоков. Таким образом, мы приходим ко второму решению — не восстанавливать модифицированный участок, а переместить точку входа в функцию. При модификации изначального входа в функцию API как минимум 5 байт приходится выделить под инструкцию безусловного перехода в функцию-заглушку, что приводит к порче нескольких инструкций. Поврежденные инструкции можно скопировать в буфер, находящийся внутри DLL-разведчика, и поставить после них команду перехода к первой инструкции после поврежденного участка. Когда все это будет сделано, остается лишь разрешить входному обработчику DLL-разведчика передать управление на перемещенные инструкции. Следующий пример поможет лучше разобраться в происходящем. Рассмотрим несколько начальных инструкций функции SelectObject на ассемблере и в машинных кодах. _SelectObject@8: 55 push ebp 8В ЕС mov ebp. esp 51 push ecx 83 65 FC 00 and dword ptr [ebp-4]. 0 _SelectObject@8+8: Пять байт, начинающихся с адреса _SelectObject@8, понадобятся для нашей маленькой хирургической операции; это приведет к порче 4 инструкций общим объемом 8 байт. Мы сохраняем первые 8 байт _Sel ectObject@8 и записываем по этому адресу инструкцию безусловного перехода в заглушку. _Se1ectObject@8: е9 хх хх хх хх jmp Stub_SelectObject@8 90 пор 90 пор 90 пор _SelectObject@8+8:
266
Глава 4. Мониторинг графической системы Windows
Обратите внимание: на самом деле мы используем лишь 5 байт, но для того, чтобы программа нормально работала, в нее включаются три пустые инструкции пор. Код заглушки выглядит следующим образом:
Stub_SelectObject(a8:
push index_selectobject jmp ProxyProlog New_Se1ectObject@8: push ebp mov ebp. esp push ecx
and dword ptr [ebp-4], 0
jmp _SelectObject@8+8
Обратите внимание на пару любопытных подробностей. Во-первых, по адресу Stub_SelectObject(P8 находится точно такой же код, как и в нашем предыдущем решении с модификацией каталога импорта. Во-вторых, функция New_ Sel ectObject@8 воссоздает начало функции _Se1 ectObject@8 перед модификацией. Эти совпадения позволяют заново использовать весь код, задействованный в решении с модификацией каталога импорта, за одним исключением: мы должны присвоить pFuncTable->m_func[index_selectobject].f_o1daddress значение New_SelectObject@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.dll, 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.
ш
mm JPWIilllt ^&fcS$&'ul ;
::
Ш^^|йрЩ^Ш||Ш ^|^Ш^Ш^щ|^^^^^йШШШ^Ш|^ i Calee 3 Leaver Return i Caller "6272992 Т' HB'ltMAP (520504 c8) ; CARDS"dll+17e9 ! user32.diiiLoadBitmapA Ц • 2 6272098 6272176 (HOC)2d7 : user32.dJI!GetDC il US"ER32"dll+c60"i i Gdi32.dii!CreateCompatibleBitmap Д: г "6272179" "6272457" HBifMAP(5205U4c8); USER32.dll+c650 " i user32."dlJ!ReieaseDC Я 2 6272458: 6272486-1 ; USER32.dll+cB63 : USER32 dll+c9c2 !'Gdi32.dll!Selectbbject ' | | 2: 6272490: 6272526 (SHBITMAP)f : Gdi32.dlliSe'lBkColor Ш 2 6272526 6272528 WHITE I USER32.dll+c9e1 "TGdiSZdiySetfextColor 1 | 2: "6272529"-' 6272532 BLACK I USER32.dll+c9f1 "]"Gdi32.dISetblBits if 1 USER3'2".'d'ii'+c'a"ie 2 6272534" "б27296б";96 i Gdi32.dll!SaveDC Ш GDI32.DLL-t6baa 3 6272545: 6272593:1 i Gdi32.dll!SelectObject III 6272594: 6272607: (HBITMAP)4c8 '! GD'l32.DLL-»6bbb" 3: "6272626""(SHPALETtE)b ! GDI32.DLL-t6bd7 '"]"G"d'i32'"dlliSe'iectPa'lette 11 Э : "6272616 i G'd'i32.diiis'etDiBitstobevice *й GDI32.DLL+6cOa 3 6272629 627290296 "TGdiSZdiiiSeiectPaietfe 11 i GDI32.DLL46dc 3 6272903 6272915 (SHPALETTE)b i' Gdi32dll!SelectObject | | GDI32.DLL*6c25 з: 62729161 6272925 (HBITMAP)4c8 ! Gdi32".'dlliRestofebC II з '• 6272926: 6272966 TRUE ! GDI32.DLL+6c32 Ш i Gdi32.dll!SetfextCoJor ;;1 2 6272967: 6272968 BLACK : USER32.dll+ca3f '"TGdSZdlliSemkCoior Я; USER32"dli+ca4a 6272968: 6272969 WHITE 1 Gdi32.dil!SelectObject Щ USER32.dll+ca62 ""6272969: 6272983 (HBITMAP)4c8 1 \ ЩМ&КИ.'«дяЛШ^ЩЩ^Ш:Щ^Я№ЩЗЩЙ¥:йШ:й^:^Ш2 Depth
I 1
г
Enter
"6271966"
1
i 1
Рис. 4.6. Полный мониторинг вызовов API дает представление о реализации LoadBitmap
Вызовы API, показанные на рисунке, отсортированы по порядку их обработки. В первом столбце указывается уровень вложенности; во втором — возвращаемое значение; в третьем — адрес вызывающей стороны и в четвертом — имя вызываемой функции. Значения параметров не показаны для экономии места. Обратите внимание: непосредственно сгенерированные файлы сортируются не по времени входа, а по времени выхода. Для упрощения анализа данных использованы средства сортировки Excel. Если внимательно присмотреться к рисунку, вы поймете, что перед вами «секретная» реализация функции LoadBitmap. Функция LoadBitmap поддерживается диспетчером окон (USER32.DLL) и предназначается для загрузки растра в аппаратно-зависимом формате GDI. Однако из документации мы не знаем, каь реализована эта функция. Из рисунка видно, что LoadBitmapA (ANSI-версия LoadBitmap) вызывает несколько функций GDI для преобразования аппаратно-неза висимого растра в аппаратно-зависимый растр. Функция CreateCompatibleBitmai создает новый DDB-растр, а функция SetDIBits выполняет преобразование. И: рисунка также видно, как функция SetDIBits реализована в GDI — она сводите* к вызову SetDIBitsToDevice.
268
Глава 4. Мониторинг графической системы Windows
ОтслеживаниеСОМ-интерфейсов DirectDraw DirectDraw API, как и остальные интерфейсы DirectX API, создан на основе технологии Microsoft COM (Component Object Model). Функциональные возможности DirectDraw предоставляются пользователю в виде нескольких СОМ-интерфейсов — например, IDirectDraw и IDirectDrawSurface. СОМ-интерфейс определяется как группа семантически связанных функций (или методов). Например, методы интерфейса IDirectDrawSurface предназначены для работы с поверхностями DirectDraw, а методы интерфейса IDirectDrawClipper управляют отсечением поверхностей DirectDraw. Давайте посмотрим, как организовать мониторинг этих методов.
Таблица виртуальных функций Методы СОМ-интерфейса вызываются через интерфейсный указатель, который фактически представляет собой указатель на объект C++ с неизвестным представлением данных. Единственное, что известно клиентской стороне, — то, что СОМ-объект начинается с указателя на таблицу виртуальных функций. Эта таблица содержит указатели на функции, реализующее все методы интерфейса и следующие в определенном порядке. Все СОМ-интерфейсы являются производными от интерфейса IDnknown, в котором определяются три метода: QueryInterface, AddRef и Release. Это означает, что первые три указателя в таблице виртуальных функций СОМ всегда реализуют эти три метода. Обычно таблица виртуальных функций C++ или СОМ генерируется компилятором в глобальной области данных, доступной только для чтения или для чтения/записи. Прослеживается аналогия с внутренними переменными, используемыми каталогом импорта модуля для хранения адресов импортируемых функций. С технической точки зрения перехватывать вызовы методов СОМ-интерфейса или DirectDraw-интерфейса совсем несложно. Все, что для этого необходимо, — найти адреса всех интересующих нас таблиц виртуальных функций, а затем заменить хранящиеся в них указатели на функции указателями на заглушки DLLразведчика. Получить адрес таблицы виртуальных функций для обычного СОМ-интерфейса просто. Найдите идентификаторы GUID класса и интерфейса, вызовите CoCreatelnstance — и реализация СОМ операционной системы загрузит нужный сервер СОМ, создаст СОМ-объект и вернет вам интерфейсный указатель. Первые 4 байта блока, на которые ссылается интерфейсный указатель, и дадут вам искомый адрес таблицы виртуальных функций. Большинство интерфейсов DirectDraw не создается стандартным вызовом CoCreatelnstance. Например, создать интерфейсный указатель для IDirectDrawSurface можно лишь одним способом — вызовом метода CreateSurface для интерфейса IDirectDraw. В контексте DirectDraw это абсолютно логично, поскольку поверхности DirectDraw всегда находятся под управлением объектов DirectDraw. Разведывательная библиотека DLL должна оказывать минимальное воздействие на работу системы. Следовательно, создавать объект DirectDraw и поверхность DirectDraw лишь для получения таблицы виртуальных функций IDirectDrawSurface было бы нежелательно. Альтернативное решение — получать данные
Отслеживание СОМ-интерфейсов DirectDraw
269
таблиц виртуальных функций автономно, в отдельной программе, сохранять их в ini-файле и затем использовать в процессе отслеживания. Программа QueryDDraw действует именно так. Она пытается создать как можно больше различных интерфейсных указателей DirectDraw и регистрирует адреса таблиц виртуальных функций, количество методов, а также имя и GUID интерфейса. В C++ определить количество методов класса практически невозможно, однако программа DirectDraw, написанная на С, должна знать это количество, поскольку таблица виртуальных функций имитируется при помощи массива указателей на функции. В следующем фрагменте показано, как получить необходимую информацию для интерфейса IDirectDraw. #define CINTERFACE finclude IDirectDraw * Ipdd: HRESULT hr = DirectDrawCreate(NULL, & Ipdd, NULL): Dumplnterface("IIDJDirectDraw", IIDJD1 rectDraw. lpdd->lpVtbl. sizeof(*lpdd->lpVtbl) );
Перед включением ddraw.h, заголовочного файла DirectDraw, определяется макрос CINTERFACE. Тем самым активизируется определение СОМ-интерфейсов в стиле С, где таблица виртуальных функций имитируется массивом указателей на функции, а указатель на таблицу виртуальных функций хранится в одном из полей структуры (IpVtbl). Определение СОМ-интерфейсов в стиле С позволяет использовать функцию sizeof(*1pdd->1pVtb1) для вычисления размера таблицы виртуальных функций, а следовательно, — и количества функций в таблице. Вызов метода C++ несколько отличается от обычного вызова функции в С или Pascal. Вызываемому методу неявно передается дополнительный указатель на текущий объект (так называемый указатель this). Хотя компилятор C++ поддерживает возможность передачи указателя this в регистрах процессора для повышения быстродействия, СОМ-интерфейсы и интерфейс DirectDraw всегда передают указатель this в стеке. Остается лишь сообщить функции вывода параметров DLL-разведчика о наличии дополнительного параметра.
Определение DirectDraw API Следующая задача — сгенерировать для всех методов DirectDraw ini-файл в формате управляющей программы Pogy. Для этого в простейший анализатор заголовочных файлов С, Skimmer, необходимо внести несколько изменений. Во-первых, начало объявления СОМ-интерфейса должно определяться по префиксу DECLARE_INTERFACE_; во-вторых, программа должна обрабатывать макросы STDMETHOD и STDMETHOD_ для восстановления типа возвращаемого значения и имени функции; в-третьих, макросы THIS и THIS_ также должны обрабатываться для передачи указателя thi s в качестве дополнительного параметра. Ниже приведена отредактированная версия полного, точного и недвусмысленного определения DirectDraw API.
[ddraw]
HRESULT DirectDrawEnumerateW(LPPENUMCALLBACKW.LPVOID)
270
Глава 4. Мониторинг графической системы Windows
HRESULT DirectDrawEnumerateA(LPPENUMCALLBACKA.LPVOID) HRESULT DirectDrawEnumerateExW(LPPENUMCALLBACKEXW.LPVOID.DWORD) HRESULT DirectDrawEnumerateExAtLPPENUMCALLBACKEXA.LPVOID.DWORD) HRESULT DirectDrawCreate(GUID*,LPDIRECTDRAW*.lunknown*) HRESULT DirectDrawCreateClipper(DWORD.LPDIRECTDRAWCLJPPER*. lunknown*) [COMJdraw] '728405aO 728318a8 23 {6cl4db80-a733-llce-a5-21-00-20-af-0b-e5-60} IID_IDirectDraw 728408eO 728318a8 24 {b3a6f3eO-2b43-llcf-a2-de-00-aa-00-b9-33-56} IID_IDirectDraw2 728407aO 728318a8 28 {9c59509a-39bd-lldl-8c-4a-00-cO-4f-d9-30-c5} IID_ID1rectDraw4 72840940 7282f034 36 {6cl4db81-a733-llce-a5-21-00-20-af-0b-e5-60} IID_IOi rectDrawSurface [IDirectDraw] HRESULT QueryInterface(THIS,REFIID.LPV01D*) ULONG AddRef(THIS) ULONG Release(THIS) ULONG Compact(THIS) HRESULT CreatedipperCTHIS.DWORD.LPDIRECTDRAWCLIPPER*.lunknown) HRESULT CreatePalette(THIS,DWORD.LPPALETTEENTRY. LPDIRECTDRAWPALETTE*.lunknown) HRESULT CreateSurface(THIS.LPDDSURFACEDESC. LPDIRECTDRAWSURFACE*.lunknown) В первой секции перечисляются обычные функции, экспортируемые из DDRAW.DLL Эта библиотека экспортирует довольно много функций. Здесь пере. числены функции, документированные в ddraw.h; другие функции (такие, как DllGetClassObject) принадлежат к числу стандартных экспортируемых функций СОМ; третьи документируются в других заголовочных файлах или вовсе не документируются. Во второй секции приведена информация о СОМ-интерфейсах, сгенерированная программой 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 HackMethodCunsigned vtable. int n. FARPROC newfunc) { DWORD cBytesWritten; WriteProcessMemory(GetCurrentProcess(). (LPVOID) (vtable + n * 4). & newfunc, sizeof(newfunc), ScBytesWritten): return cBytesWritten == sizeof(newfunc); } Параметр newfunc указывает на функцию-заглушку, описанную в разделе «Отслеживание вызовов функций Win32 API». После модификации все работает так же, как и при правке ка'талога импорта. Ниже приведен маленький пример для интерфейса IDirectDraw. HRESULT(O). ddraw.dll!SetCooperativeLevel. Ox893b28. HWNDCSOOcc). 17 HRESULT(0). ddraw.dll!SetDisplayMode. Ox893b28, 640. 480, 24 HRESULT(O), ddraw.dTHCreateSurface, Ox893b28, LPDDSURFACEDESC(12fe54). LPDIRECTDRAWSURFACE*(12ff2c). Iunknown*(0) ULONG(O). ddraw.dll'Release. Ox893b28 Как видно из приведенной последовательности вызовов, после создания объекта DirectDraw приложение вызывает методы SetCooperativeLevel, SetDisplayMode и CreateSurface интерфейса IDirectDraw, после чего уничтожает объект методом Release. Первый параметр, Ох893Ь28, представляет собой указатель this. Как видите, декодер типов данных DirectDraw приносит несомненную пользу.
Отслеживание системных вызовов GDI От рассмотрения трех разных типов отслеживания вызовов API мы переходим к тому, о чем не пишут в документации. Да, речь идет о системных функциях GDI, интерфейсе между клиентом GDI пользовательского режима и графическим механизмом режима ядра. Как упоминалось выше, DirectDraw, DirectJU и 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 D D D D D 0 D D D D D D
NtGdiCreateEllipticRgn(D.D.D.D). 77F725AB. 1020 NtGdiDdGetBltStatus(D.D), 77F726A7. 1047 NtGdiGetDeviceGartraRamp(D.D). 77F728D7. 10a8 NtGdiSTROBJ_dwGetCodePage(D). 77F72CB7. 1274 NtGdiGetTextExtentExW(D.D.D.D.0,0,0,D), 77F43C51. 10c9 NtGdiGetColorAdjustment(D.D). 77F728BB. lOal NtGdiFlushO. 77F413F9. 1093 NtGdiDdSetOverlayPosition(D.D.D). 77F7274F. 105d NtGdiPATHOBJ_bEnumCliplines(D,D.D), 77F72CD3. 1279 NtGdiEngCreateBitmap(D.O.D.D.O.D), 77F4B5CD. 1240 NtGdiColorCreatePalette(D,D.D.D,D,D). 77F7258F. 1011 NtGdiDdDestroySurface(D.D). 77F5AAB2, 1041 NtGdiDdRenderMoComp(D.D). 77F72717. 1057
Возможно, вы заметили, что мы не располагаем точной информацией о типах параметров и возвращаемых значениях. Единственное, что нам известно, — это количество параметров, которое определяется по количеству байт, извлекаемых из стека при возвращении. Поэтому мы просто помечаем каждый параметр типом D (сокращение от DWORD) и откладываем их замену более осмысленными типами до появления дополнительной информации. В DLL-разведчика приходится внести ряд изменений. Во-первых, адрес функции известен, поэтому ухищрения типа GetProcAddress для Win32 API не понадобятся. Однако программа должна убедиться в том, что по этому адресу находится код в формате вызова системной функции:
Отслеживание системных вызовов GDI
273
NtGdi_SysCall_xx mov eax, functionjndex NtGdi_SysCall_xx+5: lea edx. [esp+4] int Ox2e ret parameterjiumber * 4
В принципе можно было бы воспользоваться методом модификации с перемещением, использованным для перехвата вызовов API на уровне процесса, но нетрудно заметить, что инструкции после NtGdi_SysCa11_xx+5 существуют в ограниченном количестве вариантов — по одному для каждого количества параметров. Количество параметров, передаваемых при вызове системных функций GDI, лежит в пределах от 0 до 15. Следовательно, нам понадобится только 16 функций для замены кода, следующего после первой инструкции (сохранения индекса). После модификации код принимает следующий вид: NtGdi_SysCall_xx mov eax. functionjndex NtGdi_SysCa11_xx+5: jmp Stub_NtGdi_SysCal1_xx Stub_NtGdi_SysCall_xx push func_id jmp ProxyProlog
При возвращении из ProxyProlog мы должны передать управление одной из функций, в которых и происходит непосредственный вызов системных функций: // Для системных функций с двумя параметрами _declspec(naked) void SysCa11_08(void) { _asm lea edx. [esp+4] asm int Ox2e
_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
(SHFOND21
Se1ectObject((HDC)407, (HFONT)3el)
1
WHITE
SetBkColor((HDC)305,a9c8a2)
1
0
SetTextAlign((HDC)305,0)
1
BLACK
SetTextCol or( ( HOC ) 305 , BLACK)
2
TRUE
NtGdi Del eteOb jectApp ( ( HPEN ) 4d9 )
1
TRUE
DeleteObject((HPEN)4d9)
1
TRUE
Del eteOb ject ( ( HBRUSH ) 3e8 )
1
(SHBRUSH)IO
GetStockObject(O)
2
(SHPENU7
NtGdiGetStockObject(7)
1
(SHPENU7
GetStockObject(7)
3
(HFONT)3el
NtGdi HfontCreate(Oxl2f8cO , 0x164. 0x0 ,,0x0.0x137468)
2
(HFONT)3el
CreateFontIndirectExW(ENUMLOGFONTEXDVW*(12f8cO))
2
TRUE
NtGdiGetWidthTabl e( (HDO407 , Oxb . Oxl37b28 , 0x106 , Oxl37d3e,0xl378b8)
TRUE
GetTextExtentPointW((HDC)407,LPCWSTR(1135a394),11. LPSIZE(12fac4))
HBITMAP(3d9)
NtGdiCreateCompatibleBitmap((HDC)407,0x20,0x24)
HBITMAP(3d9)
CreateCompatibleBitmap((HDC)407,32,36)
HBITMAP(3d9)
CreateDTscardab1eBitmap((HDC)407,32,36)
TRUE
NtGdi Rectangle((HDC)2e9,0x60.0x3,0x64,0x7)
TRUE
Rectangle((HDC)2e9,96,3.100,7)
Помните о том, что функции с более высоким уровнем вложенности вызываются функциями более низкого уровня, следующими в протоколе после них. Полученные протоколы подтверждают некоторые факты, упоминавшиеся в предыдущих главах. О Часть структуры данных контекста устройства реализуется в пользовательском режиме, поэтому простые запросы к контексту легко и эффективно обрабатываются в пользовательском режиме без обращения к системным функциям режима ядра. О Таблица объектов GDI находится под управлением графического механизма, поэтому при создании и уничтожении объектов вызываются системные функции. Кисти и прямоугольные регионы занимают особое место — GDI
Отслеживание интерфейса DPI
275
кэширует удаленные объекты для повторного использования. Мы видим, что при удалении HPEN вызывается функция NtGdi Del eteOb jectApp, тогда как удаление 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-страницы содержат списки вызовов DD1 с шестнадцатеричными дампами параметров и 24-битные цветные растры, воспроизведенные с разрешением 96 dpi. Наш простой драйвер HTML хорошо подходит Для экспериментов с интерфейсом DDL Впрочем, этот вариант ограничивается драйвером принтера и фиксированным 'Набором параметров. Конечно, желательно иметь более общее решение, которое
276
Глава 4. Мониторинг графической системы Windows
бы позволяло следить за всеми графическими драйверами, экранами, принтерами, плоттерами и даже факсами. В главе 3 чрезвычайно подробно рассматриваются основные внутренние структуры данных GDI и графического механизма. В частности, там говорилось о том, что объект ядра каждого контекста устройства содержит указатель на структуру PDEV, содержащую всю информацию о физическом устройстве для графического механизма. Структура PDEV создается после загрузки драйвера экрана при вызове функций DrvEnableDriver, DrvEnablePDEV и, наконец, DrvCompletePDEV. Следовательно, PDEV содержит всю информацию, полученную от драйвера графического устройства при вызове этих функций, включая и точки входа DDI. В 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.
Код команды
Параметры
Функция
DOISPY READ
Адрес, размер
То же, что и у Periscope, — чтение блока данных из адресного пространства ядра
DDISPY
Адрес таблицы функций DDI, количество
Модификация содержимого таблицы функций (начало мониторинга вызовов DDI)
277
Код команды
Параметры
Функция
DDISPYJND
Адрес таблицы функций DDI, количество
Восстановление содержимого таблицы функций (конец мониторинга вызовов DDI)
DDISPY REPORT
Размер
Передача собранной информации управляющей программе
Для каждой функции DDI создается соответствующая промежуточная функция, которая регистрирует данные, вызывает исходную функцию и, возможно, регистрирует ее возвращаемое значение. Хронометраж в данном случае не производится, поскольку общее быстродействие GDI мы измеряем в пользовательском режиме. Мы также не беспокоимся о сохранении регистров, зная, что по правилам DDI регистры используются только для возвращения значения функции. Словом, никакого ассемблера — сплошной код С. Ниже приведена самая интересная часть DDISpy — промежуточные функции. typedef struct { PFN pProxy: PFN pReal: } PROXYFN; PROXYFN DDI_Proxy [] = // Перечисление в порядке индексов функций DDI (PFN) (PFN) (PFN) (PFN) (PFN) (PFN) (PFN) (PFN)
DrvEnablePDEV. DrvCompletePDEV, DrvDisablePDEV. DrvEnableSurface. DrvDisableSurface.
NULL. NULL, NULL, NULL, NULL.
NULL.
NULL.
NULL.
NULL.
DrvResetPDEV,
NULL,
void DDISpy_Start(unsigned fntable. int count)
{
unsigned * pFuncTable = (unsigned *) fntable; // Очистить буфер
Таблица 4.2. Команды ввода-вывода DDISpy
START
-
for (int i=0: i OxaOOOOOOO ) // Действительный указатель if ( DDI_Proxy[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 OxaOOOOOOO ) // Действительный указатель if ( DDI_Proxy[i].pProxy != NULL ) // Есть промежуточная функция { // Вернуть старый адрес pFuncTable[i] = (unsigned) DOI_Proxy[i].pReal; } }
#define Call(name) (*(PFN_ ## name) \ DDI_Proxy[INDEX_ ## name].pReal) void APIENTRY DrvDisableDriver(void) { Write("DisableDriver"): CalKDrvDisableDriverX):
WriteC'DrvTextOut"): // ... 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]; HOC hDC - GetDC(NULL);
typedef unsigned (CALLBACK * ProcO) (void); ProcO pGdiQueryTable = (ProcO) GetProcAddresst GetModuleHandlet"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 unsigned lelse unsigned unsigned iendif
pdev = buf[5]: fntable = pdev + Ox3F4;
// PDEV * // Таблица функций
pdev = buf[7]: fntable = pdev + OxBSC;
// PDEV * // Таблица функций
// Прочитать таблицу функций для проверки. ..DrvScope scope.Read(buf, (void *) fntable. 25 * 4):
BOOL APIENTRY DrvTextOut(SURFOBJ *pso. STROBJ *pstro. FONTOBJ *pfo. CLIPOBJ *pco. RECTL *prclExtra. RECTL *prclOpaque. BRUSHOBJ *pboFore. BRUSHOBJ *pboOpaque. POINTL *ppt!0rg. MIX mix)
{
279
Итоги
// Создать контекст устройства
unsigned cmd[2] = { fntable. 25 }: unsigned long dwRead; // Начать отслеживание DDI IoControl(DDISPY_START. and. sizeof(cmd). buf, 100. udwRead): // Добавить графические вызовы или переместить окно на рабочем столе // Прекратить отслеживание DDI IoControl(DDISPY_END. and. sizeof(cmd). buf. 8. SdwRead); cmd[l] = sizeof(buf); // Прочитать зарегистрированные данные IoControl(DDISPY_REPORT, and. 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
Современные видеоадаптеры
283
с 32 мегабайтами памяти, а ваш видеоадаптер использует тот же объем памяти с 128-разрядной адресацией для собственных целей. Но самое невероятное заключается в том, что видеоадаптер способен выполнять в секунду до 9 миллиардов вещественных операций, а код режима ядра Windows NT имитирует вещественные операции с использованием целых чисел. Рассмотрим основные компоненты современного видеоадаптера.
Кадровый буфер
Глава 5 Абстракция графического устройства Как известно, среди всех интерфейсов API графического программирования Windows центральное место занимает интерфейс GDI (Graphics Device Interface). DirectDraw, новый API двумерной графики от Microsoft, ориентирован на программирование игр, а интерфейс DirectSD предназначен для игр и приложений, строящих объемное изображение. Все эти графические API являются аппаратно-независимыми программными интерфейсами, что позволяет приложениям, написанным с их применением, работать на разных графических устройствах. Для обеспечения аппаратной независимости графического API необходима хорошая абстракция, которая бы позволяла представлять различные графические устройства и маскировать их различия без потери производительности. В этой главе описан главный механизм абстрагирования графических устройств в GDI — контекст устройства (device context, DC). Мы познакомимся с возможностями современных видеоадаптеров, представлением абстрактного графического устройства в виде контекста устройства, а также взаимодействием контекста устройства с модулем управления окнами ОС.
Современные видеоадаптеры Графический API в системе Windows прежде всего ориентируется на работу с видеоадаптером как с главным средством взаимодействия пользователя и компьютера. Возможно, вас удивит, насколько сложным устройством является современный видеоадаптер. В индустрии PC 64-разрядные компьютеры едва замаячили на горизонте, а в руководстве к видеоадаптеру заявлено, что он использует 128-разрядную архитектуру. Возможно, ваши программы работают в 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 перечислены объемы памяти, необходимые для хранения одного кадрового буфера при разном разрешении и формате пикселов.
285
Современные видеоадаптеры
Разрешение
Отношение «ширина/ высота»
Объем памяти для хранения кадрового буфера, Кбайт 8 6ит
15/ 1б 6ит
24 6ита
32 6ита
1280 х 1024
5:4
1280
2560
3840
5120
1600 х 1200
4:3
1875
3750
5625
7500
1920 х 1080
16:9
2025
4050
6075
8100
1920 х 1200
8:5
2250
4500
6750
9000
В максимальном режиме, поддерживаемом видеоадаптером автора, используется разрешение 1920 х 1200 с 32-разрядным кадровым буфером и частотой вертикальной развертки 60 Гц. Это означает, что каждую секунду этот видеоадаптер 60 раз читает весь 9000-килобайтный кадровый буфер и преобразует его в видеосигнал. Таким образом, в секунду видеоадаптер должен обрабатывать 540 Мбайт информации; становится понятно, почему для него нужна 128-разрядная быстрая синхронная память. Шаг •
Высота В
G
R
Таблица 5.1. Геометрия кадрового буфера Разрешение
Отношение «ширина/ высота»
Объем памяти для хранения кадрового буфера, Кбайт
640 х 480
4:3
300
600
900
1200
800 х 600
4:3
469
938
1407
1875
1024 х 768
4:3
768
1536
2304
3072
1152 х864
4:3
972
1944
2916
3888
8
**ит
15
' *•** ®ит
24
бита
32 бита
Ширина х байт на пиксел
~~
Рис. 5.1. Геометрия кадрового буфера
В разрешении 1024 х 768 при 24 бит/пиксел одна строка развертки представляется минимум 2304 байтами. Спецификация Microsoft требует, чтобы для повышения быстродействия при работе с памятью строки развертки выравнивались в кадровом буфере по 32-разрядной границе двойных слов. Объем в 2304 байта соответствует этому требованию. При этом строка развертки вовсе не обязана иметь точную длину в 2304 байта — она лишь должна быть не меньше ^
286
Глава 5. Абстракция графического устройства
этой величины. Таким образом, производителям оборудования предоставляется определенная гибкость при выравнивании строк развертки. Размер одной строки развертки в кадровом буфере называется шагом (pitch). В структуре кадрового буфера, изображенной на рис. 5.1, буфер представляет собой массив строк развертки, а размер каждого элемента массива определяется шагом буфера. В строке развертки каждый пиксел представляется определенным количеством смежных битов или байтов. Например, для кадрового буфера с кодировкой 24 бит/пиксел один пиксел представляется тремя байтами, определяющими интенсивность цветовых составляющих в последовательности «синий — зеленый — красный». Следующая функция вычисляет адрес пиксела по начальному адресу кадрового буфера, шагу, размеру и относительной позиции пиксела в буфере: char *GetPixelAddress(char * buffer, int pitch, int byteperpixel. int x, int y) return buffer
pitch
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-разрядный
Аппаратная палитра
з кадрового буфера
00
00
00
1 1 1 1 1 1 1 1
00
00
FF
00
FF
00
Современные видеоадаптеры
289
приложениях просто необходимо использовать самое лучшее — 24- или 32-разрядные кадровые буферы True Color. И в 24-, и в 32-разрядных кадровых буферах красная, зеленая и синяя составляющие представляются 8 битами. В старых видеоадаптерах старшие 8 бит 32-разрядного пиксела обычно оставались неиспользованными. У новых видеоадаптеров для ОС Windows 98 или Windows 2000 в старших 8 битах хранится информация о прозрачности (transparency). Красный сигнал
FF
АА
55
15-разрядный формат пикселов
Зеленый сигнал Синий сигнал
FF
FF
" Рис. 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 ся в 5 битах, что позволяет задействовать 2 уровней интенсивности. В таких
Красный (ОхТСОО)
Зеленый (ОхОЗЕО)
R
G
R
R
R
R
G
G
G
G
Синий (0x001 F) В
В
Старший бит
В
В
В
Младший бит 16-разрядный формат пикселов
Красный (OxFSOO)
R
R
R
R
R
Зеленый (ОхОТЕО) G
G
G
G
G
Синий (0x001 F) 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 байт каждая. На аппаратном уровне такое преобразование выполняется микросхемой R AMD AC (RAM digitalto-analog converter). Microsoft требует, чтобы видеоадаптеры поддерживали программируемые (downloadable) микросхемы RAMDAC для кадровых буферов True Color с целью выполнения гамма-коррекции на аппаратном уровне.
ДНУ
Graphics Blaster RivaTNT AT ТДСНЕ D T 0 D E S КТ OP PCI WENJ ODE8cDEV_0020&SUBSYS_1 \REGIST RY\M achine\Sy stenrAControlS et
<к
640 by 480, 8 bits, 60 Hz, 300Kb 320 by 200, 8 bits, 70 Hz, 62Kb 320 by 200,8 bits, 72 Hz, 62Kb 320 by 200,8 bits, 75 Hz, 62Kb 320 by 240,8 bits, 60 Hz, 75Kb 320 by 240,8 bits, 70 Hz, 75Kb 320 by 240,8 bits, 72 Hz, 75Kb 320 by 240,8 bits, 75 Hz, 75Kb 400 by 300,8 bits, 60 Hz, 117Kb 400 by 300,8 bits, 70 Hz, 117Kb 400 by 300,8 bits, 72 Hz, 117Kb 400 by 300,8 bits, 75 Hz, 117Kb 480 bv 360.8 bits. 60 Hz. 168Kb
Рис. 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-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 Мбайт
Основные буферы
1875 Кбайт
3072 Кбайт
3888 Кбайт
800 х 600 х 32
1024 х 768 х 32
1152x864x32
Внеэкранные буферы 2-буферы Текстуры
3750 Кбайт
6144 Кбайт
7776 Кбайт
800 х 600 х 32 х 2
1024 х 768 х 32 х 2
1152x864x32x2
938 Кбайт
2304 Кбайт
2304 Кбайт
800 х 600 х 16
1024 х 768 х 24
1152x864x32
629 Кбайт
4864 Кбайт
17216Кбайт
Как показано в таблице, если на вашем видеоадаптере установлено 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 появился отдельный зеркальный драйвер, который «видит» данные, переданные настоящему драйверу экрана. С каждым экранным устройством связывается уникальное имя, по которому на данное устройство можно ссылаться в пользовательских приложениях. Имя задается в форме \\.\DISPLAYx, где х — номер в последовательности, начинающейся с 1. Обратите внимание: в программах C/C++ строка должна записываться в виде \\\\.\\DISPLAYx, поскольку служебный символ \ в строках должен экранироваться. В Windows 2000 появилась новая функция EnumDisplayDevices, предназначенная для перечисления всех экранных устройств, установленных в системе. Приведенная ниже функция фиксирует экранные устройства и заполняет список их именами. void AddDisplayDevicestHWND hList)
Современные видеоадаптеры
295
ства (например, NVIDIA RIVA TNT), флаги состояния (например, ATTACHED_TO_DESKTOP | MODESPRUNED|PRIMARY_DEVICE), идентификатор устройства Plug-and-Play и ключ реестра. Зная имя экранного устройства, можно воспользоваться функцией EnumDisplaySettings для получения списка всех форматов кадрового буфера, поддерживаемых устройством, с частотами вертикальной развертки. Код следующего примера выполняет перечисление и заносит в список строку с краткими сведениями о каждом формате. int FrameBufferSize(int width, int height, int bpp) {
int bytepp = ( bpp + 7 ) / 8: // Байт на пиксел int byteps = ( width*bytepp + 3 ) / 4*4: // Байт на строку развертки return height * byteps: // Байт на кадровый буфер
void AddDisplaySettings(HWND hList, LPCTSTR pszDeviceName) DEVMODE dm: dm.dmSize = sizeof(DEVMODE): dm.dmDriverExtra - 0; SendMessagethList. LB_RESETCONTENT. 0. 0): for (unsigned i - 0 : EnumDisplaySettings(pszDeviceName, i, & dm): TCHAR szTemp[MAX_PATH] ;
wsprintf(szTemp. _T("*d by *d. *d bits. Id Hz. *d KB"). dm.dmPelsWidth, dm.dmPelsHeight, dm.dmBitsPerPel. dm . dmDi spl ayFrequency . FrameBuf f erSi ze ( dm . dmPel sWi dth , dm . dmPel sHei ght , dm.dmBitsPerPel) / 1024
{ OISPLAY_DEVICE Dev;
SendMessage(hList, LB ADDSTRING. 0. (LPARAM)szTemp):
Dev.cb - sizeof(Dev);
SendMessage(hList. CB_RESETCONTENT. 0. 0);
for (unsigned i-0.
EnumDisplayDevices(NULL, i. & Dev. 0); i++) SendMessage(hList, CB_ADDSTRING. 0. . (LPARAM) Dev.DeviceName): SendMessagethList. CB_SETCURSEL. 0. 0);
} Для каждого экранного устройства EnumDisplayDevices заносит в структуру DISPLAY_DEVICE имя устройства (например, \\.\DISPLAY1), строку описания устрой-
Для каждого режима EnumDisplaySettings заполняет структуру DEVMODE информацией о ширине и высоте кадрового буфера, количестве бит на пиксел, частоте развертки и т. д. Приведенная функция вычисляет размер одного кадрового буфера на основании полученной информации. Учтите, что при использовании структуры DEVMODE необходима осторожность. В Win32 API документирована открытая структура DEVMODE. Драйвер графического устройства может присоединить к открытым полям DEVMODE закрытые данные, для чего размер дополнительных данных указывается в поле dmDriverExtra. Перед вызовом EnumDisplaySettings программа заполняет поле dmSize размером
296
Глава 5. Абстракция графического устройства
(открытой части) DEVMODE и обнуляет поле dmDriverExtra, чтобы драйвер экрана не пытался получить дополнительные данные. На прилагаемом компакт-диске имеется программа DEVICE, использующая функции EnumDisplayDevices и EnumDisplaySettings. На странице DisplayDevices отображается список экранных устройств, установленных в системе, и поддерживаемые ими форматы кадровых буферов. На рис. 5.5 изображен примерный вид окна программы DEVICE.
Лх|
Device Name Graphics Blaster Riva TNT ATTACHED TO DESKTOP PCI WEN TODEBcDEV 0020bSUBSYS 1 \REGISTRYSM achineSSy sterrAControlS e<
640 by 480, 8 bits, 60 Hz, 300 Kb 320 by 200, 8 bits, 70 Hz, 62 Kb 320 by 200, 8 bits, 72 Hz, 62 Kb 320 by 200, 8 bits, 75 Hz, 62 Kb 320 by 240, 8 bits, 60 Hz, 75 Kb 320 by 240, 8 bits, 70 Hz, 75 Kb 320 by 240, 8 bits, 72 Hz, 75 Kb 320 by 240, 8 bits, 75 Hz, 75 Kb 400 by 300, 8 bits, 60 Hz, 117 Kb 400 by 300, 8 bits, 70 Hz,117Kb 400 by 300, 8 bits, 72 Hz, 117 Kb 400 by 300, 8 bits, 75 Hz,117Kb 480 bv 360. 8 bits. 60 Hz. 168 Kb
Рис. 5.5. Перечисление экранных устройств и режимов
Контекст устройства Видеоадаптеры образуют лишь один класс графических устройств, поддерживаемых графической системой Windows. Другим важным классом графических устройств являются устройства создания жестких копий — принтеры, плоттеры и факсы. Графическая система Windows NT/2000 имеет многоуровневую архитектуру. Верхний уровень состоит из 32-разрядных клиентских DLL, предоставляю-
Контекст устройства
297
щих функции API в распоряжение пользовательских приложений. Например, GDI32.DLL поддерживает функции GDI API для традиционной двумерной графики; DDRAW.DLL — функции DirectDraw API для программирования двумерной графики в играх, а D3DRM.DLL и D3DIM.DLL — функции DirectSD 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: HOC CreateDC (LPCSTR pszDriver. LPCSTR pszDevice. LPCSTR pszOutput, CONST DEVMODE * pdvmlnit): Первый параметр, pszDriver, в программах Win32 для передачи имени драйвера графического устройства не используется — это пережиток эпохи Win 16 API. Допустимыми значениями этого параметра являются только DISPLAY (контекст экранного устройства), NULL и WINSPOOL (контекст устройства для принтера). Второй параметр, pszDevice, определяет имя графического устройства. В нем может передаваться как имя экранного устройства, возвращаемое функцией EnumDisplayOevices, так и имя принтера, указанное в мини-приложении Printers (Принтеры) панели управления. Например, значение \\\\.\\DISPLAY1 или NULL обозначает первичное экранное устройство; значение \\\\.\\DISPLAY2 обычно соответствует вторичному экранному устройству, \\\\.\\DISPLAY3 может обозначать драйвер NetMeeting, обеспечивающий зеркальное воспроизведение экрана на другом мониторе. Третий параметр определяет имя порта, в который передается задание печати. В Win32 для передачи имени порта используется новая функция StartDoc, поэтому этот параметр всегда должен быть равен NULL. Последний параметр, pdvmlnit, содержит указатель на структуру DEVMODE с описанием параметров инициализации. Обычно передается значение NULL — это означает, что драйвер устройства должен использовать текущую конфигурацию устройства, хранящуюся в реестре. Для экранных устройств pdvmlnit может указывать на структуру DEVMODE, возвращаемую функцией EnumDisplaySettings и содержащую высоту, ширину, количество бит на пиксел и частоту вертикальной развертки. Для устройств создания жестких копий pdvmlnit может указывать на структуру DEVMODE, возвращаемую функцией DocumentProperties. Функция CreateDC возвращает манипулятор контекста устройства GDI, который используется при последующих вызовах функций GDI вплоть до последнего вызова DeleteDC, который освобождает системные ресурсы, занятые контекстом устройства; после этого манипулятор контекста становится недействительным. Процесс создания контекста устройства очень сложен. По имени устройства операционная система получает из реестра имя драйвера устройства и загружает драйвер. Для экрана монитора драйвер загружается только при первом вызове CreateDC, а при последующих вызовах используется ранее загруженный драйвер. Фактическая загрузка и инициализация драйвера принтера также происходит при вызове CreateDC. Информация о создании нового контекста устройства для принтера также передается спулеру и библиотеке DLL, обеспечивающей пользовательский интерфейс с драйвером принтера. При загрузке драйвера графического устройства вызывается его главная точка входа DrvEnableDriver. Функция DrvEnableDriver заполняет структуру с номером версии и адресами точек входа всех функций DDI, реализуемых драйвером. Затем графический механизм вызывает функцию DrvEnabl ePDEV, требуя, чтобы драйвер описал свои атрибуты и возможности, а также создал свою структуру
Контекст устройства
299
данных физического устройства. Параметры pdvmlnit и pszDevice, переданные при вызове CreateDC, передаются функции DrvEnablePDEV. Функция DrvEnablePDEV заполняет две важные структуры, GDI INFO и DEVINFO, информацией об атрибутах, возможностях, форматах пикселов и стандартных параметрах устройства. Графический механизм создает свою внутреннюю структуру физического устройства с информацией, возвращаемой функциями DrvEnableDriver и DrvEnablePDEV. Теперь он знает возможности графического устройства и адреса точек входа, к которым следует обращаться при вызове различных графических команд DDL В завершающей фазе создания контекста устройства графический механизм вызывает функцию драйвера DrvEnableSurface; драйвер создает графическую поверхность, на которой и происходит фактический вывод. За подробностями обращайтесь к главе 2 (описание интерфейса DDI) и главе 3 (внутренние структуры данных).
Получение информации о возможностях устройства Контекст устройства хранит (непосредственно или в виде ссылок) большой объем данных о графическом устройстве и его драйвере. Часть информации можно получить при помощи вызовов Win32 API; другая часть хранится во внутренних структурах GDI для упрощения взаимодействия с драйвером. Функция GetDeviceCaps возвращает информацию об атрибутах или возможностях графического устройства по целочисленному индексу: int GetDeviceCaps (HOC 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
0x4001
Версия драйвера; 16 бит в формате OxXYZZ, где X — основная версия ОС, Y — дополнительная версия ОС, a ZZ — помер версии драйвера. Информация сообщается драйвером
TECHNOLOGY
DT_RASOISPLAY
Информация сообщается драйвером. DT_PLOTTER — для плоттеров, DT_RASDISPLAY — для растровых видеоадаптеров, DT_RASPRINTER — для растровых принтеров, DT_RASCAMERA — для растровых камер, DT_CHARSTREAM — для символьных потоков
HORSIZE
320
Ширина физической поверхности в миллиметрах. Для экрана выводится приблизительное значение. Информация сообщается драйвером
VERTSIZE
240
Высота физической поверхности в миллиметрах. Для экрана выводится приблизительное значение. Информация сообщается драйвером
HORZRES
1024
Ширина физической поверхности в пикселах. Информация сообщается драйвером
VERTRES
768
Высота физической поверхности в пикселах. Информация сообщается драйвером
BITSPIXEL
8,16,24,32
Количество смежных битов в каждой цветовой плоскости. Информация сообщается драйвером
PLANES
1
Количество цветовых плоскостей. Информация сообщается драйвером
NUMBRUSHES
-1
Количество кистей устройства
NUMPENS
-1
Количество перьев устройства. Для плоттеров— количество физических перьев
NUMFONTS
0
Количество шрифтов устройства
NUMCOLORS
-1
Количество элементов в палитре устройства или -1, если палитра отсутствует
CURVECAPS
Ox IFF
Возможности вывода кривых. Стандартный ответ:
301
Контекст устройства
Индекс
Пример
Возвращаемое значение и смысл
POLYGONALCAPS
OxFF
Возможности вывода многоугольников. Стандартный ответ: PC_POLYGON|PC_RECTANGLE|PC_WINDPOLYGON| PC_SCANLINE | PC_WIDE | PC_STYLED | PC_WIDESTYL£D | PCJNTERIORS. Обратите внимание: PC_POLYPOLYGON и PC_PATHS не включаются в стандартный ответ, но это не значит, что они не поддерживаются устройством — просто при сообщении возможностей допущена ошибка
TEXTCAPS
0x7807
Возможности вывода текстовых строк. Информация сообщается драйвером
CLIPCAPS
RASTERCAPS
Возможности отсечения. Стандартный ответ: CP_RECTANGLE. Обратите внимание: это не означает, что устройство не может выполнять отсечение по сложным регионам Ох7е99
RC_BITBLT|RC_BITMAP64|RC_GDI20_OUTPUT|RC_DI_BITMAP| RCJDIBTODEV | RCJIGFONT j RC_STRETCHBLT | RC_FLOODFI LL | RCJTRETCHDIB | RC_OP_DX_OUTPUT
ASPECTX
36
Относительная ширина пиксела устройства в интервале от 1 до 100. Информация сообщается драйвером
ASPECTY
36
Относительная высота пиксела устройства в интервале от 1 до 100. Информация сообщается драйвером
ASPECTXY
51
Относительная диагональ пиксела устройства, Sqrt(ASPECTX*2+ASCPECTY A 2). Информация сообщается драйвером
LOGPIXELSX
96 или 120
Логическое разрешение в точках на дюйм (dpi) по ширине. Информация сообщается драйвером
LOGPIXELSY
96 или 120
Логическое разрешение в точках на дюйм (dpi) по высоте. Информация сообщается драйвером
SIZEPALETTE
О, 16 или 256
Количество элементов в системной палитре. Значение действительно лишь в том случае, если RASTERCAPS содержит флаг RC_PALETTE
NUMRESERVED
2 или 20
Количество зарезервированных элементов в системной палитре. Значение действительно лишь в том случае, если RASTERCAPS содержит флаг RC_PALETTE. Ответ генерируется в зависимости от системных настроек
COLORRES
24
Количество бит в представлении одного пиксела
CC_CHORD | CC_CIRCLES | CCJLLIPSES | CCJNTERIORS | CC_PIE | CC_ROUNDRECT | CCJTYLED | CCJJIDE | CCJJIDESTYIED
LINECAPS
OxFE
Возможности вывода линий. Стандартный ответ:
Растровые возможности. Частично сообщаются драйвером, частично берутся из стандартного ответа:
LC_POLYLINE | LC_MARKER | IC_POLYMARKER | LC_WIDE | LCJTYLED | LC~WIDESTYLED | LCJNTERIORS
Продолжение
302
Глава 5. Абстракция графического устройства
Таблица 5.3. Продолжение Индекс
Пример
Возвращаемое значение и смысл
PHYSICALWIDTH
О
Для устройств создания жестких копий: ширина физической страницы в единицах устройства. Информация сообщается драйвером
PHYSICALHEIGHT
О
Для устройств создания жестких копий: высота физической страницы в единицах устройства. Информация сообщается драйвером
PHYSICALOFFSETX
О
Для устройств создания жестких копий: ширина непечатаемого левого поля в единицах устройства. Информация сообщается драйвером
PHYSICALOFFSETY
О
Для устройств создания жестких копий: высота непечатаемого верхнего поля в единицах устройства. Информация сообщается драйвером
SCALINGFACTORX
100
Для устройств создания жестких копий: коэффициент масштабирования по оси х
SCALINGFACTORY
100
Для устройств создания жестких копий: коэффициент масштабирования по оси у
VREFRESH
60
Вертикальная частота развертки для текущего видеорежима. Информация сообщается драйвером
DESKTOPHORZRES
DESKTOPVERTRES
BLTALIGNMENT
1024
768
О
Ширина рабочего стола в пикселах (отличается от ширины одного экрана при работе с несколькими мониторами) Высота рабочего стола в пикселах (отличается от высоты одного экрана при работе с несколькими мониторами) Предпочтительное выравнивание при блиттинге на устройство. Нулевое значение означает, что для устройства используется аппаратное ускорение, поэтому выравнивание может быть произвольным. Информация сообщается драйвером
SHADEBLENDCAPS
О
Возможности альфа-наложения и градиентной заливки. Информация сообщается драйвером
COLORMGMT CAPS
2
Возможности управления цветом. Информация сообщается драйвером
Перед выводом на устройство создания жестких копий хорошо написанная программа всегда должна проверять точные размеры бумаги и полей. Следует помнить, что приложение может изменить ориентацию листа и перейти от стандартной книжной ориентации к альбомной; при этом ширина и высота листа, а также левые и верхние поля меняются местами.
303
Контекст устройства
Для приложений, работающих под управлением Windows NT/2000, проверка графических возможностей устройства (CURVECAPS, LINECAPS, POLYGONCAPS, CLIPCAPS и т. д.) уже не столь важна, поскольку при необходимости графический механизм помогает драйверу устройства в обработке графических команд GDI. Проверяя возможности контекста устройства, приложение также может оптимизировать свое быстродействие. Например, если устройство не поддерживает градиентных заливок, приложение может имитировать заливку своими средствами, упростить или вообще отказаться от градиентной заливки. Приложения, работающие в 8-разрядных режимах с 256 цветами, могут использовать графические заготовки с уменьшенным количеством цветов (вместо 24-разрядных). В системе Windows NT/2000 графический механизм хранит гораздо больше информации об устройстве, чем можно получить при помощи функции GetDeviceCaps, предназначавшейся для 16-разрядного GDI. Драйвер графического устройства должен сообщить информацию о выводе стилевых линий, о многочисленных полутоновых параметрах, о поддержке устройством цветового пространства CIE (Commission Internationale de L'Eclairage) и некоторых внутренних графических возможностях. Например, графическому механизму может потребоваться информация о том, поддерживает ли устройство непрозрачные прямоугольники при выводе текста, поддерживается ли спулинг EMF, допускается ли закраска непрозрачных текстовых прямоугольников произвольной кистью, поддерживаются ли дробные координаты при выводе текста, поддерживается ли 4-разрядное сглаживание текста и т. д.
х| indeK
1 Value
TECHNOLOGY DRIVERVERSION HORZSIZE VERTSIZE HORZRES VERTRES LOGPIXELSX „, LOGPIXELSY BITSPIXEL PLANES NUMBRUSHES NUMPENS л- NUMMARKERS NUMFONTS NUMCOLORS PDEVICESIZE CURVECAPS LINECAPS
#[--,' Т "'"•' "
- ;-';-^-"; ' "-'|Г"2
*
1
0x4000 320mm 240mm 1152 pixels ' 864 pixels 96dpi 96 dpi 32 bits 1 planes -1 -1 0 0 -1 0
;
i
—I
Iff
+1'
fe
'J
_tJl
!/! г
?*!?4/^ -. > .'*uV^«
Рис. 5.6. Получение информации о возможностях графического устройства
304
Глава 5. Абстракция графического устройства
Функция GetDeviceCaps работает элементарно и не требует пространных комментариев. В программе DEVICE кнопка GetDeviceCaps на странице Display Devices открывает диалоговое окно с перечнем всех флагов устройства (рис. 5.6).
Атрибуты в контексте устройства В графических командах GDI используются два вида данных — атрибуты, общие для разных команд, и значения, специфические для конкретной команды. Конечно, было бы крайне неэффективно требовать, чтобы общие параметры и атрибуты снова и снова указывались в программе. В Windows GDI в контексте устройства хранятся следующие атрибуты: О система координат, режим отображения и мировое преобразование; О основной цвет, цвет фона, палитра и параметры управления цветом; О параметры вывода линий; О параметры заливки областей; О шрифт, межсимвольные интервалы и выравнивание текста; О режим масштабирования растров; О регион отсечения; О ряд других атрибутов. У каждого атрибута имеется набор допустимых значений и значение по умолчанию, заносимое в контекст устройства при создании. Для каждого атрибута обычно определяется пара функций Win32 API, предназначенных для чтения и присваивания ему нового значения. В табл. 5.4 перечислены атрибуты контекста устройства, значения по умолчанию и функции для работы с ними. Таблица 5.4. Атрибуты контекста устройства (Windows 2000) Атрибут
Значение по умолчанию
Функции доступа
Ассоциированный манипулятор окна
NULL
WindowFromDC (только чтение)
Базовая точка контекста Растр
Растр 1 x 1
Значение по умолчанию
Функции доступа
Габариты окна
{1,1}
GetWindowExtEx, SetWindowExtEx, ScaleWindowExtEx
Базовая точка окна
{0,0}
GetWindowOrgEx, SetWindowOrgEx, OffsetWindowOrgEx
Преобразование
Матрица тождественного преобразования
GetWorldTransform, SetWorldTransform, Modi fyWorldTransform
Цвет фона
Системный цвет фона
GetBkColor, SetBkColor
Цвет текста
Черный
GetTextColor, SetTextColor
Палитра
DEFAULT_PALETTE
GetCurrentObject, EnumObjects, SelectPalette
Регулировка цвета
GetColorAdjustment, SetColorAdjustment
Цветовое пространство
GetColorSpace, SetColorSpace
Режим ICM
SetlCMMode
Профиль ICM
GetlCMProfile, SetlCMProfile
Текущая позиция пера
{0,0}
GetCurrentPositionEx, MoveToEx, LineTo, BezierTo,...
Бинарная растровая операция
R2_COPYPEN
GetROP2, SetROP2
Режим вывода фона
OPAQUE
GetBkMode, SetBkMode
Логическое перо
BLACK_PEN
SelectObject, GetCurrentObject GetDCPenColor, SetDCPenColor
GetDCOrgEx (только чтение)
Направление дуг
GetCurrentObject, SelectObject (только для совместимых контекстов устройств)
GetArcDirection, SetArcDirection
Угловой лимит
10.0
GetMiterLimit, SetMiterLimit
Логическая кисть
WHITEJRUSH
SelectObject, GetCurrentObject
GM_COMPATIBLE
GetGraphicsMode, SetGraphicsMode
Режим отображения
ММ_ТЕХТ
GetMapMode, SetMapMode
Габариты области просмотра
{1, 1}
GetViewportExtEx, SetViewPortExtEx, SealeVi ewportExtEx
{0, 0}
Атрибут
Цвет пера DC
Графический режим
Базовая точка области просмотра
305
Контекст устройства
GetVi ewportOrgEx, SetVi ewportOrgEx, OffsetViewportOrgEx
GetDCBrushColor, SetDCBrushColor
Цвет кисти DC Базовая точка кисти
{ 0, 0 }
GetBrushOrgEx, SetBrushOrgEx
Режим заполнения многоугольников
ALTERNATE
GetPolyFillMode, SetPolyFillMode Продолжение ^
306
Глава 5. Абстракция графического устройства
307
Контекст устройства
Таблица 5.4. Продолжение Атрибут
Значение по умолчанию
Функции доступа
Режим масштабирования растров
STRETCH_ANDSCANS
GetStretchBl tMode, SetStretchBl tMode
Логический шрифт
Системный шрифт
Дополнительные межсимвольные интервалы
SelectObject, GetCurrentObject, GetCharWidth32, GetKerningPairs, GetTextMetrics,... GetTextCharacterExtra, SetTextCharacterExtra
Флаги подстановки шрифтов
SetMapperFlags
Выравнивание текста
TA_TOP|TA_LEFT
GetTextAlign, SetTextAlign
Выключка текста (выравнивание по ширине)
{0,0}
SetTextJustification (только присваивание)
Раскладка
Technology width height DC Origin Window Bitmap Graphics Mode Mapping Mode Viewport Extent Viewport Origin Window Extent Window Origin WnrM tt
1 1152
864 {0,0} 0x0 "" 0x1050038 1 1
{1,1} {0,0}
{1.1} {0,0}
и nnnnnn nnnnnnn nnnonnn i nnnnnn nnmnnn nnnnnnni
GetLayout, SetLayout
Траектория
BeginPath, dosePath, EndPath, GetPath
Область отсечения
Клиентская область, вся поверхность устройства
Метарегион
SelectObj'ect, GetClipBox, GetClipRgn, SelectClipRgn, ExcludedipRect, IntersectClipRect GetMetaRgn, SetMetaRgn
Ограничивающие прямоугольники
GetBoundsRect, SetBoundsRect
Эти атрибуты контекста устройства будут подробно рассмотрены ниже А пока вы лишь получили общее представление о количестве информации, хранящейся в контексте устройства. Ц
е
П РеЧИСЛеНЫ ат
иб
к
п пп г Р У™ онтекстов устройств, поддерживаемые в 2000. Список включает почти все атрибуты, поддерживаемые на разп я п Г Ш w-nf ТФТГХ- Большинств° атрибутов было унаследовано из 16разрядного Windows API. Некоторые атрибуты (например, мировые преобразования координат) в полной мере поддерживаются только в Windows NT/2000 ки^Гп^5 т\™indowl 200° появился ряд новых атрибутов - например,' кисть DC, перо DC и атрибуты ICM. и р. В программе DEVICE кнопка DC Attributes на странице Display Devices вызывает диалоговое окно для вывода списка всех доступных атрибутов контекста (рис. 5.7). В этом диалоговом окне предусмотрено несколько возможностей получения манипулятора контекста устройства. В частности, программа может создать новый контекст функцией CreateDC или получить контексты устройств связанные с различными окнами. учрииств,
Рис. 5.7. Атрибуты контекста устройства
Связь контекста устройства с окном Контекст устройства, созданный функцией CreateDC, можно рассматривать как графическую поверхность, распространяющуюся на всю площадь устройства — на весь экран для экранных устройств или на всю страницу для принтеров. Однако функция CreateDC не предоставляет стандартный способ получения контекста устройства в среде Microsoft Windows и обычно применяется только при работе с устройствами создания жестких копий (таких, как принтеры).
Графический вывод в многооконной среде Графический вывод в первую очередь ориентируется на экран монитора — ресурс, совместно используемый несколькими приложениями в операционной системе Windows. Обычные приложения Windows работают в оконном режиме, при котором вывод каждого приложения ограничивается определенной частью экрана. Окно обычно имеет прямоугольную форму, а его параметры указываются при вызове функции CreateWindow. Впрочем, операционная система позволяет создать окно произвольной формы — в виде прямоугольника с закругленными углами, эллипса или многоугольника. Чтобы изменить форму окна, достаточно создать объект региона и передать его манипулятор функции SetWindowRgn. Регион задается в экранных координатах относительно базовой точки окна. Ниже приведен простой пример создания обычного окна с последующим преобразованием его к эллиптической форме.
308
Глава 5. Абстракция графического
const TCHAR szProgram [] = _Т("Window Region"); const TCHAR szRectWin [] = _T("Rectangular Window"): const TCHAR szEptcWin [] = _T("Elliptic'Window"); int WINAPI WinMainCHINSTANCE hlnstance. HINSTANCE. LPSTR. int) { HWNO hWnd •= CreateWindow(_T("EDIT"). NULL, WS_OVERLAPPEDWINDOW. 10, 10, 200, 100, GetDesktopWindowO, NULL, hlnstance. NULL): ShowWi ndow(hWnd. SW_SHOW); SetWindowTexUhWnd. szRectWin); MyMessageBox(NULL, szRectWin, szProgram. MB_OK): HRGN hRgn = CreateEllipticRgntO. 0. 200, 100): SetWindowRgn(hWnd, hRgn. TRUE): SetWindowTexUhWnd, szEptcWin); MessageBoxtNULL. szEptcWin. szProgram, MB_OK); DestroyWindow(hWnd): return 0; }
На рис. 5.8 изображены два окна: обычное прямоугольное и эллиптическое. Прямоугольное окно (слева) имеет обычную для окон верхнего уровня рамку и строку заголовка, тогда как у эллиптического окна рамка и строка заголовка отсекаются по границам эллиптического региона.
Rectangular Window
elliptic Window
Рис. 5.8. Окна разной формы ПРИМЕЧАНИЕ Непрямоугольные окна и специализированные неклиентские области считаются новым течением в разработке пользовательских интерфейсов Windows-приложений. При выводе в окнах нетрадиционной формы используются регионы, определяемые при помощи растровых или векторных изображений. Обработка неклиентских сообщений заменяет стандартную обработку неклиентской области.
Контекст устройства
309
Несколько окон, одновременно находящихся на экране, могут перекрывать друг друга. В старой реализации Microsoft Windows передние окна полностью или частично блокировали окна, находящиеся сзади, хотя в своей последней реализации Windows 2000 компания Microsoft стремится к интерпретации окон как экранных объектов, которые могут объединяться с использованием различных операторов. В результате перекрытия у каждого окна появляется еще один важный атрибут — видимая часть. Окно делится на две части: клиентскую и неклиентскую. Неклиентская часть включает рамку, строку заголовка, строку меню, панели инструментов, полосы прокрутки и прочие служебные элементы. К клиентской части относится вся площадь окна, не входящая в неклиентскую часть, — как правило, это прямоугольная область в середине окна. Вывод в неклиентской области обычно обеспечивается стандартной функцией окна, DefVJindowProc, представляемой модулем управления окнами (user32.dll). DefWindowProc имеет доступ к стилю окна, тексту окна, информации фокуса и другим данным, необходимым для прорисовки неклиентской области. В большинстве случаев пользовательское приложение обеспечивает вывод в клиентской части окна. В многозадачных средах типа Windows состояние экрана неустойчиво. В любой момент времени какое-нибудь приложение может вызвать временное окно, вывести информацию и прекратить свое существование. Приложения, продолжающие работать, несут полную ответственность за восстановление нормального изображения на экране. В этом случае операционная система посылает окнам, чье изображение было нарушено, сообщения с запросом на перерисовку. Окнополучатель не знает, что произошло на экране, поэтому ему нужно точно сообщить, как часть изображения нуждается в перерисовке. В противном случае перерисовка всего окна приведет к напрасному расходованию драгоценных ресурсов. Ниже перечислены факторы, которые должны учитываться контекстом устройства при выводе в окне. О Базовая точка — левый верхний угол окна. О Размеры — ширина и высота окна. О Регион окна — подмножество прямоугольной области, определяемой базовой точкой и размерами окна (для окон нетрадиционной формы). О Видимость — перерисовывается только видимая (не перекрытая) часть региона окна. О Все окно или клиентская область — хочет ли приложение обеспечить специализированную прорисовку неклиентской области или его интересы ограничиваются клиентской областью? ь О Обновляемая область — участок окна, реально нуждающийся в обновлении.
Получение контекста устройства, связанного с окном Функция CreateDC не позволяет приложению создать контекст устройства, связанный с конкретным окном. В Win32 API предусмотрено несколько функций для создания контекстов устройств, связанных с окнами:
310 HOC HOC HOC HOC
Глава 5. Абстракция графического устройства
GetWindowDC (HWND hWnd); GetDCCHWND hWnd): GetDCEx(HWND hWnd. HRGN hrgnClip. DWORD flags); BeginPaint(HWND hWnd. LPPAINTSTRUCT IpPaint):
Функция 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
Использовать контекст устройства из кэша диспетчера окон, даже если у класса окна установлен флаг стиля CS_OWNDC или CS_CLASSDC
DCX_PARENTCLIP
Использовать прямоугольник и видимый регион родительского окна, не обращая внимания на флаги стиля родительского окна WS_CLIPCHILDREN и CS_PARENTDC
DCX_CLIPSIBLINGS DCX_CLIPCHILDREN OCX_NORESETATTRS
Исключить из видимого региона все регионы соседних окон Исключить из видимого региона все регионы дочерних окон Не восстанавливать значения по умолчанию для атрибутов контекста устройства
DCX_LOCKWINDOWUPDATE
Игнорировать блокировку обновления, установленную функцией LockWindowUpdate
DCX_EXCLUDERGN
Исключить регион, заданный параметром hrgnClip, из видимого региона
DCXJNTERSECTRGN
Построить новый видимый регион как пересечение региона, заданного параметром hrgnCI i p, с текущим видимым регионом
DCX'EXCLUDEUPDATE DCXJNTERSECTUPDATE
Исключить обновляемый регион окна из видимого региона Построить новый видимый регион как пересечение обновляемого региона с текущим видимым регионом
DCXJ/ALIDATE
Объявить содержимое окна действительным — другими словами, сбросить обновляемый регион
OxlOOOO
Недокументированный флаг, который автоматически делает вызов GetDCEx успешным
312
Глава 5. Абстракция графического устройства
При таком обилии флагов GetDCEx может использоваться для замены других функций. Скажем, вызов GetDCEx(hWnd, NULL, DCX_WINDOW|DCX_NORESETATTRS) легко заменяет GetWindowDC(hWnd), а вызов GetDCEx(hWnd, NULL, DCX_NORESETATTRS) заменяет GetDC(hWnd). При помощи дополнительных флагов можно отменить использование системой контекста устройства, принадлежащего окну или классу, а также исключить из видимого региона соседние и дочерние окна. Кроме того, функция GetDCEx позволяет видоизменить видимый регион с использованием дополнительного параметра-региона или обновляемого региона окна и даже сбросить данные обновляемого региона окна. Мы добрались до последнего способа создания контекста устройства, связанного с конкретным окном. Функция BeginPaint возвращает контекст устройства, предназначенный для обработки сообщения WM_PAINT. Если рассматривать BeginPaint только с точки зрения возвращаемого контекста, ее можно реализовать следующим образом: HOC BeginPaintOtHWND hWnd. LPPAINTSTRUCT IpPaint) { DWORD flags = 0: if ( GetWindowLongthWnd. GWL_STYLE) & WS_CLIPCHILDREN) flags |= DCX_CLIPCHILDREN: if ( GetWindowLong(hWnd. GWL_STYLE) & WS_CLIPSIBLINGS) flags |= DCX_CLIPSIBLINGS: return GetDCExChWnd. NULL, flags | DCXJNTERSECTUPDATE | DCXJALIDATE):
} Функция BeginPaint проверяет стиль окна и определяет, нужно ли исключить из видимого региона дочерние и соседние окна, после чего определяет пересечение видимого региона с обновляемым регионом окна. При этом содержимое окна объявляется действительным, то есть обновляемый регион сбрасывается. Флаги DCX_CACHE и DCX_NORESETATTRS в данном случае не используются, поэтому функция GetDCEx должна проверить стиль окна и узнать, как следует поступить в этой ситуации. Впрочем, настоящая реализация BeginPaint решает и другие задачи. Например, если каретка находится в выводимом регионе, BeginPaint скрывает ее, чтобы предотвратить возможное стирание каретки. Функция BeginPaint посылает сообщение WM_ERASEBKGND обработчику сообщений окна. Если приложение обрабатывает это сообщение, оно получает возможность вывести однородный или растровый фон. Если сообщение передается стандартной функции окна 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 Кбайт.
Классовый контекст устройства Флаг CS_CLASSDC поля стилей структуры WNDCLASS сообщает модулю управления окнами, что для данного класса следует создать контекст устройства, совместно используемый всеми окнами класса. Такой контекст называется классовым контекстом устройства (class device context). Классовый контекст создается при создании первого экземпляра окна данного класса и инициализируется значениями по умолчанию.
314
Глава 5. Абстракция графического устройства
При вызове функций GetDC, GetWi ndowDC и BeginPaint для окна, относящегося к этому классу, возвращается контекст устройства, связанный с классом окна, с обновленным прямоугольником вывода, видимым регионом и пустой областью отсечения. Все остальные атрибуты классового контекста (логическое перо, цвет текста, режим отображения и т. д.) сохраняют прежние значения. После завершения вывода функция ReleaseDC или EndPaint возвращает контекст устройства классу, не уничтожая его и не сбрасывая значения атрибутов. Классовый контекст устройства уничтожается лишь с уничтожением последнего окна класса. Если вы где-нибудь читали, что для классового контекста устройства можно опускать вызовы ReleaseDC и EndPaint, поскольку они все равно ничего не делают, — забудьте об этом. Подобные рекомендации вредны; ради ничтожной выгоды вы можете нарваться на большие неприятности. Кстати, именно EndPaint восстанавливает каретку, скрываемую при вызове BeginPaint. Классовые контексты устройств удобно использовать для управляющих окон, которые выводятся с одними и теми же атрибутами, поскольку это сокращает время на подготовку контекста к выводу и его освобождение. К числу других преимуществ классовых контекстов устройств относится экономия памяти. В наше время классовые контексты устройств поддерживаются для обеспечения совместимости. Их преимущества становятся несущественными на фоне увеличения объема памяти и быстродействия процессора, а также архитектуры защищенных адресных пространств Win32. Классовые контексты устройств не рекомендуется использовать в программировании Win32.
Закрытый контекст устройства Флаг CS_OWNDC поля стилей структуры WNDCLASS сообщает модулю управления окнами, что для каждого окна, созданного на базе данного класса, должен создаваться отдельный контекст устройства. Таким образом, каждое окно на протяжении всего жизненного цикла связано со специальным контекстом устройства. Такие контексты устройств называются закрытыми (private). Закрытый контекст устройства всего один раз инициализируется значениями по умолчанию. При каждом вызове GetDC, GetWindowDC и BeginPaint загружается закрытый контекст окна с новым прямоугольником вывода и видимым регионом. Получив контекст устройства, приложение может изменять его атрибуты и выполнять графические команды. Функции ReleaseDC и EndPaint возвращают контекст устройства окну, не изменяя его, поэтому при следующем получении контекста его атрибуты (такие, как перо и кисть) сохраняют прежние значения. В документации MSDN закрытые контексты устройств описаны невразумительно (см. раздел «Private Display Device Contexts»). В частности, там утверждается, что приложение должно получать манипулятор закрытого контекста только один раз и многократно использовать его, а также — что приложение может включить обновляемый регион в обработку сообщения WM_PAINT при помощи функции Begi nPai nt. Закрытые контексты устройств обеспечивают максимальное быстродействие ценой максимальных затрат памяти. Контекст устройства использует ресурсы трех типов — манипулятор GDI, память в адресном пространстве пользовательского приложения и память в адресном пространстве ядра. Закрытые контексты
Контекст устройства
315
устройств имеют смысл только для окон со сложными атрибутами, подготовка которых занимает много времени, и для окон, нуждающихся в частом обновлении. Рекомендуется использовать приватные контексты лишь в тех случаях, когда фактор быстродействия значительно важнее повышенных затрат памяти и ресурсов GDI.
Родительский контекст устройства Флаг CS_PARENTDC не связан с той проблемой, которую пытаются решать закрытые и классовые контексты устройств. При установке этого флага функция GetDC или BeginPaint для дочернего окна использует прямоугольник вывода и видимый регион родительского окна для подготовки контекста устройства; вот почему эти контексты устройства называются родительскими (parent). Родительский контекст устройства выделяется из кэша, поэтому его атрибуты инициализируются значениями по умолчанию. Отличие состоит в том, что родительский контекст устройства наследует прямоугольник вывода и видимый регион родительского окна, что позволяет сэкономить время на вычисление прямоугольника вывода и видимого региона дочернего окна. Флаг CS_PARENTDC учитывается лишь в простом случае, когда дочернему окну хочется задействовать параметры родительского окна при своей прорисовке. Он игнорируется в ситуациях, когда родительское окно использует закрытый или классовый контекст устройства, когда родительское окно отсекает свои дочер-„ ние окна, а также когда дочернее окно отсекает свои дочерние или соседние окна.
Прочие контексты устройств До настоящего момента мы ограничивались рассмотрением контекстов устройств, созданных функцией CreateDC, и контекстов, связанных с окнами. Эти два типа контекстов обеспечивают полноценные средства для работы с устройством, то есть они позволяют получать информацию и передавать графические команды видеоадаптеру или принтеру. В среде Windows существуют и другие разновидности контекстов устройств — а именно, информационные, совместимые и метафайловые контексты.
Информационный контекст устройства Иногда потребности приложения ограничиваются простым получением атрибутов графического устройства. Например, при загрузке документа текстовый редактор должен узнать у стандартного принтера размеры бумаги и полей, чтобы правильно отформатировать документ в стиле WYSIWYG. В таких ситуациях Windows позволяет создать усеченный контекст устройства, называемый информационным контекстом. Информационный контекст создается функцией CreateIC: HOC CreatelCd-PCTSTR pszDriver. LPCTSTR pszDevice. LPCTSTR pszOutput. CONST DEVMODE * pdvmlnist):
316
Глава 5. Абстракция графического устройства
Функция CreateIC аналогична CreateDC, однако она работает быстрее и расходует меньше памяти. Попытки графического вывода по манипулятору ин-. формационного контекста, возвращённому CreateIC, просто игнорируются. Информационный контекст удаляется функцией DeleteDC (функции DeleteIC не существует).
Совместимый контекст устройства Предполагается, что контекст устройства обеспечивает аппаратно-независимый интерфейс приложения с графическими устройствами. Однако контексты устройств, возвращаемые описанными выше функциями, позволяют выводить графику только на физических устройствах — таких, как видеоадаптеры и принтеры. Работа этих устройств обеспечивается драйверами, получающими низкоуровневые команды от графического механизма. Но в некоторых ситуациях бывает очень удобно осуществлять вывод на графическом устройстве, имитируемом в памяти в виде растра. Совместимый контекст устройства (memory device context) позволяет выполнять вывод на связанном с ним растре или копирование растра на поверхность другого графического устройства. HOC CreateCompatibleDCCHDC hDC);
Параметр hDC определяет существующий контекст устройства, с которым должен быть «совместим» создаваемый контекст. Совместимый контекст устройства использует растр в качестве графической поверхности. По умолчанию при создании совместимого контекста этот растр состоит из одного пиксела. Win32 содержит функции для создания растров и их связывания с поверхностью (функция SelectObject). Совместимые контексты удаляются функцией DeleteDC. Совместимый контекст наследует многие атрибуты от своего «эталонного» контекста. Более того, для совместимого контекста функция GetDeviceCaps возвращает те же результаты, как и для эталонного контекста. Совместимый контекст устройства с флагом DT_RASPRINTER в атрибуте TECHNOLOGY способен сбить с толку функцию, работающую как с совместимыми, так и с обычными контекстами устройств. Совместимые контексты устройств — весьма полезная штука. Мы подробно рассмотрим их в главе 10, при описании аппаратно-зависимых растров (DDB) и DIB-секций.
Метафайловый контекст устройства Другой разновидностью контекста, не соответствующего реальному физическому устройству, является метафайловый контекст устройства. Совместимый контекст позволяет сформировать растр с использованием графических команд GDI; метафайловый контекст устройства позволяет сохранить команды GDI в виде потока данных или дискового файла, который затем воспроизводится как аудиозапись или видеоклип. Главное отличие между этими типами контекстов состоит в том, что совместимый контекст для хранения результатов вывода создает растр с фиксированными размерами и разрешением, а метафайловый контекст
317
Контекст устройства
сохраняет векторные и растровые команды, которые затем точно масштабируются по разным размерам. Метафайловые контексты устройств создаются двумя функциями. Одна функция генерирует метафайлы WinlG, а другая — расширенные метафайлы Win32: HOC CreateMetaFileCLPCTSTR IpszFile); HDC CreateEndMetaFile(HDC hdcRef, LPCTSTR IpszFileName, CONST RECT * IpRect,
LPCTSTR IpDescription); Как метафайлы Windows (метафайлы WinlG), так и расширенные метафайлы широко используются в коммерческих приложениях для хранения графических заготовок (cliparts). Расширенные метафайлы также занимают важное место в реализации спулинга в Windows 95, 98, NT и 2000. Метафайлам посвящена глава 16 настоящей книги. Подведем итог: контекст устройства — удобная концепция, используемая в Windows API для обеспечения аппаратно-независимого графического вывода. В этом разделе мы познакомились с разными классами контекстов устройств, рассмотрели способы их создания, атрибуты и методы для работы с атрибутами. В табл. 5.6 приведена краткая сводка разных контекстов устройств и их характеристик. Таблица 5.6. Краткая сводка контекстов устройств
Тип контекста
Создание, уничтожение
Применение
Общий контекст устройства
CreateDC, DeleteDC
Доступ ко всей поверхности устройства, вывод на первичный и вторичный экран, зеркальное воспроизведение и устройства создания жестких копий
Контекст устройства, связанный с окном
GetWindowDC, GetDC, GetDCEx, BeginPaint, EndPaint, ReleaseDC
Вывод в части экранной поверхности, соответствующей видимому региону окна или его клиентской области с исключением регионов дочерних и соседних окон. Контекст устройства, возвращаемый функцией BeginPaint, ограничивает область вывода той частью, которая входит в обновляемый регион окна. Контексты этого типа делятся на обычные, классовые, закрытые и родительские
Информационный контекст
CreateIC, DeleteDC
Получение информации о возможностях устройства и драйвера
Совместимый контекст
CreateCompatibleDC, DeleteDC
Вывод на растровой поверхности в памяти и передача изображения в другой контекст устройства
Метафайловый контекст
CreateMetaFile, CreateEnhMetaFile, DeleteDC
Запись команд 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 Графический механизм создает структуру логического устройства для хранения информации о драйвере графического устройства. Драйвер графического устройства может существовать в нескольких воплощениях с различными параметрами — например, форматом пикселов кадрового буфера или настройками печати. Это позволяет осуществлять динамическое переключение видеорежимов и одновременный спулинг нескольких заданий. При создании очередного экземпляра драйвера устройства ему по запросу графического механизма или приложения передается структура DEVMODE; драйвер возвращает две структуры с информацией об атрибутах и возможностях устройства и драйвера. Графический механизм сохраняет полученную информацию в структуре физического устройства. Графический драйвер также создает собственную структуру данных и передает ее манипулятор графическому механизму. В структуре физического устройства хранится большой объем информации о драйвере — размеры, возможности, ограничения, особые штриховые кисти, полутоновые узоры и т. д.
320
Глава 5. Абстракция графического устройства
Структура контекста устройства содержит указатели на структуры логического и физического устройства. Из структуры физического устройства берется информация, возвращаемая пользовательскому приложению в ответ на запрос GetDeviceCaps. Кроме того, структура физического устройства сообщает графическому механизму, каким образом графические команды GDI должны разбиваться на вызовы DDI, обслуживаемые драйвером устройства. Например, поддерживает ли драйвер работу с кривыми Безье или же они должны разбиваться на се?менты? По указателям на функции косвенного вызова в структуре логического устройства графический механизм выходит на функцию, обрабатывающую тот или иной вызов DDL Таким образом, все аппаратно-зависимые аспекты графической системы Windows инкапсулируются в этих двух структурах. Кроме того, в контекст устройства входят поля, обеспечивающие его связь с окном, растром или метафайлом, а также объекты и атрибуты Win32 API. С одними полями можно работать средствами Win32 API, другие частично или полностью скрыты от пользовательских приложений. Некоторые поля доступны только для чтения (скажем, базовая точка контекста устройства); другие доступны как для чтения, так и для записи. По соображениям быстродействия некоторые часто используемые атрибуты контекстов устройств хранятся в пользовательском адресном пространстве, что позволяет легко получить их значения без переключения в режим ядра и обратно. Впрочем, основная часть контекста устройства хранится в адресном пространстве режима ядра, в котором работают графический механизм и нормальные драйверы графических устройств. GDI предпринимает особые меры по защите контекстов устройств — впрочем, как и остальных объектов GDI, которые будут рассмотрены в следующей главе. При создании контекста пользовательскому приложению предоставляется только его манипулятор, по которому приложение ссылается на контекст при вызове функций GDI. По манипулятору контекста устройства GDI находит элемент таблицы объектов GDI (см. следующую главу), содержащий указатели на обе структуры данных контекста (пользовательского режима и режима ядра). Структуру контекста устройства в Windows 2000 в некоторой степени иллюстрирует рис. 5.9. В пользовательском режиме контекст устройства представлен структурой DCATTR, содержащей практически все атрибуты и объекты, задействованные средствами Win32 API (кроме палитры, цветового пространства и т. д.). В режиме ядра используется структура DCOBJ, в которой содержится прямоугольник окна, регион отсечения, системный регион (prgnVis на рисунке), регион Рао и другие регионы. Поле PPDEV содержит указатель на структуру физического устройства, PDEV_WIN32K. В полях GDI INFO, DEVINFO и AHSURF структуры PDEV_WIN32K хранится информация, полученная от драйвера устройства. Поле PLDEV содержит указатель на структуру логического устройства, LDEV_WIN32K. Большая часть структуры LDEV_WIN32K занята таблицей из 89 указателей на функции косвенного вызова, APFN, которая также дублируется в структуре PDEV_WIN32K. Контексты устройств имеют много общего с объектами в объектно-ориентированных системах и языках типа C++. Различные атрибуты, хранящиеся в контексте устройства, соответствуют переменным класса, а таблица функций, хранящаяся в глубинах контекста устройства, в точности аналогична таблице виртуальных функций в объекте C++, содержащем виртуальные функции. Драйвер
321
Пример: родовой класс рамочного окна
графического устройства обеспечивает актуализацию абстрактного контекста, реализуя функции DDL После того как контекст устройства должным образом подготовлен, приложение работает с ним при помощи стандартного набора функций, не беспокоясь о том, как реализуются графические вызовы.
1 1 Таблица объектов GDI
anpaev dctype flgraphics Палитра Цветовое пространство Логическое перо hdcsave Логическое перо Траектория Цвет фона prgnClip Основной цвет prgnMeta Графический режим prgnAPI гор2 prgnVis Режим заполнения фона Режим заливки фигур prgnRao erclWindow strchbltmode ppdev xform hsem Базовая точка окна
dcattr
dcobj
ppdevnext hsemdvlck hsempointer spritestate hlfntdefault ahsurf devinfo gdiinfo psurface hspooler nextldev prevldev ddglobal pgraphisdev levtype pdevmode 1 cRefs pldev J pgdidrvinfo uldrvversion apfn[89] apfn[89]
pdev_win32k
Idev_win32k
Рис. 5.9. Контекст устройства Windows 2000 и его структуры данных
Рисунок 5.9 ни в коем случае не претендует на полноту. Обратитесь к главе 3 за дополнительной информацией или займитесь собственными исследованиями, используя либо WinDbg с расширением отладчика GDI, либо программу Fosterer из главы 3. В главе 4 представлена программа, которая модифицирует таблицу функций GDI в структуре PDEV_WIN32K с целью отслеживания вызовов интерфейса DDL В главе 2 приведен пример реализации интерфейса DDI в драйвере принтера.
Пример: родовой класс рамочного окна Начиная с этой главы, работа практически всех графических примитивов GDI будет поясняться конкретными примерами. Почти во всех программах, написанных ранее, пользовательский интерфейс состоял из диалогового окна с несколькими вкладками-страницами — для демонстрации GDI API этого явно недостаточно. В этом разделе мы разработаем родовой набор классов для написания Windows-программ, удовлетворяющий перечисленным ниже условиям. О Главное окно программы имеет строку заголовка, меню, системное меню и рамку, которую можно перетаскивать для изменения размеров главного окна. О В главном окне находится панель инструментов для ускоренного вызова часто используемых команд. Кнопки панели инструментов снабжаются наглядными растровыми изображениями и всплывающими подсказками (tooltips).
322
Глава 5. Абстракция графического устройства
О В главном окне присутствует строка состояния, разделенная на несколько панелей. О В оставшейся части главного окна (клиентской области) программа может выводить все, что считает нужным. В дальнейшем эта область называется «холстом» (canvas). Программа, реализующая эти требования, функционально эквивалентна базовойлрограмме, сгенерированной мастером приложений MFC при выборе однодокументного интерфейса (SDI), отключении архитектуры «документ/представление» и без поддержки баз данных и элементов ActiveX. Чтобы библиотека была действительно универсальной, в нее включаются классы C++, содержащие виртуальные функции. Все классы C++ в этой книге начинаются с префикса «К»; это позволяет использовать их совместно с классами MFC, обычно начинающихся с буквы «С».
Класс панели инструментов Панели инструментов реализуются следующим классом: class KToolbar HWND .UINT
m_hToolTi p: m Control ID;
HINSTANCE m_Res!nstance: UINT m_Res!d; public: HWND
m_hWnd:
KToolbarO m_hWnd = NULL: m_hToolTip = NULL; m_ControlID = 0; m_Res!nstance = NULL; m Resld = 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
размеры окна панели инструментов в соответствии с новой шириной клиентской области родительского окна.
Класс строки состояния Окно строки состояния тоже устроено очень просто. Объявление класса KStatusWindow выглядит следующим образом: typedef enum { pane_l. pane_2. pane_3 class KStatusWindow public: HWND mJiWnd: UINT m_ControlID: KStatusWindowO mJiWnd = NULL: m Control ID = 0:
void void void void
Create(HWND hParent. UINT nControlID); Resize(HWND hParent. int width, int height); SetText(int pane. HINSTANCE hlnst, int messid. int param=0); SetText(int pane. LPCTSTR message):
}: Метод Create создает окно строки состояния как дочернее по отношению к основному окну. Метод Resize изменяет ширину окна в соответствии с шириной клиентской части родительского окна и делит строку состояния на три панели. Два метода SetText предназначены для вывода сообщений в строке состояния.
Класс холста Класс KCanvas описывает окно холста, в котором происходит весь основной вывод приложения. Класс KCanvas создается как производный от класса XWindow, описанного в главе 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 IParam):
KStatusWindow * m_pStatus; KCanvasO: 7oid SetStatusCHINSTANCE hlnst. KStatusWindow * pStatus) Ч mjilnst = hlnst: m_pStatus = pStatus: } virtual -KCanvasO: }: Виртуальный метод WndProc обрабатывает все сообщения, отправленные окну холста. В реализации по умолчанию обрабатываются сообщения WM_CREATE и WM_PAINT. В ходе обработки сообщения WM_PAINT вызываются функции 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 KCanvas KStatusWindow
* m_pToolbar: * m_pCanvas: * m_pStatus:
const TBBUTTON * m_pButtons; int mjiButtons:
int int
mjiToolbarHeight: mjnStatusHeight:
virtual LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam):
325
Пример: родовой класс рамочного окна
virtual LRESULT OnCreate(void): virtual LRESULT OnSize(int width, int height): virtual BOOL OnCommand(WPARAM wParam. LPARAM IParam);
public: KFrame(HINSTANCE hlnstance, const TBBUTTON * pButtons, int nCount. KToolbar * pToolbar. KCanvas * pCanvas, KStatusWindow * pStatus): virtual -KFrameO: Виртуальный метод WndProc обеспечивает основную обработку сообщений окна. В процессе обработки сообщения WM_CREATE он вызывает метод OnCreate, в процессе обработки сообщения WM_SIZE — метод OnSize, а в процессе обработки сообщения WM_COMMAND — метод OnCommand. В самом главном рамочном окне рисовать нечего, поскольку его клиентская область полностью перекрывается окнами панели инструментов, холста и строки состояния. Однако конструктор KFrame: :KFrame(...) заслуживает внимания. Мы хотим, чтобы этот класс был по возможности универсальным и подходящим для многократного использования, поэтому экземпляры KToolbar, KCanvas и KStatusWindow создаются вне класса KFrame. Указатели на них передаются конструктору класса KFrarae вместе с определениями кнопок панели инструментов. Обратите внимание: вы можете создать класс, производный от 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_pToolbar->m_hWnd, & rect): mjnToolbar-Height = rect.bottom - rect.top: } else
mjiTool bar-Height = 0: // Окно строки состояния находится // в нижней части клиентской области
»
326
Глава 5. Абстракция графического устройства
if ( m_pStatus ) { m_pStatus->Create(m_hWnd, ID_STATUSWINDOW); GetWindowRect(m_pStatuS-<@062>m_hWnd. & rect):
const TBBUTTON tbButtons[] = { { STD_FILENEW, IDM_FILE_NEW. TBSTATEJNABLED. TBSTYLE_BUTTON. { 0. 0 }. IDSJILENEW. 0 }. { STD_HELP.
m_nStatusHeight = rect.bottom - rect.top;
} eTse * m_nStatusHeight - 0;
// Создать окно холста, расположенное над окном строки состояния
IDM_APP_ABOUT,
TBSTYLE_BUTTON,
TBSTATEJNABLED.
{ 0. 0 }. IDS__HELPABOUT, 0 }
int WINAPI WinMairKBINSTANCE nlnst, HINSTANCE, LPSTR IpCmd. int nShow) { «Toolbar KCanvas KStatusWindow
if ( m_pCanvas ) { GetClientRect(m_hWnd. & rect);
KMyFrame
m_pCanvas->SetStatus(m_h!nst. m_pStatus);
toolbar; canvas; status:
frame(hlnst. tbButtons. 2, & toolbar, & canvas. & status);
frame.CreateEx(0. _T("ClassName"), _T("Program Name"), WS_OVERLAPPEDWINDOW, CWJJSEDEFAULT. CWJSEDEFAULT. CWJJSEDEFAULT. CWJSEDEFAULT, NULL. LoadMenu(hInst. MAKEINTRESOURCE(IDR_MAIN)). hlnst);
m_pCanvas->CreateEx(0, _T("Canvas Class"). NULL. WSJ/ISIBLE | WS_CHILD. 0, mjiToolbarHeight, rect.right, rect.bottom - m nToolbarHeight - m_nStatusHeight. m hWnd. NULL, nfhlnst);
frame.ShowWi ndow(nShow); frame.UpdateWi ndowt);
return 0; Программа проверяет указатели на все объекты дочерних окон и вызывает методы их создания лишь в том случае, если указатель проходит проверку. В результате ни одно из дочерних окон не является строго обязательным — программа работает и без них. Система управления окнами ОС следит за тем, чтобы панель инструментов занимала верхнюю часть клиентской области, а строка состояния находилась внизу. Вызов CreateEx для окна холста учитывает это обстоятельство и производит соответствующую регулировку позиции и высоты холста. Стандартная реализация KFrame: .-OnSize обеспечивает правильную позицию и размеры трех дочерних окон при изменении размеров главного окна. Первичная обработка команд меню осуществляется методом OnCommand. По умолчанию полученное сообщение передается функции KCanvas:: OnCommand.
327
Пример: родовой класс рамочного окна
frame. MessageLoopO: return 0; } Примерный вид окна нашей программы показан на рис. 5.10.
1
[Create a New Document]
Тестовая программа Главным фактором при оценке классов рамочного окна являются их удобство и универсальность при программировании. Мы рассмотрим лишь самые интересные фрагменты программ, чтобы не повторять одно и то же снова и снова. В приведенной ниже простой тестовой программе используются все четыре класса окон. Программа создает окно со строкой заголовка, панель инструментов с двумя кнопками и подсказками, холст и окно строки состояния. По сравнению с базовой программой MFC, сгенерированной мастером, здесь многого не хватает, в том числе макросов, глобальных переменных, выделения памяти из кучи и обращений к системным DLL.
Рис. 5.10. Пример программы, использующей родовые классы рамочного окна
328
Глава 5. Абстракция графического устройства
Если вы думаете, что структура TBBUTON здесь используется неправильно, вероятно, вы читали неверную документацию. Структура Win32 TBBUTTON состоит из семи полей. В MSDN и прочей документации не упоминается пятое поле: BYTE bReserved[2]. Компилятор C++ прощает неточности до тех пор, пока вы не начнете работать с двумя последними полями, в которых программа хранит идентификаторы строковых ресурсов подсказок.
Пример программы: графический вывод в контексте устройства Графический вывод в среде Windows, как и большинство других процессов, управляется событиями. Предполагается, что приложение всегда должно уметь воспроизвести свое полное изображение, поскольку экран совместно используется несколькими окнами, принадлежащими разным приложениям. Когда возникает необходимость в перерисовке окна, функции окна посылается сообщение WM_PAINT. Оно играет ключевую роль в графическом выводе, выполняемом в программах Windows, однако описать его с концептуальной точки зрения нелегко. Сообщение WM_PAINT автоматически генерируется диспетчером окон, когда окно является видимым, когда в системе нет более срочных сообщений и с окном связан непустой обновляемый регион.
Обновляемый регион окна Обновляемый регион окна определяется несколькими факторами — ограничивающим прямоугольником окна; регионом, заданным функцией SetWindowRgn, и его связью с другими окнами на рабочем столе. Сообщение WM_PAINT не ставится в очередь сообщений программного потока и не обрабатывается наравне с прочими сообщениями. Вместо этого при возникновении необходимости в перерисовке окна устанавливается бит, который заставляет планировщика окон напрямую вызвать обработчик сообщений окна при отсутствии других сообщений в очереди. Существует и другой способ выполнить форсированную перерисовку окна — вызвать функцию UpdateWi ndow. Изначально обновляемый регион окна пуст. Его состояние обновляется при вызове следующих функций: BOOL InvalidateRectCHWND hWnd. CONST RECT * IpRect. BOOL bErase); BOOL ValidateRect(HWND hWnd. CONST RECT * IpRect): BOOL InvalidateRgnCHWND hWnd. HRGN hRgn. BOOL bErase): BOOL ValidateRgnCHWND hWnd. HRGN hRgn): Функции InvalidateRect/InvalidateRgn включают прямоугольник или регион в обновляемый регион окна. Если при вызове передается параметр NULL, в обновляемый регион включается вся клиентская область окна. Функции ValidateRect/ValidateRgn решают обратную задачу: они исключают прямоугольник или регион из обновляемого региона окна. Если при вызове передается параметр NULL, из обновляемого региона исключается вся клиентская область окна. Параметр
Пример программы: графический вывод в контексте устройства
329
bErase сообщает диспетчеру окон, следует ли генерировать сообщение стирания фона WM_ERASEBKGND при вызове BeginPaint. Обновляемый регион окна также изменяется при изменении размеров или прокрутке окна, а также при удалении, перемещении или изменении размеров другого окна, расположенного поверх данного. При изменении размеров окна генерируется сообщение WM_SIZE; диспетчер окон проверяет флаги CS_HREDRAW и CSJ/REDRAW в стиле класса окна (WNDCLASSEX.style), а не в стиле самого окна. Если флаг CS_HREDRAW или CS_VREDRAW установлен, то при изменении ширины или высоты окна вся клиентская область объявляется недействительной; в противном случае недействительной объявляется только добавленная область окна. Любые изменения размеров окна приводят к его немедленной перерисовке. Когда пользователь изменяет окно перетаскиванием рамки, диспетчер окон обычно лишь имитирует изменение размеров окна до того момента, когда будет отпущена кнопка мыши. В новых операционных системах семейства Windows (Windows 95, 98 и 2000) в приложении Display (Экран) панели управления имеется флажок, управляющий этим режимом. Если на вкладке Эффекты (Effects) установлен флажок Show window contents while dragging (Отображать содержимое окна при перетаскивании), сообщение об изменении размеров многократно генерируется в процессе перетаскивания. Если перерисовка окна выполняется медленно, это может привести к серьезным задержкам. Прокрутка окна или связанного с ним контекста устройства также приводит к перерисовке окна. При прокрутке окна или его клиентской области пикселы перемещаются вверх или вниз, налево или направо, и в окне появляются новые, не прорисованные участки содержимого. Такие участки тоже включаются в обновляемый регион окна. Сведения о текущем обновляемом регионе окна возвращаются двумя функциями: int GetUpdateRgnCHWND hWnd. HRGN hRgn. BOOL bErase): BOOL GetUpdateRecUHWND hWnd, LPRECT IpRect, BOOL bErase): Функция GetUpdateRgn возвращает обновляемый регион окна через манипулятор существующего региона hRgn; другими словами, перед вызовом функции манипулятор hRgn должен содержать действительный манипулятор объекта региона, а после вызова функции он содержит данные обновляемого региона окна. Функция GetUpdateRect просто возвращает ограничивающий прямоугольник для обновляемого региона окна. Параметр bErase управляет отправкой сообщения WM_ERASEBKGND в том случае, если обновляемый регион не пуст. *
Сообщение WM_PAINT Когда в функцию окна поступает сообщение WM_PAINT, приложение обычно вызывает функцию BeginPaint. Функция BeginPaint получает контекст устройства и инициализирует системный регион пересечением видимого региона окна с обновляемым регионом. Перед возвратом из BeginPaint обновляемый регион объявляется действительным (то есть сбрасывается), чтобы система могла начать новый цикл накопления данных обновляемого региона.
330
Глава 5. Абстракция графического устройства
Помимо возвращения HDC, функция BeginPaint также заполняет структуру PAINTSTRUCT: typedef struct { HOC hdc: BOOL bErase; RECT rcPaint: - BOOL fRestore: BtlOL flncUpdate: BYTE rgbReserved[32]; } PAINTSTRUCT: Поле hdc содержит тот же манипулятор HDC, который возвращается функцией BeginPaint; значение используется функцией EndPaint для освобождения контекста устройства. Если флаг bErase равен TRUE, приложение должно само стереть фон окна, поскольку все попытки стирания фона завершились неудачей. Если при вызове InvalidateRect или InvalidateRgn был установлен флаг bErase (признак стирания фона), реализация BeginPaint отправляет сообщение WM_ERASEBKGND функции окна, которая должна либо обработать сообщение, либо передать его функции DefWindowProc. Последняя использует для стирания фона манипулятор фоновой кисти, указанный в поле WNDCLASSEX. hbrBackground. Но если кисть не задана, считается, что стереть фон не удалось и эта задача должна быть решена самим приложением. . Поле rcPaint содержит ограничивающий прямоугольник текущего системного региона контекста (то есть региона, нуждающегося в перерисовке). Существует несколько вариантов обработки сообщения WM_PAINT после вызова BeginPaint. Если вы пишете хоть сколько-нибудь нетривиальную программу, подумайте над тем, как оптимизировать обработку WM_PAINT. О В простейшем варианте функция окна выводит в окне все, что заблагорассудится, и перекладывает все хлопоты с отсечением на GDI. Если перерисовка связана со сложными вычислениями и большим количеством графических вызовов, могут возникнуть серьезные проблемы с быстродействием. О Нормальная реализация должна сама проверить прямоугольник rcPaint и перерисовать только те объекты, которые с ним пересекаются. При перерисовке небольших фрагментов изображения это приведет к существенному повышению быстродействия — особенно в ситуации, когда при перетаскивании рамки окна открываются новые участки. О Более изощренная реализация может напрямую работать с системным регионом. Поле rcPaint содержит ограничивающий прямоугольник системного региона, причем последний вовсе не обязан иметь прямоугольную форму. Системный регион может быть значительно меньше области, накрываемой прямоугольником rcPaint. Непосредственная прорисовка на уровне системного региона повышает быстродействие графического вывода. О Если вывод занимает много времени, стоит рассмотреть методику постепенного обновления окна. Например, на загрузку большого растрового изображения в web-браузере может потребоваться очень много времени. Обработчик сообщения WM_PAINT должен быстро отобразить информацию, имеющуюся на
Пример программы: графический вывод в контексте устройства
331
локальном компьютере и вернуть управление с последующим обновлением окна при поступлении новых данных. В промежутках пользователь может прокрутить окно, ознакомиться с отображаемой информацией и даже завершить просмотр. Системный регион контекста устройства в течение долгого времени оставался скрытым от программистов. В новых версиях заголовочных файлов Windows документируется функция GetRandomRgn, позволяющая получить информацию о системном регионе. Хотя эта функция давно экспортируется из GDI32.DLL, раньше она считалась недокументированной. int GetRandomRgn(HOC hDC, HRGN hrgn, INT i N u m ) : Единственным документированным значением параметра INum является значение SYSRGN, однако при вызове можно передать и другие недокументированные индексы для получения других регионов, связанных с DC (эта тема рассматривается в главе 7). Функция GetRandomRgn (hDC, hRgn, SYSRGN) копирует данные системного региона контекста устройства в данные региона, определяемого манипулятором hRgn; перед вызовом функции этот манипулятор должен соответствовать действительному объекту региона. Полученный регион раскладывается на прямоугольники функцией GetRegionData. Если все сказанное звучит слишком запутанно, не ломайте голову — весь процесс подробно рассматривается в главе 6. Перед возвращением из обработчика WM_PAINT функция окна должна вызвать функцию EndPaint, которая при необходимости освобождает ресурсы, связанные с контекстом устройства, или возвращает общий контекст в кэш.
Наглядное представление сообщений перерисовки окна В нормальной реализации WM_PAINT обновляемый регион перерисовывается так, чтобы новое изображение идеально стыковалось с изображением, присутствующим на экране. Но нам как программистам хочется получить наглядное представление о сообщениях WM_PAINT — увидеть, когда они генерируются, какая часть изображения входит в системный регион и узнать, используется ли манипулятор контекста устройства многократно или каждый раз создается заново. Кроме того, нам хотелось бы понаблюдать за генерацией и обработкой других сообщений, связанных с перерисовкой (таких, как UMJCCALCSIZE, 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
Продолжение
332
Глава 5. Абстракция графического устройства
Листинг 5.1. Продолжение #include finclude #include finclude finclude finclude
" \include\win.h" " \include\Canvas. TI" " \include\Status.h" \1nc1ude\FrameWnd.h" " \include\LogWindow.h" "
#include 'Resource.h" class KMyCarwas : public KCanvas
{
virtual void OnOraw(HDC hDC. const RECT * rcPaint): virtual LRESULT WndProc(HWND hWnd. UINT uMsg, WPARAM wParam. LPARAM IParam); int m_nRepaint; int ra_Red. m_Green. m_Blue; HRGN mJiRegion: KLogWindow m_Log; DWORD m_Redraw;
public: BOOL OnCommandtWPARAM wParam, LPARAM IParam);
KMyCanvasO {
mjiRepaint = 0; mJiRegion - CreateRectRgn(0. 0, 1. 1);
m_Red m_Green m_Blue m Redraw
Ox4F: Ox8F; OxCF;
BOOL KMyCanvas::OnCommand(WPARAM wParam. LPARAM IParam) {
switch ( LOWORD(wParara) ) {
case IDM_VIEW_HREDRAW: case IDM_VIEW_VREDRAW: { HMENU hMenu = GetMenu(GetParent(m_hWnd)); MENUITEMINFO mil; memset(&mii. 0. sizeof(mii)); mii.cbSize - sizeof(mii): mii.fMask = MIIM_STATE; if ( GetMenuStatethMenu. LOWORD(wParam).
333
Пример программы: графический вывод в контексте устройства
MF_BYCOMMAND) & MF_CHECKED ) mii.fState = MFJJNCHECKED; else mii.fState = MF_CHECKED: SetMenuItemInfo(hMenu. LOWORD(wParam). FALSE.
mil);
if ( LOWORD(wParam)==IDM_VIEW_HREDRAW m_Redraw *= 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 IParam) { 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 WM_NCCALCSIZE: m_Log.Log("WMJCCALCSIZE\r\n"): Ir = DefWindowProc(hWnd. uMsg, wParam, IParam); m_Log.Log("WM_NCCALCSIZE returns U\r\n". lr); if ( wParam ) { Ir &- - (WVR_HREDRAW | WVRJREDRAW): lr |= m_Redraw; } break; case WM_NCPAINT: m_Log.Log("WM_NCPAINT HRGN *0x\r\n". (HRGN) wParam); lr = DefWindowProc(hWnd. uMsg. wParam. IParam): m_Log.Log("WN_NCPAINT returns\r\n"); break; case WMJRASEBKGND: m_Log.LogC"WM_ERASEBKGND HDC ^Ox\r\n". (HDC) wParam): lr - DefWindowProcthWnd. 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 Id, width Id. height ld\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\n"); m_Log.Log("Begi nPai nt\r\n"); HOC hDC = BeginPaint(m_hWnd. &ps); m_Log.Log("BeginPaint returns HOC Ш\г\п", hDC); OnDraw(hDC, &ps.rcPaint); m_Log.Log("EndPa i nt\r\n"): EndPaint(m_hWnd. &ps); m_Log.Log("EndPaint returns " . "GetObjectType(l08x)-ld\r\n" hDC. GetObjectType(hDO): m_Log.Log("WM_PAINT returns\r\n"); } return 0;
default; Ir = DefWindowProc(hWnd. uMsg. wParam. IParam); } '• return Ir;
void KMyCanvas;:OnDraw(HDC hDC, const RECT * rcPaint) { RECT rect;
GetClientRect(m_hWnd, & rect); GetRandomRgnthDC. mJiRegion. SYSRGN): POINT Origin; GetDCOrgExChDC. & Origin);
Пример программы: графический вывод в контексте устройства
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;
SetTextAlignthDC, 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: irdh.nCount; i++) {
int x = (pRect[i].left + pRect[i].right)/2; int у = (pRect[i].top + pRect[i].bottom)/2:
wsprintf(mess. "WM_PAINT Id. rect Id". mjiRepaint. i+1): ::TextOut(hDC. x. у - lineheight. mess. _tcslen(mess)); wsprintf(mess. "(Id, Id, Id. Id)". pRect[i].left, pRect[i].top. pRect[i].right. pRect[i].bottom); ::TextOut(hDC. x. y. mess. _tcslen(mess));
delete [] (char *) pRegion;
if ( ((unsigned) hDC) & OxFFFFOOOO ) OffsetRgn(m_hRegion. - Origin.x. - Origin.y);
wsprintf(mess. _T("WM_PAINT message Id. Id rects in sysrgn"). mjiRepaint. rectcount); if ( m_pStatus ) m_pStatus->SetText(pane_2. mess):
mjiRepaint ++;
HBRUSH hBrush = CreateSo1idBrush(RGB(m_Red. m_Green, m_Blue));
TCHAR mess[64]:
FrameRgn(hDC. m_hRegion. hBrush. 4, 4): FrameRgnthDC. m_hRegion. (HBRUSH) GetStockObject(WHITE_BRUSH). 1. 1);
wsprintftmess. _T("HDC Ox£X. OrgUd. Id)"). hDC. Origin.x. Origin.y);
335
Продолжение
336
Глава 5. Абстракция графического устройства
Листинг 5.1. Продолжение DeleteObject(hBrush):
int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE. LPSTR. int nShow) { KMyCanvas canvas; KStatusWindow status; KFrame frame(hlnst. NULL. 0. NULL, & canvas. & status); frame.CreateEx(0. JC'ClassName"). _T("WinPaint"), WS_OVERLAPPEDWINDOW. CW_USEDEFAULT, CW_USEDEFAULT. CWJJSEDEFAULT. CWJJSEDEFAULT. NULL. LoadMenu(hInst. MAKEINTRESOURCE(IDR_MAIN)). hlnst): frame.ShowWindow(nShow): frame.UpdateWi ndow();
frame. MessageLoopO; return 0:
} Вероятно, вы ждете подробных объяснений. Длинный список включаемых файлов — верный признак того, что мы используем готовые классы. В программе задействованы классы KWindow, KCanvas, KToolbar, KFrame и новый класс KLogWi ndow. Класс KLogWi ndow управляет многострочным временным окном «EDIT», в котором хранится зарегистрированная информация. Эти и другие классы скомпонованы в библиотеку, которая подключается к программе. Класс KMyCanvas создается производным от KCanvas. В нем переопределяется функция окна, а также обработчики командных сообщений и сообщений перерисовки. Новая функция OnCommand обрабатывает две команды меню, переключающие состояние флагов перерисовки при вертикальном и горизонтальном изменении размеров. Выше уже упоминались флаги CSJHREDRAW и CSJ/REDRAW структуры WNDCLASSEX, определяющие необходимость перерисовки клиентской области при изменении размеров окна. Функция KMyCanvas::OnCommand позволяет переключать внутренний флаг m_Redraw, учитываемый при обработке WM_NCCALCSIZE. Новая функция окна обрабатывает ряд сообщений, связанных с прорисовкой окна, - WM_NCCALCSIZE, WM_NCPAINT, WM_NCPAINT, WM_ERASEBKGND, WM_SIZE и, наконец, WM_PAINT. В данном случае обработка сводится к вызову стандартной функции окна DefWi ndowProc (исключение составляет сообщение WM_PAINT, обрабатываемое методом OnDraw). Однако программа не ограничивается простой передачей управления, а еще регистрирует данные до и после обработки сообщения. При обработке WM_PAINT сохраненные данные передаются до и после вызовов BeginPaint и EndPaint. При обработке сообщения WM_NCCALCS1ZE окну предоставляется возможность вычислить размер клиентской области. Его обработка имеет один полезный аспект — когда параметр wParam равен TRUE, функция окна должна возвращать WVR_HREDRAW и/или WVRJ/REDRAW, если изменение размеров окна приводит к перерисовке всей клиентской области. Таким образом, это сообщение фактически свя-
Пример программы: графический вывод в контексте устройства
337
зывает флаги CSJ/REDRAW и CS_HREDRAW с диспетчером окон. Программа модифицирует результат, полученный от DefWi ndowProc, с учетом режима, выбранного пользователем в меню программы. Таким образом, за последствиями установки этих флагов можно понаблюдать без перекомпиляции тестовой программы. Функция KMyCanvas: :OnDraw написана таким образом, чтобы сообщение WM_PAINT наглядно представлялось в окне программы. Работа функции начинается с получения информации о размерах клиентской области, системного региона и базовой точке контекста устройства. Существует две разные интерпретации системного региона. В Windows NT/2000 системный регион задается в экранной (или физической) системе координат; в Windows 95/98 системный регион задается в клиентской системе координат. Программа проверяет, работает ли она в Windows NT/2000, и если проверка дает положительный результат — переходит к клиентским координатам при помощи функции OffsetRgn. Поскольку мы знаем, что 32-разрядные манипуляторы GDI используются только в Windows NT/2000, программа определяет версию операционной системы простой проверкой старшего слова манипулятора НОС. Затем программа выводит манипулятор и базовую точку контекста в первой панели строки состояния и вычисляет цвет для вывода системного региона. При обработке каждого сообщения WM_PAINT программа изменяет одну из цветовых составляющих (красную, зеленую или синюю). После этого все готово к анализу региона, который может быть пустым, состоять из одного прямоугольника или из сотен прямоугольников. Программа дважды вызывает функцию GetRegionData. В первый раз функция вызывается для получения размера данных региона, а во второй — для получения самих данных. И снова не стоит подолгу вникать в смысл происходящего; подробности будут приведены в главе 6. Для каждого прямоугольника программа выводит номер и координаты центра. После обработки всех прямоугольников программа выводит номер сообщения WM_PAINT и количество прямоугольников во второй панели строки состояния. Наконец, контур системного региона обводится белой рамкой толщиной один пиксел и цветной рамкой толщиной три пиксела. Теперь запустите программу и поэкспериментируйте с ней. Вы поймете, как генерируются сообщения WM_PAINT и какую область они занимают. На рис. 5.11 показано, как выглядит программа при поочередном изменении размеров окна по обеим осям. Первое сообщение WM_PAINT перерисовывает окно стандартных размеров. Затем мы уменьшаем размер окна; при этом генерируется второе сообщение WM_PAINT, системный регион которого не содержит ни одного прямоугольника. Затем окно сворачивается и восстанавливается, в результате чего генерируется третье сообщение для перерисовки уменьшенной клиентской области (первый прямоугольник на рис. 5.11). При изменении размеров окна в одном направлении генерируются сообщения WM_PAINT с системным регионом, состоящим из одного прямоугольника. Но при одновременном масштабировании окна в обоих направлениях генерируется сообщение WM_PAINT с системным регионом из двух прямоугольников (прямоугольники 1 и 2 для сообщения WM_PAINT с номером 7). Если открыть и закрыть меню, сообщение WM_PAINT не генерируется, поскольку система сохраняет изображение при выводе меню и автоматически восстанавливает его. Но если
338
Глава 5. Абстракция графического устройства
накрыть окно программы другим окном или перетащить окно за край экрана и вернуть его на место, сообщения перерисовки обязательно появятся. Если установить флаги CS_HREDRAW и CSJ/REDRAW в меню View, при изменении размеров окна перерисовывается вся клиентская область, а не только вновь появившиеся участки. Если в приложении Display (Экран) панели управления установлен флажок Show window contents while dragging (Отображать содержимое окна при перетаскивании), при перетаскивании окна за рамку генерируются частые сообщения перерисовки.
BeginPaint jWM_NCPAINT HRGN 8( |WN_NCPftIHT return!: |WM_ERASEBKGND HOC I JWM_ERftSEBKGND retij IBeginPaint return:! |EndPaint ! lEndPaint returns ( |WM_PAINT returns |WM_HCCALCSIZE |WM_NCCftLCSI2E retl |WM_SI2E type fl, wi |WM_SIZE returns JUM PAINT
/M_PAINT 3, reel fM_PAINT 4, reel1 (0,0,122,70) I (122, 0,238, 70)J
! PAINT 6, rei PAINT 7, 38, 0,328, IZj ,0,399,1
WM_PAINT 5, rect 1 (0,70,238,121) WM PAINT 7, rect 2 (0,124,399,181)
Рис. 5.11. Последовательность отправки сообщений WM_PAINT при изменении размеров окна
В окне, расположенном слева, также выводятся довольно интересные результаты. Ниже приведен протокол изменения размеров одного окна, для наглядности снабженный отступами. WM_NCCALCSIZE WMJCCALCSIZE returns О WM_SIZE type 0. width 581, height 206 WMJIZE returns WM_PAINT BeginPaint WM_NCPAINT HRGN 9e040469 WM_NCPAINT returns WMJRASEBKGND HOC 3b0105ae WMJRASEBKGND returns BeginPaint returns HOC 3b0105ae EndPaint EndPaint returns GetObjectType(3b0105ae)=0 WM_PAINT returns Когда пользователь завершает перетаскивание границы окна в новое положение, генерируется сообщение WM_NCCALCSIZE, за которым следуют сообщения WM_SIZE и WM_PAINT. Во время обработки WM_PAINT функция BeginPaint генерирует сообщение WM_NCPAINT для перерисовки неклиентских областей и сообщение WM_ERASEBKGND для стирания фона. При вызове WM_ERASEBKGND передается манипулятор контекста устройства, возвращенный функцией BeginPaint. Интересно заметить, что в
339
Итоги
Windows NT/2000 после выхода из EndPaint манипулятор контекста устройства, возвращенный BeginPaint, становится недействительным (GetObjectType возвращает 0), но после нескольких повторных вызовов этот манипулятор НОС появляется снова. Это доказывает, что графический механизм поддерживает глобальный кэш манипуляторов контекстов устройств.
Итоги Эта глава посвящена одной из важнейших концепций графического программирования в среде Windows - контекстам устройств. Мы рассмотрели важный класс графических устройств - видеоадаптеры; узнали, как составить список экранных устройств с поддерживаемыми видеорежимами и как получить информацию о возможностях устройств. Кроме того, в этой главе описаны различные типы контекстов устройств и способы их создания. Особое внимание было уделено контекстам устройств, связанным с конкретными окнами. Также было рассмотрено управление графическим выводом в окне с использованием обновляемого региона окна. В конце главы были созданы классы C++, демонстрирующие концепции графического программирования Windows на примере наглядной обработки сообщений WM_PAINT. Однако весь материал этой главы представляет собой лишь общее описание контекстов устройств и их связи с управлением окнами. Применение контекстов устройств при графическом выводе будет подробно рассмотрено в последующих главах.
Примеры программ В отличие от глав 3 и 4 примеры этой главы являются вполне обычными программами Windows. Они демонстрируют некоторые, неочевидные особенности контекстов устройств и их связи с выводом в окне (табл. 5.7). Таблица 5.7. Программы главы 5
_
Каталог проекта
Описание
Samples\Chapt_05\Device
Получение списка экранных устройств, видеорежимов, получение информации о возможностях устройств и атрибутах контекстов
Samples\Chapt_05\Ellipse
Демонстрация возможности создания прямоугольных и непрямоугольных окон
Samples\Chapt_05\FrameWindow
Пример программы для тестирования семейства классов рамочного окна
Samples\Chapt_05\WinPaint
Наглядное представление сообщений перерисовки окна, системного региона, а также флагов CS HREDRAW и CS_VREDRAW
400
Глава 7. Пикселы
if ( m_bValid[2] ) {
// Метарегион
hBrush = CreateHatchBrush(HS_HORIZONTAL. RGBCO. OxFF. 0)): Fi11Rgn(hDC, m_hRandomRgn[2].. hBrush); DeleteObject(hBrush);
401
Отсечение SetMetaRgn(hDC); DeleteObject(hRgn); hRgn = CreateEllipticRgn(0, 0. rect.hght*3/4, rect.bottom); SelectClipRgnthDC. hRgn); break;
} DeleteObject(hRgn); Функция DrawRegions вызывается после возврата из EndPaint. Она использует новый контекст устройства, возвращаемый функцией GetDC, и поэтому может рисовать во всей клиентской области, а не только в системном регионе. При выводе региона отсечения и метарегиона используются разные штриховые кисти. Регион API должен представлять собой пересечение этих двух регионов. Мы знаем, что в только что созданном контексте региона отсечения, метарегиона и региона API быть не должно, поэтому для получения осмысленных результатов необходимо подготовить эксперимент при помощи функции TestClipMeta, приведенной ниже. В программе ClipRegion определяются четыре режима, выбираемые в главном меню. Первый режим не устанавливает региона отсечения и метарегиона, поэтому вы можете увидеть ситуацию по умолчанию. Во втором режиме устанавливается регион отсечения, в третьем — метарегион, а в четвертом — регион отсечения вместе с метарегионом. В качестве региона отсечения используется эллиптический регион, находящийся в левых трех четвертях клиентской области. Метарегион также имеет форму эллипса и находится в верхних трех четвертях клиентской области. void KMyCanvas::TestClipMeta(HOC hOC. const RECT & rect) { HRGN hRgn;
// При установке метарегиона и региона отсечения // вывод происходит только в пересечении системного региона // с метарегионом и регионом отсечения HBRUSH hBrush - CreateSolidBrush(RGB(0, 0. OxFF)): FillRect(hDC. & rect, hBrush); OeleteObject(hBrush): DumpRegions(hDC); } Функция TestClipMeta вызывается главной функцией вывода KMyCanvas: :OnDraw после вывода системного региона. Таким образом, после установки региона отсечения и метарегиона при выводе учитываются все три региона. Программа пытается закрасить всю клиентскую область однородной синей кистью. Если наши выкладки верны, закрашено будет только пересечение системного региона, региона отсечения и метарегиона, а все остальное отсекается. На рис. 7.2 показано, как выглядит окно программы при одновременной установке региона отсечения и метарегиона.
iofxf
ЛЫПШъиЛ
switch ( m_test ) { case IDM_TEST_DEFAULT: break; case IDM_TEST_SETCLIP: hRgn = CreateEllipticRgntO. 0. rect.right*3/4. rect.bottom); SelectClipRgn(hOC. hRgn); DeleteObject(hRgn): break: case IDM_TEST_SETMETA: hRgn = CreateEllipticRgn(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): SelectClipRgn(hDC. hRgn);
Рис. 7.2. Регионы в контексте устройства
402
Глава?. Пикселы
Прямоугольник рамки окна изображает системный регион. Вертикальными линиями закрашивается регион отсечения, а горизонтальными — метарегион. Регион API закрашен сеткой из линий, а область сплошной закраски обозначает пересечение регионов (то есть область вывода). Если провести все четыре опыта и просмотреть содержимое окна выходных данных, можно получить довольно интересные результаты. Пример: V/ IDM_TEST_DEFAULT RandomRgn(l) no region RandomRgn(2) no region RandomRgnO) no region RandomRgn(4) SIMPLEREGION RgnBox-[464. 247, 922. 590) 1 rects // IDM_TEST_SETCLIP RandomRgn(l) COMPLEXREGION RgnBox=[0. 0. 342. 342) 201 rects RandomRgn(2) no region RandomRgnO) COMPLEXREGION 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 RandomRgnO) 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 RandomRgnO) 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-разрядное значение типа COLORREF и для разделения данных COLORREF на составляющие RGB. Эти макросы можно представить в виде следующих подставляемых (inline) функций: COLORREF RGB(BYTE ByRed. BYTE byGreen. BYTE byBlue): BYTE GetRValueCCOLORREF rgb): BYTE GetGValueCCOLORREF rgb): BYTE GetBValueCCOLORREF rgb): Ниже перечислены некоторые удобные определения для часто используемых цветов: const COLORREF black RGB( 0. 0. 0): RGB(Ox80. 0, 0); const COLORREF darkred RGB( 0.0x80. 0): const COLORREF darkgreen const COLORREF darkyellow RGB(0x80.0x80. 0): RGB( 0. 0.0x80): const COLORREF darkblue const COLORREF darkmagenta - RGB(Ox80. 0.0x80):
404
Глава?. Пикселы
const COLORREF darkcyan const COLORREF darkgray
- RGB(
const const const const const
COLORREF COLORREF COLORREF COLORREF COLORREF
moneygreen skyblue cream mediumgray lightgray
= RGB(OxCO.OxDC.OxCO): = RGB(OxA6,OxCA,OxFO): - RGB(OxFF.OxFB.OxFO): = RGB(OxAO,OxAO.OxA4); = RGB(OxCO.OxCO.OxCO):
const const const const const const const
COLORREF COLORREF COLORREF COLORREF COLORREF COLORREF COLORREF
red green yellow blue magenta cyan white
= RGBCOxFF, 0, 0): - RGB( O.OxFF. 0); = RGB(OxFF.OxFF. 0); - RGB( 0, O.OxFF): = RGBCOxFF. O.OxFF); = RGBC O.OxFF.OxFF);
0.0x80,0x80):
- RGB(0x80.0x80.0x80);
- RGB(OxFF.OxFF.OxFF):
В GDI API организовано аппаратно-независимое использование значений в формате RGB. Обычно приложение не занимается преобразованием цветов перед их сохранением в памяти видеоадаптера — эта задача решается драйвером экрана. В некоторых режимах пользовательское приложение управляет содержимым системной палитры для улучшения качества изображения. Системная палитра рассматривается вместе с растрами в одной из последующих глав книги. Для экспериментов с изменением цвета проще всего воспользоваться функцией SetPixel, изменяющей цвет одного пиксела. Функция SetPixel определяется следующим образом: COLORREF SetPixel(hOC HDC. int x. int y. COLORREF crColor); Функция SetPixel выводит на поверхности один пиксел цвета crColor в точке с координатами (х, у), с учетом настройки логического координатного пространства и отсечения. Впрочем, один пиксел — это слишком тривиально, поэтому в следующем примере мы нарисуем цветовой куб RGB (то есть куб, образованный 256 уровнями красной, зеленой и синей составляющих в трехмерном пространстве). Код программы RGBCube приведен в листинге 7.1.
405
Цвет
// Зеленый = 255. правая грань, со сдвигом for (b=0: b<256: b++) for (r=0; r<256; r+=2) SetPixelV(hDC. 255+128-Г/2. b+128-r/2. RGBCr. OxFF. b)): Программа рисует три грани объемного куба, у которых красная, синяя и зеленая составляющие равны 255. Первая грань имеет прямоугольную форму, а две других выводятся со сдвигом для имитации объема. В принципе сдвиг можно было бы выполнить путем мировых преобразований, но эта задача легко решается вручную простым пропусканием половины строк развертки и небольшим смещением выводимых пикселов. В нарисованном цветном кубе видны все вершины, кроме черного начала координат. Небольшой объем вспомогательного кода обеспечивает отображение области просмотра и вывод осей. Окончательный результат показан на рис. 7.3. Синий COLORREF (0,0,255)
.-/—Зеленый COLORREF (0,255,0)
Красный COLORREF (255,0,0) Рис. 7.3. Программа наблюдения за объектами GDI
Листинг 7.1. Вывод трехмерного куба RGB без использования мировых преобразований void RGBCubetHDC hDC)
int г. g. b;
// Нарисовать и пометить оси // Красный = OxFF for (g=0: g<256: g++) for (b=0; b<256: b++) SetPixel(hDC. g. b. RGBCOxFF. g. b)): // Синий = OxFF. верхняя грань, со сдвигом for (g=0: g<256: g++) for (r=0; r<256: r+=2) SetPixeKhDC. g+128-r/2. 255+128-Г/2. RGB(r. g. OxFF)):
К сожалению, на рисунке наш красивый цветной куб окрашен в оттенки серого цвета. Возникает другой вопрос — как цвет, заданный в формате RGB, преобразуется в оттенки серого? Ниже приведены простые формулы. Grayscale = (Red*30 + Green*59 + Blue*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 измеряется в угловых величинах от О до 360 градусов; 0 соответствует красному цвету, 60 — желтому, 120 — зеленому, 180 — голубому, 240 — синему, 300 — малиновому, а 360 — снова красному. Оттенок определяет угловое смещение цвета относительно красного угла куба RGB при взгляде вдоль диагонали от белого угла к черному. Яркость определяет относительную высоту точки в развернутом кубе; 1 соответствует белому цвету, а 0 — черному. Насыщенность определяет расстояние цветовой точки от диагонали, проведенной от белого угла к черному. Функция, приведенная в листинге 7.1, рисовала трехмерный куб RGB без обращения к мировым преобразованиям GDI. В листинге 7.2 показано, как использовать мировые преобразования для отображения каждой из трех граней в соответствующую треть шестиугольника HLS. Помимо всего прочего, этот фрагмент призван проиллюстрировать технику мировых преобразований.
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): // Задать положение центра шестиугольника с небольшими полями SetViewportOrgEx(hDC. (int)r + 40. (int)y + 40. NULL); // Красный = 255 affine.MapTrKO.O. 255,0, 0,255, r . O . x . y . x.-y); SetWorldTransform(hDC, & affine,m_xm); ColorRect(hDC, RGB(OxFF. 0. 0). RGB(0. 1. 0). RGB(0. 0. Ш: // Зеленый = 255 affine.MapTrKO.O. 255.0. 0.255, -x.y. x.y. -r.O): SetWorldTransform(hDC, & affine.m_xm): ColorRect(hDC. RGB(0. OxFF. 0). RGBU. 0. 0). RGB(0. 0. D); // Синий = 255 affine.MapTrKO.O. 255.0, 0.255, -x.-y. - r . O . x . - y ) : SetWorldTransformChOC. & affine.m_xm); ColorRectChDC. RGB(0, 0. OxFF), RGB(0. 1, 0). RGBd. 0. 0 ) ) :
Программа рисует каждую из трех граней как прямоугольник в мировой системе координат. Прямоугольники отображаются на параллелограммы, образующие шестиугольник. Преобразование из мировых координат в страничные выполняется классом K A f f i n e с привязкой по трем точкам. Цветовое пространство HLS изображено на рис. 7.4.
Листинг 7.2. Вывод развернутого куба RGB в виде сверху void ColorRecttHDC hDC. COLORREF cO. COLORREF dx, COLORREF dy) for (int x=0; x<256; x++) for (int y=0: y<256: y++) SetPixeHhOC. x, y. cO + x
dx + у * dy);
MoveToExChDC, 0. 0. NULL): LineToChOC. 0, 255); LineToChDC. 255, 255); MoveToEx(hDC. 0. 0. NULL): LineToChDC. 255. 0); LineTo(hDC, 255. 255): // LineTo(hDC. 0. 0); Рис. 7.4. Куб RGB в направлении от белого угла к черному
408
Глава?. Пикселы
409
Цвет lightness saturation hue
Цветовое пространство HLS образует коническую фигуру, которую иногда изображают при помощи двух шестиугольников. В центре фигуры яркость равна 0,5. В вершине фигуры яркость равна 1 (белый цвет), а в нижней точке — О (черный цвет). Оттенок определяется угловым расстоянием, причем в углах О, 60, 120, 180, 240 и 300 градусов расположены соответственно красный, желтый, зеленый, голубой, синий и малиновый цвета. Насыщенность определяет глубину цвета (от тусклого к интенсивному); точки с максимальной интенсивностью 1 находятся на краях шестиугольника, а точки с нулевой интенсивностью расположены в центре. Яркость 0 соответствует черному, а яркость 1 — белому цвету. В цветовом пространстве HLS оттенок обычно представляется вещественным числом из интервала [0..360], а яркость и насыщенность лежат в интервале [0..1]. В листинге 7.3 приведен класс C++ для преобразования цветов между моделями RGB и HLS.
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:
Листинг 7.3. Класс KColor: преобразование между RGB и HLS class KColor {
typedef enum { Red. Green. Blue };
if (hue >= 360) hue = hue - 360;
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 ) { rax = blue: major = Blue:
if ( mn==mx )
= mn/255: =• 0; =0;
unsigned char Value(double ml. double m2. double h)
{
if (h >= 360) h — 360; else if (h < h +- 360:
0)
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
Цвет
4Ц
c.hue = hue: c.lightness = 0.5 : c.saturation = ((double) sat)/scale: c.ToRGBO: SetPixeHhDC. 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++) SetPixeHhDC. scale+20+x. 1. RGB(c.red. c.green, c.blue)); Первая часть этой функции демонстрирует преобразование цвета из модели HLS в RGB и вывод на экран. В трехмерной модели имитируется разная освещенность объектов, при этом яркость изменяется в зависимости от расстояния и угла между объектом и источником света. Вторая часть функции показывает, как решить эту задачу в модели HLS изменением яркости при постоянном оттенке и насыщенности. Эта методика очень полезна при создании градиентных заливок, когда простого смешения цветов RGB оказывается недостаточно.
Индексируемые цвета и палитры
Рис. 7.5. Цветовая палитра модели HLS Листинг 7.4. Вывод цветовой палитры HLS void HLSColorPalette(HDC hDC. int scale. KColor & selection) KColor c:
for (int hue=0; hue<360; hue++) for (int sat«0: sat<-scale: sat++)
Помимо задания цветов в модели RGB, в Win32 API также поддерживается возможность их задания в виде индексов палитры — цветовой таблицы, содержащей значения RGB. У каждого контекста устройства имеется такой атрибут, как логическая палитра. Логические палитры принадлежат к числу объектов GDI, и для ссылок на них используются манипуляторы типа HPALETTE. Ниже перечислены основные функции для работы с палитрами. HPALETTE CreateHalftonePalette(HDC hDC); COLORREF GetNearestCo1or(HDC hDC. CQLORREF crColor); UINT GetNearestPaletteIndex(HPALETTE hpal. COLORREF crColor): UINT GetPaletteEntries(HPALETTE hPal . UINT iStartlndex. UINT nEntries. LPPALETTEENTRY Ippe); UINT RealizePalettetHDC hDC); HPALETTE SelectPalette(HDC hDC. HPALETTE hPal . BOOL bForceBackground): Функция CreateHalftonePalette создает палитру из 256 цветов, равномерно распределенных в кубе RGB. Такая палитра позволяет отображать многоцветную графику полутоновыми методами в видеорежимах, использующих палитру. Функция GetNearestColor ищет в текущей логической палитре контекста устрой-
412
Глава 7. Пикселы
Цвет PALETTEENTRY entry[256];
ства цвет, ближайший к заданному. Функция GetNearestPalettelndex возвращает индекс в палитре цвета, ближайшего к заданному. Функция GetPaletteEntries загружает из логической палитры определения элементов из заданного интервала. Функция RealizePalette готовит системную палитру к отображению цветов из логической палитры. Функция SelectPalette присоединяет логическую палитру к контексту устройства и возвращает исходную логическую палитру. Манипулятор текущей палитры также можно получить функцией GetCurrentObject(hDC OBJ_PAL). Для работы с палитрой существуют следующие макросы: COLORREF PALETTEINDEX(WORD wPalettelndex): COLORREF PALETTERGB(BYTE bRed, BYTE bGreen. BYTE bBlue): Макрос PALETTEINDEX получает индекс и возвращает 32-разрядное описание элемента палитры. Когда такое описание используется в контексте устройства, оно интерпретируется как элемент логической палитры контекста. Макросу PALETTERGB, как и макросу RGB, передаются значения красной, зеленой и синей составляющих. При использовании этих макросов в контексте устройства без системной (аппаратной) палитры их возвращаемые значения интерпретируются одинаково. Но если макрос PALETTERGB используется в контексте с системной палитрой, входящие в него значения составляющих RGB позволяют найти ближайшее совпадение в логической палитре устройства, словно приложение указало индекс в палитре. Реализация PALETTERGB в виде функции может выглядеть так: COLORREF PALETTERGBtHDC hDC. BYTE bRed, BYTE bGreen. BYTE bBlue); COLORREF rslt = RGBtbRed, bGreen, bBlue):
413
int nun = GetPaletteEntries(hPalette. 0. 256. entry): HPALETTE hOld = SelectPalette(hDC, hPalette, FALSE): if ( GetDeviceCapsdiDC. RASTERCAPS) & RC_PALETTE ) RealizePalette(hDC): for for for for
(int j=0; j<(num+15)/16; j++) (int i=0: i<16; i++) (int y=0: y<24: y++) (int x=0; x<24: x++) SetPixeKhDC. i*25+x. j*25+y, PALETTEINDEX(j*16+i)):
SelectPalette(hDC, hOld. FALSE); if ( bHalftone ) DeleteObject(hPalette);
}
Эта программа работает как для текущей логической палитры, так и для полутоновой палитры. Функция GetPaletteEntries возвращает количество цветов в логической палитре; по ее возвращаемым значениям также можно вывести значения составляющих RGB каждого цвета. Затем программа реализует палитру, если это необходимо, и отображает ее содержимое рядами из 16 цветов при помощи макроса PALETTEINDEX. На рис. 7.6 показано расположение 256 цветов полутоновой палитры.
if ( GetDeviceCapsdiDC. RASTERCAPS) & RC_PALETTE) HPALETTE hPal = GetCurrentObject(hDC. OBJ_PAL): int indx = GetNearestPaletteIndex(hPal. 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) GetCurrentObjectthDC. OBJ_PAL): if ( bHalftone ) hPalette - CreateHalftonePalette(hDC);
Рис. 7.6. Полутоновая палитра, выведенная с использованием макроса PALETTEINDEX
Палитра, создаваемая по умолчанию в контексте устройства, содержит всего 20 цветов — 16 цветов старого видеоадаптера VGA и еще 4 цвета, определяю-
414
Глава?. Пикселы
щих текущую цветовую схему Windows. Конечно, этого недостаточно даже для изображения среднего качества. Полутоновая палитра состоит из 256 цветов, равномерно распределенных в кубе RGB. Но если приложение захочет изобразить закат солнца, вероятно, ему понадобится больше теплых цветов, а холодные цвета окажутся лишними. Win32 содержит функции, позволяющие приложениям определять собственные палитры и управлять их взаимодействием с системной палитрой и другими приложениями системы, конкурирующими за общий ресурс системной палитры. Управление палитрой рассматривается после обсуждения обработки растров в GDI. Вероятно, область применения макросов RGB и PALETTEINDEX вам уже ясна. Макрос RGB лучше всего подходит для режимов High Color и True Color. Макрос PALETTEINDEX предназначен для режимов, требующих палитру; впрочем, он работает в режимах High Color и True Color при условии, что в контексте используется действительная логическая палитра. А что получится, если испытать макрос RGB в режиме с палитрой? Ничего хорошего. Слева на рис. 7.7 наш красивый RGB-куб изображен в 256-цветном режиме, причем результат не зависит от выбора в контексте устройства полутоновой палитры.
415
Вывод пикселов
выводится независимо от других, без полутоновой обработки всей поверхности. Чтобы улучшить результат без написания собственного алгоритма полутонирования, следует сохранить пикселы в 24-разрядном растре и воспользоваться командами вывода растров с полутоновой поддержкой.
Нетривиальные
возможности
Даже после чтения всего десятка страниц можно уверенно сказать, что работа с цветом — непростая тема. В Win32 API предусмотрены дополнительные средства создания логических палитр и операции с системной палитрой, которые будут рассматриваться ниже в этой книге. Microsoft также предоставляет в ваше распоряжение специальный интерфейс API управления цветом — ICM 2.0, но эта тема выходит за рамки GDI. Полное описание возможностей ICM 2.0 в книге не приводится, хотя отображение и регулировка цветов будут дополнительно рассматриваться при подробном описании палитр в главе 13. Windows GDI поддерживает две разновидности цветовых пространств: цветовое пространство RGB и пространство индексов палитры, соответствующее базовым возможностям видеоадаптера. Поддерживаемый современными видеоадаптерами альфа-канал не является самостоятельным компонентом цветового пространства GDI. Он поддерживается только в DirectX и в новой функции GDI AlphaBlending. На рис. 7.8 показано, как 32-разрядная величина COLORREF представляется в форматах RGB, PALETTERGB и PALETTEINDEX. В GDI эти форматы различаются по первому байту 32-разрядного числа. Red
0
Green
Blue
RGB(Red, Green, Blue)
1
0
index PALETTEINDEX(index)
Рис. 7.7. Цветовой куб RGB, выведенный в 256-цветном режиме с полутоновой палитрой (слева использован макрос RGB, справа — макрос PALETTERGB)
Весьма своеобразный трехмерный объект, не правда ли? Однако это совсем не то, что требовалось. Для вывода куба, который на самом деле состоит из 3 х 256 х 256 разных цветов, используется всего 9 цветов! Чтобы улучшить качество изображения, необходимо создать, выбрать и реализовать в контексте устройства полутоновую палитру (см. листинг 7.5). Есть и другой, не менее важный аспект — заменить макрос RGB макросом PALETTERGB. В правой части рис. 7.7 видны волшебные последствия такой замены. Из рисунка видно, что куб, выведенный при помощи макроса PALETTERGB, не обладает идеальной симметрией. Это объясняется неравномерностью распределения цветов полутоновой палитры. Но даже улучшенный вариант по сравнению с версией для режима True Color смотрится весьма уродливо. Дело в том, что при выводе куба каждый пиксел
Red
2
Green
Blue
PALETTERGB(Red, Green, Blue)
_L 32 бита
24
J
I
|
16
8
О
Рис. 7.8. Три способа задания цветов в GDI
Вывод пикселов Функции вывода пикселов Win32 неоднократно встречались в этом разделе. После изложения теоретических обоснований пришло время для более точного и формального описания этих функций. В Win32 API предусмотрены следующие функции для работы с пикселами:
416
Глава 7. Пикселы
COLORREF GetPixel (HOC hDC. int X. int Y ) ; COLORREF SetPixelV(HDC hDC. int X. int Y. COLORREF crColor): COLORREF SetPixel (HDC hDC. int X. int Y. COLORREF crColor);
В параметре hDC передается манипулятор контекста устройства. Прежде всего следует помнить, что не все устройства поддерживают работу с пикселами, а выражаясь точнее — не все драйверы устройств поддерживают непосредственные операции с пикселами. На уровне DDI не существует функции, которая бы обеспечивала вывод отдельных пикселов. Вместо .этого GDI преобразует команды вывода пикселов в команды DDI, выполняющие блиттинг растров. Следовательно, в сомнительных случаях приложение должно проверить значение GetDeviceCaps(hDC, RASTERCAPS)&RC_BITBLT и узнать, поддерживается ли устройством блитгинг растров, частным случаем которого является вывод отдельного пиксела. Параметры (X,Y) определяют позицию пиксела в логической системе координат — мировой для расширенного графического режима или страничной для совместимого графического режима. Перед определением окончательной позиции пиксела координаты проходят мировое преобразование, отображение окна в область просмотра и отображение координат устройства в физические координаты. Непосредственный вывод пиксела также зависит от того, входит ли пиксел в фактическую область отсечения контекста устройства, также называемую регионом Рао. Границы региона Рао определяются пересечением системного региона, метарегиона и региона отсечения данного контекста. Если точка находится за пределами фактической области отсечения, возвращается код ошибки (CLR_INVALID, то есть OxFFFFFFFF, или FALSE для SetPixel V). Параметр 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, а не в формате PALETTEINDEX или PALETTERGB, даже в контекстах устройств с поддержкой палитры. Это может вызвать проблемы в контекстах с палитрой. Например, приведенная ниже функция копирует пикселы в новую позицию. Если применить ее к кубу в правой части рис. 7.7, результат будет напоминать левый куб за исключением того, что в этом случае используется только 20 цветов. void CopyPixeKHDC hDC. int xO. int yO. int xl. int yl) { SetPixeKhDC. xl, yl. GetPixeKhDC. xl. yl)): }
Чтобы эта функция нормально работала, последний параметр SetPixel должен иметь вид GetPixeKhDC, xl, yl)|PALETTERGB(0,0,0) или GetPixeKhDC, xl, yl) 0x02000000. Разобраться в том, как реализованы функции вывода пикселов, особенно интересно, поскольку речь идет о самой простой графической операции. Кроме того, это поможет вам лучше понять, с какими затратами связано выполнение некоторых графических команд в GDI. В Windows NT/2000 обе функции, SetPixel и SetPixelV, обслуживаются одной и той же системной функцией _NtGdiSetPixel@16, вызываемой после проверки параметров по манипулятору контекста устройства. Имя _NtGdiSetPixel016 означает, что при вызове функции передаются четыре параметра. При этом инициируется программное прерывание, которое обрабатывается одноименной функцией механизма GDI режима ядра (win32k.sys). Функция ядра NtGdiSetPixel блокирует контекст устройства, отображает логические координаты в физические, выполняет простую проверку границ, при необходимости преобразует COLORREF в индекс и вызывает функцию драйвера DrvBitBlt (если она поддерживается) или функцию EngBitBlt. При необходимости цветовое значение транслируется в формат RGB. Перед возвращением из функции контекст устройства разблокируется. Функция GetPixel обрабатывается системной функцией _NtGdiGetPixel012. Эта функция создает временный растр и копирует в него пиксел вызовом DrvCopyBits/ EngCopyBits. Главное, на что необходимо обратить внимание в этой процедуре — это быстродействие. Для каждого вызова GetPixel, SetPixel или SetPixelV графическая система должна инициировать программное прерывание, переключиться в режим ядра и обратно, отобразить координаты, преобразовать индекс палитры, построить временный растр и затем вызвать функцию блиттинга драйвера устройства. Для одного пиксела объем работы получается довольно большим. В главе 1 была описана методика хронометража некоторых операций с использованием специальной инструкции процессора Pentium. Ею можно воспользоваться и для измерения быстродействия функций работы с пикселами. Некоторые результаты представлены в табл. 7.2.
418
Глава?. Пикселы
Таблица 7.2. Быстродействие функций работы с пикселами: Pentium 200 МГц Функция
Такты (256 цветов)
Такты (32-разрядный цвет)
Наивысшая скорость (пикселов/с)
SetPixeKRGBQ)
1850
1286
152881
SetPlxel(PALETTERGBO)
4897
1284
153 374
SetPixel(PALETTEINDEX)
1345
1295
153115
SetPixeWRGBO)
1880
1284
153115
GetPixelO
6362
6499
30251
Хронометраж показывает довольно интересные результаты. Во-первых, 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 для неопределенных точек. Число итераций преобразуется в COLORREF по цветовым таблицам, после чего пиксел выводится функцией SetPixel. При рассмотрении других графических примитивов GDI будет показано, как повысить скорость вывода за счет нетривиальных графических команд. Пример программы Mandelbrot наглядно покажет, насколько хорошо вы разобрались в API режимах отображения, прокрутки, отсечения, цветовых пространств и вывода пикселов.
420
Глава?. Пикселы
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
Мониторинг таблицы объектов и получение информации об использовании объектов GDI процессом, позволяющей своевременно узнать о возможных утечках ресурсов
Samples\Chapt_07\ClipRegion
Наглядное представление системного региона, метарегпона и региона отсечения
Samples\Chapt_07\ColorSpace
Демонстрация цветовых пространств RGB и HLS и цветов полутоновой палитры
Samples\Chapt_07\PixelSpeed
Хронометраж функций вывода пикселов
Samples\Chapt_07\Mandelbrot
Графическое представление множества Мандельброта с использованием функций вывода пикселов
Рис. 7.9. Полное множество Мандельброта (128 итераций)
Рис. 7.10. Подмножество Мандельброта в увеличении 64:1 (1024 итерации)
423
Бинарные растровые операции
ми растровыми операциями (binary raster operations), или сокращенно ROP2. В табл. 8.1 приведен перечень бинарных растровых операций, поддерживаемых в GDI. В данном случае буквой Р обозначается цвет пера, поскольку операции ROP2 используются для рисования линий. Таблица 8.1. Бинарные растровые операции
Глава 8 Линии и кривые При соединении отдельных пикселов возникают линии и кривые. Следовательно, если вы умеете рисовать отдельные пикселы, рисование линий и кривых превращается в стандартную задачу из области математики и программирования. Впрочем, на практике линии и кривые могут обладать разными цветами, стилями, шириной, узором и прочими атрибутами, поэтому процедура рисования кривых бывает весьма нетривиальной. В этой главе рассматриваются некоторые концепции и средства GDI, связанные с рисованием линий и кривых, — бинарные растровые операции, режимы заполнения фона, перья, линии, кривые и траектории.
Бинарные растровые операции При вызове функции SetPixel для вывода пиксела на поверхности устройства описатель цвета преобразуется в цветовой формат (физический цвет), соответствующий формату кадрового буфера, а затем значение соответствующего пиксела приемника заменяется преобразованным цветом. Если интерпретировать кадровый буфер как массив пикселов D, то операцию SetPixel можно рассматривать как присваивание D[x,y] = Р, где Р — преобразованный цвет (или физический цвет). Обобщая эту операцию, можно определить функцию /, объединяющую исходный цвет пиксела D[x,y] с цветом Р и порождающую новое цветовое значение, которое присваивается соответствующему пикселу приемника. Другими словами, D[x,y] =/(D[x,y],P) или D = /(D,P). Функция / получает два параметра, то есть является бинарной функцией. Теоретически число таких функций бесконечно, однако в GDI поддерживается лишь одна их разновидность: поразрядные логические операции. В этих операциях к битам двух аргументов, находящихся в одинаковых позициях, применяются логические операции. Для бинарных логических операций сущест2х2 вует 16 бинарных функций (2 ). В GDI эти функции называются бинарны-
ROP2
Формула
Описание
R2_BLACKPEN
D=0
Всегда 0, черный цвет в режиме RGB
R2_NOTMER6EPEN
D = ~(D P)
Инверсия R2_MERGEPEN
R2JWSKNOTPEN
D = D&-P
Конъюнкция приемника с инвертированным пером
R2_NOTCOPYPEN
D = -P
Инверсия цвета пера
R2JASKPENNOT
D = P&-D
Конъюнкция пера с инвертированным приемником
R2JOT
D = -D
Инверсия приемника
A
R2JORPEN
D=D P
Приемник и перо объединяются операцией исключающего «ИЛИ»
R2_NOTMASKPEN
D = -(D&P)
Инверсия R2_MASKPEN
R2JIASKPEN
D = D&P
Конъюнкция приемника с пером
A
R2JOTXORPEN
D -= ~(D P)
Инверсия R2_XORPEN
R2JOP
D=D
Приемник не изменяется
R2JER6ENOTPEN
D = D|-P
Дизъюнкция приемника с инвертированным пером
R2_COPYPEN
D=P
Перо
R2JIERGEPENNOT
D = P|-D
Дизъюнкция пера с инвертированным приемником
R2_MERGEPEN
D = P|D
Конъюнкция пера с приемником
R2_WHITE
D=1
Всегда 1, белый цвет в режиме RGB
Контекст устройства GDI содержит атрибут, определяющий текущую операцию ROP2. Этот атрибут также называется режимом рисования (draw mode). Для получения текущего значения этого атрибута используется функция GetROP2, а для присваивания ему нового значения — функция SetROP2. Эти функции определяются следующим образом: int SetROP2(HDC hDC. int fnDrawMode): int GetROP2(HDC hDC): Функция SetROP2 назначает в контексте устройства новую бинарную операцию (при условии передачи корректного значения) и возвращает исходный режим рисования. Функция GetROP2 просто возвращает текущий режим рисования. По умолчанию в контексте устройства выбирается режим R2_COPYPEN, при котором пикселу приемника просто присваиваетсях цвет пера. Режим рисования
424
Глава 8. Линии и кривые
используется всюду, где используются перья, — то есть при выводе линий и кривых, а также при обводке заполненных областей. На рис. 8.1 показан эффект применения всех 16 бинарных растровых операций к 20 разноцветным полосам на экране в режиме True Color. Сначала программа рисует вертикальные полосы с применением цветов, взятых из палитры контекста устройства по умолчанию. Перед выводом каждой горизонтальной полосы происходит переключение бинарной растровой операции. Взгляните на рисунок; в режиме R2_BLACK фон закрашивается черным цветом, в режиме R2_NOT фон инвертируется, в режиме R2_NOP фон не изменяется, а в режиме R2_WHITE фон закрашивается белым цветом. В 256-цветном режиме цвета могут изменяться непредсказуемо, если только системная палитра не была специально подготовлена для последующего выполнения логических операций.
R2JBLACK R2_NOTMERGEPEN R2_MASKNOTPEN R2_NOTCOPYPEN R2_MASKPENNOT R2_NOT R2_XORPEN R2_NOTMASKPEN R2_MASKPEN R2_NOTXORPEN R2_NOP R2_MERGENOTPEN RZ_COPYPEN R2_MERGEPENNOT R2_MERGEPEN R2_WHITE Рис. 8.1. Эффект от выполнения бинарных растровых операций (режим True Color)
При использовании бинарных растровых операций следует помнить о том, что операции определяются для физических цветов, а не для логических значений COLORREF. Таким образом, результат операций является более или менее аппаратно-зависимым. Для устройств, использующих цветовое пространство RGB, операции применяются к каждой из трех составляющих RGB, поэтому результат вполне предсказуем, но не всегда оправдан с точки зрения логики. В цветовой модели RGB хранятся значения интенсивности основных цветов, поэтому применение поразрядных логических операций не всегда находит соответствие в цветовом восприятии. Для устройств с палитрой растровые операции применяются к цветовым индексам, поэтому результат зависит от упорядочения цветов в палитре. В многозадачных ОС семейства Windows приложения не обладают полным контролем над аппаратной палитрой системы. В GDI существуют
425
Бинарные растровые операции
специальные функции, при помощи которых приложение вносит изменения в системную палитру, причем приоритетным правом обладают окна переднего плана. Обрабатывая специальные сообщения, приложение может реагировать на изменения в системной палитре. Палитры подробно рассматриваются в главе 13. Бинарные растровые операции играют важную роль в компьютерной графике. Режим R2J3LACK используется для закраски пикселов черным цветом (0), а режим R2_WHITE окрашивает пикселы в белый цвет (1, или OxFFFFFF в 24-разрядном кадровом буфере). Режим R2__NOTCOPYPEN меняет цвет пера на противоположный. Режим R2_NOP полностью подавляет вывод линий и кривых — это очень удобно, если вы не хотите обводить прямоугольник рамкой. Режим R2_MASKPEN обеспечивает избирательное подавление битов на графической поверхности контекста устройства. Например, если режим R2_MASKPEN используется для пера RGB(OxFF,0,0) в кадровом буфере RGB, при выводе линий данные синего и зеленого канала маскируются, в результате остаются только данные красного канала. При использовании цвета RGB(Ox7F,Ox7F,Ox7F) подавляются яркие цвета, поскольку после вывода максимальная интенсивность каждого канала будет равна только 127 вместо 255. Режимы R2_NOT и R2_XORPEN часто используются в интерактивной компьютерной графике для вывода перекрестий и эластичных контуров. Перекрестие состоит из горизонтальной и вертикальной линий, пересекающих весь экран. Точка пересечения этих линий определяет текущую позицию курсора. Перекрестия часто применяются при выравнивании объектов в графических редакторах. Эластичные контуры изменяются динамически и обозначают некие границы, определяемые пользователем при помощи мыши или клавиатуры. Эластичные прямоугольники и другие фигуры часто используются при построении и выделении геометрических фигур в графических пакетах. В процессе перемещения мыши построение фигуры считается еще не законченным, поэтому фиксировать фигуру нельзя. Вместо этого, когда пользователь перемещает мышь, приложение должно быстро нарисовать контур, стереть его, восстановить исходное содержимое и переместить в новую позицию. Бинарные операции R2_NOT, R2_XORPEN и R2_NOTXORPEN позволяют быстро рисовать временные линии и удалять их, не оставляя следа, поскольку при повторном применении этих операции восстанавливается исходНое содержимое — одно из свойств логических операций. В листинге 8.1 показано, как реализовать перекрестие с использованием операции R2_NOT. Класс окна хранит последнюю позицию курсора в переменных (mjastx,mjasty). Для каждого сообщения WMJIOUSEMOVE функция вывода перекрестия вызывается дважды — для удаления старых и для рисования новых линий. Листинг 8.1. Вывод перекрестия с использованием R2_NOT
void KMyCanvas::DrawCrossHair(HDC hDC. bool on) { if ( m lastx<0 ) return; RECT rect; GetCl i entRect (m SetROP2(hDC. R2 NOT):
rect): Продолжение
426
Глава 8. Линии и кривые
Листинг 8.1. Продолжение MoveToExthDC, rect.left. mjasty. NULL); LineTo(hDC. rect.right, mjasty); MoveToEx(hDC. mjastx. rect.гор, NULL); LineTo(hDC. m_lastx. rect.bottom): }
. LRESULT KMyCanvas::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam) { switchC uMsg ) { case WM_CREATE: m_lastx = mjasty = -1: // Перекрестия еще нет return 0: case WM_MOUSEMOVE: {
HOC hOC = GetDC(hWnd): OrawCrossHair(hDC); mjastx = LOWORD(lParam): mjasty = HIWORD(lParam); DrawCrossHairthDC. true): ReleaseDC(hWnd. hDC);
Хотя каждая из операций R2_NOT, R2J
Режим заполнения фона и цвет фона Для некоторых графических примитивов GDI делит выводимые пикселы на два класса: основные (foreground) и фоновые (background). Например, при выводе текста пикселы, образующие глифы символов, считаются основными, а остальные пикселы текстовой области считаются фоновыми. При выводе пунктирных
Перья
427
линий пикселы отрезков считаются основными, а пикселы промежутков — фоновыми. Основные пикселы выводятся всегда, а вывод фоновых пикселов необязателен. В каждом контексте устройства присутствует такой атрибут, как режим заполнения фона, управляющий выводом фоновых пикселов. При выводе фоновых пикселов задействуется цвет, заданный другим атрибутом контекста устройства — цветом фона. Для работы с этими атрибутами в GDI используются следующие функции: int GetBkMode(HDC hOC): int SetBkMode(HDC hDC. iBkMode): COLORREF GetBkColor(HDC hDC): COLORREF SetBkColor(HDC 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 GetCurrentObjecKHDC hDC. UINT uObjectType): HGDIOBJ SelectObjecttHDC hDC. HGDIOBJ hgdiobj): int GetObjecUHGDIOBJ hgdiobj. int cbBuffer, LPVOID IpvObject): int EnumObjectsCHDC hOC. int nObjectType. GOBJENUMPROC IpObjectProc. LPARAM IParam); Функция GetCurrentObject возвращает манипулятор текущего объекта GDI в контексте устройства; тип объекта определяется параметром uObjectType. Например, GetCurrentObject (hDC, OBJ_PEN) возвращает манипулятор текущего объекта логического пера. Функция SelectObject заменяет манипулятор объекта GDI в контексте манипулятором нового объекта и возвращает старый манипулятор. Таким образом, если при вызове SelectObject был указан манипулятор действительного объекта логического пера, функция SelectObject изменяет значение атрибута логического пера в контексте устройства. Функция GetObject возвращает исходное определение объекта GDI. Функция EnumObjects вызывает заданную функцию для каждого объекта GDI, доступного для пользователей, в контексте устройства. Объект логического пера, как и другие объекты GDI, поглощает ресурсы пользовательского пространства и ресурсы ядра, а также занимает место в таблице объектов GDI. Следовательно, когда необходимость в логическом пере отпадает, его следует исключить из контекста устройства и уничтожить функцией DeleteObject. По тем же причинам приложение не должно создавать слишком большого количества объектов GDI и не должно уничтожать их лишь при выходе из приложения. В системах Win32 все процессы в системе используют общую таблицу, рассчитанную на 16 384 манипуляторов GDI. Следовательно, если приложение создает 1024 манипуляторов объектов GDI, в системе одновременно может работать не более 16 таких приложений. Впрочем, при завершении процесса операционная система удаляет все созданные им объекты GDI и освобождает все занимаемые ресурсы.
Перья
429
Стандартные перья В GDI определяются четыре стандартных объекта перьев, которые могут использоваться любыми приложениями. Чтобы получить манипулятор стандартного пера, вызовите функцию GetStockObject с индексом стандартного объекта. Например, GetStockObject(BLACK_PEN) возвращает однородное черное перо толщиной в один пиксел, также назначаемое по умолчанию в контексте устройства. Вызов GetStockObject(WHITE_PEN) возвращает однородное белое перо толщиной в один пиксел. GetStockObject (NULL_PEN) возвращает пустое перо, которое ничего не рисует и может использоваться для временного запрета вывода линий. Эти стандартные перья (черное, белое и пустое) существуют уже давно. В Windows 98/2000 наконец появилось новое стандартное перо — перо DC, возвращаемое вызовом GetStockObject(DC_PEN). Перо DC, или выражаясь шире — объект GDI контекста устройства, является абсолютно новой концепцией. Обычные объекты GDI «намертво» фиксируются при создании. Их можно использовать, можно удалять, но нельзя изменять. Если вам понадобился слегка отличающийся объект GDI, приходится создавать новый объект и удалять старый. При большом количестве объектов GDI это приводит к снижению быстродействия (например, при реализации градиентных заливок без прямой поддержки со стороны GDI). Перо DC принадлежит к новому типу объектов GDI и является лишь одним из частных случаев. Можно называть эти объекты объектами GDI контекста устройства, поскольку они имеют особый смысл лишь при присоединении к контексту устройства. После выбора в контексте устройства такие объекты можно до определенной степени модифицировать. Если такие изменения поддерживаются GDI, вы избавляетесь от необходимости создавать новые или заменять старые объекты. По умолчанию перо DC является однородным черным пером толщиной в один пиксел. После выбора пера DC в контексте устройства вы можете изменять только его цвет. При работе с цветом пера DC используются следующие функции: COLORREF GetDCPenColor(HDC hDC): COLORREF SetDCPenColor(HDC hDC. COLORREF crColor); Функция GetDCPenCol or возвращает текущий цвет пера DC в контексте устройства. Функция SetDCPenColor устанавливает новый цвет пера и возвращает старый цвет пера. Эти функции могут использоваться даже в том случае, если перо DC не выбрано в контексте устройства. Цвет пера DC можно рассматривать как новый атрибут контекста устройства, требуемый для рисования линий только в том случае, если перо DC выбрано в качестве текущего объекта пера. Следующий фрагмент показывает, как нарисовать градиентную заливку всего одним пером: HGDIOBJ hOld = SelectObject(hDC. GetStockObject(DC_PEN)): for (int i=0; i<128- i++) { SetDCPenColor(hDC. RGBCi. 255-i. 128+i)): MoveToE>t(hDC. 10. i+10. NULL): LineTo(hDC. 110. i+10): SelectObject(hDC. hOld);
430
Глава 8. Линии и кривые
После получения и выбора пера DC программа при помощи функции SetDCPenColor постепенно изменяет цвет пера в интервале от RGB(0,255,128) до RGB(127,128,255), создавая линейную градиентную заливку. После завершения вывода программа восстанавливает исходное перо. Без пера DC нам пришлось бы при каждой итерации создавать новое перо, выбирать его с контексте устройства и удалять старое перо. Стандартные объекты заранее создаются операционной системой и совместно используются всеми процессами, работающими в системе. После завершения работы со стандартными объектами их манипуляторы удалять не нужно. Впрочем, вызов DeleteObject для манипулятора стандартного пера абсолютно безопасен — DeleteObject просто возвращает TRUE, не выполняя никаких действий.
Простые перья Все стандартные перья имеют однородный цвет и единичную толщину. Чтобы рисовать прерывистые или более толстые линии, приложение создает нестандартные объекты логического пера. Ниже приведены две несложные функции для создания простых перьев: HPEN CreatePen(int fnPenStyle. int nWidth, COLORREF crColor): HPEN CreatePenlndirect(CONST LOGPEN * Iplgpn): Структура LOGPEN содержит три параметра логического пера, а именно его стиль, толщину и ссылку на цвет. Следовательно, эти две функции представляют собой разновидности одной и той же функции. В практической реализации CreatePenlndirect извлекает данные из структуры LOGPEN и вызывает функцию CreatePen. Стиль пера определяет порядок следования пикселов и расположение линии. В табл. 8.2 перечислены различные стили перьев и ограничения, накладываемые на их реализацию. Таблица 8.2. Простые стили перьев
431
Перья
Одной из составляющих стиля пера является правило чередования отрезков и промежутков в нарисованной линии. Перья со стилями PS_SOLID и PS_INSIDEFRAME рисуют сплошные линии, а перо PS_NULL вообще ничего не рисует, поэтому остается разобраться с 4 оставшимися стилями. Пунктирное перо PS_DASH состоит из отрезков длиной 18 пикселов, разделенных промежутками в 6 пикселов. Точечное перо PS_DOT состоит из отрезков длиной 3 пиксела, разделенных промежутками в 3 пиксела. Пунктирно-точечное перо PS_DASHDOT строится по правилу «отрезок 9 пикселов, промежуток 6 пикселов, отрезок 3 пиксела, промежуток 6 пикселов». Для перьев PS_DASHDOTDOT используется цикл «отрезок 9 пикселов, промежуток 3 пиксела, отрезок 3 пиксела, промежуток 3 пиксела, отрезок 3 пиксела, промежуток 3 пиксела». Вероятно, в Microsoft решили, что один пиксел слишком мал, поэтому одна точка на линии представляется тремя пикселами. На рис. 8.2 показаны циклы чередования пикселов в стилях, перечисленных в табл. 8.2. В левом столбце указано название стиля, в среднем — пример линии, нарисованной пером этого стиля, а справа та же линия изображена в увеличении. Линии на рисунке выведены в режиме заполнения OPAQUE темным пером на светлом фоне. Как видно из рисунка, перо PS_NULL не выводит ничего, даже пикселов фона. Если толщина линии составляет один пиксел в системе координат устройства, перо PS_INSIDEFRAME эквивалентно PS_SOLID. В линиях других стилей наглядно прослеживается цикл чередования пикселов. PS_SOLID PS_DASH PS_DOT PS_DASHDOT PSJ)ASHDOTDOT PS_NULL PS INSIDEFRAME
МЙШШИВ
••ПППСОП1 IHODDI 3DOQQDMI
•••агдгдм! папппппппппппппппппппппппппппп Рис. 8.2. Стили простых перьев
Стиль
Вид линии
Выравнивание
PSJOLID
Сплошная, рисуются все пиксель!
По центру
PSJASH
Пунктирная
По центру
Толщина <1
PS_DOT
Точечная
По центру
Толщина <Д
PS_DASHDOT
Чередование отрезков и точек
По центру
Толщина < 1
PSJIASHDOTDOT
Отрезок и две точки
По центру
Толщина < 1
PS_NULL
Линия не рисуется
Нет
PSJNIDEFRAME
Сплошная, рисуются все пикселы
Внутри контура
Ограничения
Толщина > 1 , используется при обводке замкнутых областей
Второй параметр CreatePen определяет толщину линии в логической системе координат. Фактическая толщина линии в физических координатах зависит от мировых преобразований и от отображения окна в область просмотра. Например, в режиме MMJ.OENGLISH с тождественным мировым преобразованием перо с логической толщиной 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 пикселов, эту линию можно нарисовать несколькими разными способами. Первый вопрос — как пикселы линии должны располагаться по отношению к базовой оси? Для всех перьев, кроме перьев со стилем PS_INSIDEFRAME, нарисованная линия центруется относительно своей базовой оси. Перья со стилем PS_INSIDEFRAME используются при обводке некоторых фигур GDI (прямоугольников простых и с закругленными углами, эллипсов и т. д.). В этом случае центр линии смещается внутрь области таким образом, чтобы линия не выходила за границы контура. Чтобы нарисовать линию внутри контура, GDI необходимо точно знать, какая из двух сторон линии является внутренней, а какая — внешней. Следовательно, при выводе обычных линий и даже многоугольников перо со стилем PS_INSIDEFRAME рисует обычную сплошную линию, центрованную по базовой оси, поскольку GDI не знает, какая сторона является внутренней. Стиль PS_INSIDEFRAME активизируется лишь при рисовании определенных фигур с четко различаемой внутренней и внешней частью. Не каждую линию удается точно отцентрировать по базовой оси. Только в вертикальных и горизонтальных линиях с нечетной толщиной один пиксел находится в центре, а остальные равномерно распределяются по обе стороны от него. При выводе утолщенных линий возникает и другой вопрос — как должны выглядеть концы линий? Для перьев, созданных функцией CreatePen, линии всегда заканчиваются полукруглыми концами.
433
Перья
точки) появляются дополнительные пикселы (темные точки), образующие утолщенные линии и закругленные концы. Обратите внимание: только при нечетной толщине линия симметрично центруется относительно базовой оси. ПРИМЕЧАНИЕ В Windows 95/98 утолщенные линии рисуются не так, как показано на рисунке, полученном в Windows 2000. Пикселы не распределяются равномерно по двум сторонам базовой оси, а завершение не имеет нормальной полукруглой формы.
Расширенные перья Если разобраться со всеми ограничениями, становится ясно, что простые перья, созданные функциями CreatePen или CreatePenlndirect, существуют только в двух формах: стилевое перо с толщиной один пиксел и утолщенное перо с круглым завершением. Стиль PS_INSIDEFRAME из-за своей реализации в Windows GDI особой пользы не приносит, поскольку он может применяться лишь для некоторых фигур, вписанных в прямоугольник. Однако для этих фигур приложение может легко добиться того же эффекта при помощи обычного пера, слегка уменьшив ограничивающий прямоугольник. Для преодоления этих ограничений в Win32 API появилась новая функция ExtCreatePen, создающая расширенные перья с обогащенным набором атрибутов. HPEN ExtCreatePen(DWORD dwPenStyle, DWORD dwWidth. CONST LOGBRUSH * Iplb'. DWORD dwStyleCont. CONST DWORD * IpStyle); Функция ExtCreatePen позволяет создавать перья 2 типов, 9 стилей, с 3 видами завершений и 3 видами соединений. Вся эта информация определяется одним параметром dwStyle в виде комбинации флагов из табл. 8.3, объединенных поразрядным оператором ИЛИ (|). Таблица 8.3. Типы, стили, завершения и соединения расширенных перьев Флаг
Смысл
Ограничения
Типы PS_COSMETIC
Косметическое перо. Толщина равна одному пикселу
PS GEOMETRIC
Геометрическое перо. Толщина пера задается в логических единицах
Стили PSJOLID
Рис. 8.3. Концы линий разной толщины На рис. 8.3 изображены концы линий со стилем PS_DOT, которые были нарисованы перьями разной толщины, созданными функцией CreatePen. Если физическая толщина равна 0 или 1, нарисованная линия действительно состоит из точек. С увеличением толщины линии по обеим сторонам базовой оси (светлые
PS DASH
Непрерывная линия, рисуются все пикселы. Выравнивание по центру Пунктирная линия. Выравнивание по центру
В Windows 95/98 не поддерживаются геометрические перья с этим стилем Продолжение
434
Глава 8. Линии и кривые
Таблица 8.3. Продолжение Флаг
Смысл
PS_DOT
Точечная линия. Выравнивание по центру
Ограничения
ных кистей. Два последних параметра ExtCreatePen определяют пользовательское правило чередования пикселов в перьях стиля PSJJSERSTYLE.
Косметические перья
PS_DASHDOT
Пунктирно-точечная линия. Выравнивание по центру
PS_DASHDOTDOT
Отрезок и две точки. Выравнивание по центру
PS_NULL
Линия не рисуется
PS_INSIDEFRAME
Непрерывная линия, рисуются все пикселы. Выравнивание внутри контура
Только геометрические перья, используется лишь при обводке некоторых фигур GDI
PS_USERSTYLE
Чередование отрезков и промежутков задается параметрами dwStyleCount и IpStyle. Выравнивание по центру
Поддерживается только в Windows NT/2000
PS_ALTERNATE
Чередование «пиксел — промежуток»
Поддерживается только в Windows NT/2000 и только для косметических перьев
Завершение PS_ENDCAP_ROUND
435
Перья
Закругленное завершение (к линии добавляется половина круга)
Только для геометрических перьев
PS_ENDCAP SQUARE
Квадратное завершение (к линии добавляется половина квадрата)
Только для геометрических перьев
PS_ENDCAP_FLAT
Плоское завершение
Только для геометрических перьев
PS_JOIN_BEVEL
Усеченное соединение
Только для геометрических перьев
PS_JOIN_MITER
Заостренное соединение
Только для геометрических перьев
PS JOIN ROUND
Закругленное соединение
Только для геометрических перьев
Соединение
Расширенные перья делятся на два типа: косметические и геометрические. Параметр dwWidth определяет толщину пера. Для косметических перьев толщина может быть равна только 1. Для геометрических перьев толщина задается в логических координатах, поэтому фактическая толщина линий зависит от мировых преобразований и отображения окна в область просмотра. Расширенные перья используют структуру LOGBRUSH для определения цветов и узоров. В косметических перьях допускаются только однородные цвета и сплошная заливка, а геометрические перья допускают наличие смешанных цветов и различных узор-
Косметические перья всегда рисуют линии толщиной в один пиксел. Хотя в MSDN утверждается, что косметические перья обладают произвольной шириной, задаваемой в системе координат устройства, единственным допустимым значением параметра dwWidth является 1; при любых других значениях вызов функции завершается неудачей. В Windows NT/2000 появились два новых стиля: PSJJSERSTYLE и PS_ALTERNATE. Стиль PS_ALTERNATE может использоваться только в косметических перьях для создания «настоящих» точечных линий. Стиль PSJJSERSTYLE позволяет приложению определять собственные последовательности чередования пикселов (биты стиля). Для создания пера с пользовательским стилем необходимы два дополнительных параметра, dwStyleCount (тип DWORD) и IpStyle (массив DWORD). Первый элемент массива содержит длину первого отрезка, второй — длину промежутка, третий — длину второго отрезка и т. д. При этом одна единица соответствует трем пикселам вместо одного. Следовательно, пользовательский стиль позволяет имитировать стили PS_DASH, PS_DOT, PS_DASHDOT и PS_DASHDOTDOT, но не стиль PS_ ALTERNATE. В приведенном ниже фрагменте создается косметическое перо с циклом чередования {4,3,2,1} — то есть отрезок из 12 пикселов, промежуток из 9 пикселов, отрезок из 6 пикселов и промежуток из 3 пикселов. const DWORD cycle[4] = { 4. 3. 2. 1 }: const LOGBRUSH brush = {BS_SOLID. 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 PSJMULL PSJNSIDEFRAME PSJJSERSTYLE PS ALTERNATE
••••••••••ппапппм
•••шпмапппимшпимащимпш •••••••••DDDDDDMHZDDDDDIHMB
паппаапппппппппппппапппапппппп •••••вшшввмпшапппппиммиппп •пипипипиаипипипипипиаипипипиа Рис. 8.4. Стили косметических перьев
436
Глава 8. Линии и кривые
Как подсказывает название, косметические перья хорошо подходят для рисования тонких линий, особенно на экране монитора. При выводе в контексте устройства принтера, обладающем повышенным разрешением, стилевые линии выглядят как более светлые сплошные линии, и даже сплошные линии видны лишь при высоком контрасте с цветом фона.
Геометрические перья Геометрическое перо рисует линию «наконечником» в виде геометрической фигуры. Говоря точнее, геометрическое перо обладает переменной толщиной, стилем, завершением и соединением. Давайте рассмотрим эти атрибуты более подробно. Толщина геометрического пера задается в логических координатах, но в отличие от перьев, созданных функцией CreatePen, толщина геометрического пера не может быть равна 0. С реальной физической толщиной геометрического пера дело обстоит сложнее. В режиме ММ_ТЕХТ с тождественным преобразованием одна логическая единица устройства преобразуется в одну физическую единицу, поэтому физическая толщина совпадает с логической. Если мировое преобразование и отображение окна в область просмотра используют одинаковый масштаб по обеим осям, толщина логического пера масштабируется в соответствии с заданным масштабным коэффициентом. Но если масштаб различается, вертикальные и горизонтальные линии, нарисованные одним и тем же геометрическим пером, будут иметь разную толщину. Это относится и к перьям, созданным функцией CreatePen. Линия, нарисованная геометрическим пером, рассматривается не как простая последовательность пикселов, а как геометрическая фигура. Например, линия (0,0)-(100,0), нарисованная пером толщины 10, по форме совпадает с прямоугольником, определяемым противоположными углами (0,0) и (100,10). Мировые преобразования и режим отображения распространяются на линию в той же степени, как и на прямоугольники. На рис. 8.5 изображены контрольные точки повернутой геометрической линии (хО,уО) - (х1,у1). dx = pen_width * sin(o)/2 dy = pen_width * cos(o)/2
(x1-dxy1+dy)
" Jxl+dxyl-dy) (xO-dxyO+dy) (xO.yO) (xO+dxyO-dy)
Рис. 8.5. Геометрическая линия как геометрическая фигура
Перья
437
Атрибут завершения геометрической линии определяет вид «наконечников», добавляемых к обоим концам линии или ее внутренних отрезков. Линия, изображенная на рисунке, выводится без завершений, что соответствует стилю PS ENDCAP_FLAT (плоское завершение). К обоим концам утолщенных линий, нарисованных простыми перьями, добавляются полукруглые наконечники (PS_ENDCAP_ ROUND). При квадратном завершении (PS_ENDCAP_SQUARE) к обоим концам присоединяются половины квадратов. В Windows NT/2000 для геометрических перьев реализованы все стили кроме стиля PS_ALTERNATE, зарезервированного для косметических линий. В отличие от простых перьев, созданных функцией CreatePen, утолщенные геометрические линии рисуются в соответствии со своим стилем и не преобразуются в сплошные линии. Геометрические перья не изображают одну точку тремя пикселами; вместо этого размер точки или отрезка масштабируется вместе с толщиной линии. При этом одна точка изображается одним пикселом, как при использовании косметических линий со стилем PS_ALTERNATE. С утолщением пера увеличиваются и размеры точек. Каждый отрезок или точка оформляются соответствующими завершениями. Следовательно, отрезки пунктирной линии могут заканчиваться плоскими, круглыми или квадратными завершениями. К сожалению, в Windows 95/98 реализация Win32 GDI API выглядит несколько иначе. В этих системах, основанных на 16-разрядных версиях GDI, утолщенные геометрические перья реализуются в виде сплошных линий. Геометрические перья уже не ограничиваются одним сплошным цветом. Функции ExtCreatePen передается структура LOGBRUSH, содержащая информацию о цвете, стили кисти и стиле штриховки. Структура LOGBRUSH обычно используется для определения логических кистей, предназначенных для заливки фигур. Однако геометрические линии принципиально не отличаются от геометрических фигур, поэтому вполне естественно, что GDI позволяет закрашивать их кистями. На рис. 8.6 изображены геометрические линии с разными завершениями, стилями и узорами. Обратите внимание: стиль PS_ALTERNATE для геометрических линий считается недействительным, а стиль PS_NULL ничего не рисует. В отличие от косметических перьев геометрические перья рисуют линии в прозрачном режиме, игнорируя цвет и режим заполнения фона (с одним исключением — при работе со штриховой кистью эти атрибуты используются для закраски фона между штрихами). Также стоит обратить внимание на то, что величина промежутков в пользовательском стиле задается в пикселах, а не в логических единицах, которые изменяются вместе с толщиной пера. С утолщением пера или с добавлением завершений отрезки в линиях пользовательского стиля могут «наползать» друг на друга. Стыки утолщенных линий с завершениями могут выглядеть не так, как можно было бы предположить. На рис. 8.7 показано, как выглядит буква Z, состоящая из трех утолщенных линий с разными завершениями. Тонкие белые линии обозначают положение базовых осей. На рисунке видны завершения, созданные стилями PS_ENDCAP_SQUARE и PS_ENDCAP_ROUND. Плоские и квадратные завершения стыкуются неровно. Хотя линии с круглыми завершениями стыкуются нормально, при использовании режима R2_XORPEN общие части линий будут отличаться по цвету, поскольку они прорисовываются дважды.
438
Глава 8. Линии и кривые
w=3, flat
w=7, flat
w=11, square
w=15, round, hatch
PS_SOLID PS_DASH PS_DOT PS_DASHDOT PS_DASHDOTDOT PS_NULL PSJNSIDEFRAME PS_USERSTYLE PS ALTERNATE Рис. 8.6. Геометрические линии с различной толщиной, завершениями, штриховкой и стилями
PS ENDCAP FLAT
PS ENDCAP SQUARE
PS ENDCAP ROUND
PS_ENDCAP_ROUND R2 XORPEN
Рис. 8.7. Соприкосновение линий с разными завершениями
Для обеспечения плавной стыковки линий GDI позволяет объединить несколько линий и кривых в один графический вызов. Если геометрическое перо используется для рисования линии или кривой, состоящей из нескольких сегментов, способ стыковки сегментов определяется специальным атрибутом — соединением. Существует три типа соединений. Усеченное соединение строится по форме двух сегментов с плоскими соединениями; к стыку добавляется треугольник, заполняющий впадину. Заостренное соединение строится аналогичным образом, но в этом случае линии продолжаются до точки пересечения. Закругленное соединение выглядит так же, как и при стыковке двух линий с круглыми завершениями. На рис. 8.8 изображена та же Z-образная фигура, нарисованная функцией P o l y l i n e GDI, позволяющей за один вызов нарисовать несколько линий с разными типами соединений. Кроме улучшения внешнего вида соединений, функция Polyline рисует каждый пиксел только один раз, поэтому даже при использовании операции R2_XORPEN вся линия будет нарисована одним цветом.
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 peOldLlmit): Функция SetMiterLimit определяет угловой лимит — максимальное отношение длины заострения к толщине линии. На второй фигуре слева (см. рис. 8.8) длина заострения равна расстоянию от пересечения внешних границ линии до пересечения внутренних границ. Толщина пера равна расстоянию между двумя пересечениями по оси у. По умолчанию угловой лимит в контекстах устройств равен 10,0. На рисунке отношение равно примерно 4,35. Если отношение длины заострения к толщине линии превышает угловой лимит, вместо заостренного соединения используется усеченное. Если вы предпочитаете математический подход, то при пересечении двух линий под углом 0 угловое отношение определяется выражением l/sin(0/2), независящим от толщины пера. В приведенном выше примере ширина Z-образной фигуры вдвое превышает ее высоту, поэтому sin(O) = 1/V5, sin(0/2) = 0,229753, а угловое отношение равно 4,352502.
Получение информации о логических перьях По известному манипулятору объекта логического пера приложение может узнать тип пера и получить его описание при помощи двух общих функций: DWORD GetObjectType(HGDIOBJ h); int GetObject(HGDIOBJ hgdiobj. int cbBuffer, LPVOID IpvObject): Функция GetObjectType возвращает идентификатор типа объекта GDI. Для простого пера возвращается константа OBJ_PEN; для расширенного пера возвращается OBJ_EXTPEN. Функция 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 OBJ_PEN: GetObject(hPen. sizeof(logpen). & logpen): break:
440
Глава 8. Линии и кривые case OBJJXTPEN: size - GetObjectChPen, 0. NULL); pextlogpen = (EXTLOGPEN *) new char[size]; GetObjectChPen. size, pextlogpen); break; default:
441
Перья // Класс для работы с объектами перьев class KPen : public KSelect { public: HPEN m_hPen: KPenCint style, int width. COLORREF color HDC hDC-NULL) { m_hPen = CreatePen(style, width, color); Select(hDC):
if ( pextlogpen )
{
delete [] (char *) pextlogpen; pextlogpen = NULL:
KPenCint style, int width. COLORREF color, int count. DWORD * gap. HDC hDC-NULL) { LOGBRUSH logbrush = { BS_SOLID. color, 0 }:
Класс для работы с объектами перьев GDI
m_hPen = ExtCreatePenCstyle. width. & logbrush. count, gap); Select(hDC): } void Select(HDC hDC) { KSelect::Select(hDC. mJnPen): } -KPenО {
Чтобы воспользоваться нестандартным объектом пера, необходимо создать его и выбрать в контексте устройства; после использования объект исключается из контекста и удаляется. Сделать это несложно, но не слишком интересно. Ниже приведен простой класс КРеп C++, предназначенный для работы с простыми перьями и обычными расширенными перьями. // Класс для выбора объектов GDI class KSelect
UnSelectO: DeleteObject(m_hPen):
{ HGDIOBJ m_h01d: HOC mJiDC: public: void SelectCHOC hDC. HGDIOBJ hObject) { if ( hDC ) { m_hDC = hDC: mjidld = SelectObjectChDC. hObject):
else m_hDC = NULL: m hOld - NULL;
void UnSelect(void) { if ( mJiDC ) {
SelectObject(m_hDC. m_h01d): mJiDC = NULL; m_h01d = NULL;
Класс KPen содержит два конструктора. Первый конструктор создает простые перья, а второй — расширенные перья без узорной кисти. В принципе можно написать еще один конструктор, который бы создавал расширенные перья с узорной кистью. Оба конструктора получают необязательный параметр - манипулятор контекста устройства. Если этот параметр задан, манипулятор созданного пера GDI выбирается в заданном контексте устройства. Деструктор исключает перо из контекста, если оно все еще остается выбранным, и удаляет объект. Два дополнительных метода предназначены для явного выбора и исключения манипулятора пера из контекста. Применять класс КРеп чрезвычайно просто. Если в некотором фрагменте используется всего одно перо, заключите экземпляр класса КРеп в^ соответствующий блок, передайте конструктору манипулятор контекста устройства, и в дальнейшем создание объекта GDI, его выбор в контексте, исключение и удаление будут выполняться автоматически. Если фрагмент программы работает с несколькими перьями, не передавайте манипулятор контекста конструктору; вместо этого в нужные моменты следует вызывать методы Select и UnSelect. Рассмотрим несколько простых примеров.
{
KPen red(PS_SOLID. I. RGBCOxFF, 0. 0). hDC): // Рисовать красным пером
442
Глава 8. Линии и кривые
// При выходе из этого блока перо автоматически // исключается из контекста и уничтожается
KPen red (PS_SOLID. 1. RGB(OxFF. 0, 0)): KPen green (PSJOLID, 1, RGB(0, OxFF. 0)): red.Select(hDC): // Рисовать красным пером red.UnSelect(hOC); 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 PolyPolylineCHDC hDC. CONST POINT * Ippt. CONST DWORD * IpdwPolyPoints, DWORD nCount); Функция MoveToEx не рисует линий — oira всего лишь перемещает текущую позицию пера в контексте устройства в точку с заданными координатами. Исходная позиция возвращается в параметре IpPoint. Такие функции, как LineTo, PolylineTo, PolyBezierTo и даже функции вывода текста, начинают вывод с текущей позиции пера. Все координаты задаются в логическом пространстве. Функция LineTo рисует линию от текущей позиции пера к точке (nXEnd, nYEnd) и переводит текущую позицию в точку (nXEnd, nYEnd). Внешний вид линии зависит от всех атрибутов, описанных в предыдущем разделе. В процессе вывода также могут учитываться и другие атрибуты контекста устройства — например, мировое преобразование, режим отображения, бинарная растровая операция, режим заполнения фона, цвет фона и угловой лимит. При этом необходимо учитывать ряд обстоятельств. Во-первых, пиксел физического координатного пространства, отображаемый в точку (nXEnd, nYEnd), не рисуется, а начальная точка рисуется. Например, если вы проводите линию из точки (0,0) в точку (100,0) при тождественном отображении из логических координат в физические, рисуются пикселы (0,0)-(99,0), но не рисуется пиксел
443
Линии
(100,0). При этом текущая позиция пера перемещается в точку (100,0), поэтому эта точка будет нарисована при выводе следующей линии, проведенной из этой точки. Если вы рисуете несколько соединенных линий функцией LineTo, каждый пиксел, за исключением последнего, рисуется ровно один раз. Это условие сохраняется при смещениях, масштабированиях, поворотах и других преобразованиях координатного пространства. Если при выводе линии используются такие бинарные растровые операции, как R2_XORPEN, точки соединения отрезков по виду не отличаются от других пикселов. Если бы функция LineTo прорисовывала последний пиксел, точки стыков рисовались бы дважды и поэтому выводились бы цветом фона. Из-за этого правила и по другим причинам операции рисования линий являются направленными. Другими словами, для двух точек (хО,уО) и (х1,у1) линия, проведенная из (хО,уО) в (х1,у1), несколько отличается от линии, проведенной из (х1,у1) в (хО,уО). Также следует помнить о том, что каждый вызов функции рисования линии стилевым пером заново начинает цикл чередования пикселов. Совмещение стилей разных линий (что-то наподобие изменения базовой точки кисти) в GDI не поддерживается. Например, для трех точек (хО,уО), (х1,у1) и (х2,у2), расположенных на одной прямой, результат рисования двух линий (хО,уО) - (х1,у1) и (х1,у1) - (х2,у2) может отличаться от результата рисования одной линии (хО,уО) - (х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 COLORREF color[] = { RGB(0. 0. 0). RGB(255. 0. 0). RGB(0.255.0), RGB(0.0.255). RGB(255.255.0). RGB(0. 255. 255). RGB(255. 255. 0). RGB(127. 255. 0). RGB(0, 127. 255). RGB(255. 0. 127) }: for (int p=0: p
SetDCPenColor(hDC. color[min(p-q.N-p+q)]): MoveToEx(hDC. (int)(220 + Radius * sintp * theta)), (int)(220 + Radius * cos(p * theta)). NULL); LineTothDC. (int)(220 + Radius * sin(q * theta)). (int)(220 + Radius * cos(q * theta)));
Функции PolylineTo передается массив структур POINT, содержащих коорди наты х и у. Сначала функция рисует первый отрезок от текущей позиции пера i первой точке массива POINT, а затем последовательно соединяет линиями во остальные точки массива. В конце рисования текущая позиция пера перемеща ется в последнюю точку. Функция Polyline получает те же параметры, что i
444
Глава 8. Линии и кривые
Polyline!, но работает несколько иначе — она не использует и не обновляет текущую позицию пера. Функция Polyline проводит отрезок от первой точки массива ко второй, а затем последовательно соединяет все остальные точки массива. Для массива из п структур POINT функция PolylineTo рисует п отрезков, а функция Polyline рисует п-1 отрезок. В общем случае функции PolylineTo и P o l y l i n e нельзя заменить вызовами MoveToEx и Li neTo. При использовании стилевого пера функции Pol yl i ne и Pol yl i neTo при переходе к новому отрезку продолжают старый цикл чередования пикселов, а при нескольких вызовах LineTo рисунок каждый раз начинается заново. При работе с геометрическим пером атрибут завершения применяется к первой и последней точке, а атрибут соединения — к каждому стыку. Использование функций Polyline и PolylineTo улучшает внешний вид стыков, чего довольно трудно добиться с помощью функции LineTo. Это особенно важно при увеличении изображения или при печати, где характерны утолщенные перья. Достаточно сравнить рис. 8.7, где используются серии вызовов Li neTo, и рис. 8.8, где используется один вызов Polyline. Кроме того, многократный вызов функции API обрабатывается дольше, чем однократный вызов для нескольких отрезков. Наконец, при выводе в метафайловый контекст устройства функции P o l y l i n e и PolylineTo занимают меньше места, чем серия вызовов Li neTo. Однако функции PolylineTo и Polyline не идеальны; в частности, они не поддерживают концепцию замкнутых контуров. Завершение выводится в первой и последней точке всегда, даже если их координаты в точности совпадают. Если вы рисуете геометрические фигуры, все углы которых кратны 90°, квадратные завершения и заостренные соединения обеспечат нормальное замыкание контура, а для рисования замкнутых фигур с закругленными углами всегда можно воспользоваться закругленными завершениями и соединениями. Но если вы рисуете произвольный треугольник или многоугольник, в нем могут встретиться острые или тупые углы. Заостренное соединение обеспечивает правильность всех стыков, кроме самой первой точки (которая также является последней). Существует стандартное решение — добавить в конец дополнительный отрезок от первой точки ко второй. Даже при использовании бинарных растровых операций, при которых существуют различия между однократным и двукратным выводом, фигура, нарисованная за один вызов PolylineTo или Polyline, не содержит пикселов, нарисованных дважды. В следующем фрагменте функция Polyline используется для рисования треугольника. Необязательный параметр extra позволяет создать дополнительный отрезок, улучшающий вид конечной точки: void TriangletHDC hDC, int xO. int yO. int xl. int yl. int x2. int y2. bool extra=false) { POINT corner[5]= { x O . y O . xl.yl. x2.y2. x O . y O , xl.yl}; if (extra) // Дополнительный отрезок. // улучшающий вид замкнутой фигуры Polyline(hDC. corner. 5 ) : else Polyline(hDC. corner. 4):
445
Линии
Функция PolyPolyline позволяет нарисовать несколько ломаных линий за один вызов. В ее последнем параметре nCount вместо количества вершин передается количество ломаных. Третий параметр, IpdwPolyPoints, представляет собой массив с количествами вершин в каждой ломаной. Функция PolyPolyline рисует всю фигуру в целом; она не сводится к простому вызову Polyline для каждой ломаной. Различия проявляются при использовании перекрывающихся ломаных и таких растровых операций, как R2_XORPEN. Если фигура рисуется одним вызовом, каждый пиксел прорисовывается всего один раз; в противном случае перекрывающиеся пикселы прорисовываются многократно, а их цвет обычно отличается от цвета пикселов при однократной прорисовке. На растровых устройствах (таких, как экран монитора или принтер) пикселы, образующие линию, выбираются при помощи специальных алгоритмов DDA (Digital Differential Analyzer). Примером классического алгоритма DDA является алгоритм Брезенхэма (Bresenham). Для представления поверхности физического устройства графический механизм Windows NT/2000 использует координаты с фиксированной точкой в формате 28.4. Линии рисуются по так называемому алгоритму GIQ (Grid Intersection Quantization), при котором каждый пиксел окружается воображаемым ромбом величиной 1 x 1 пиксел. Пиксел рисуется, если линия имеет общие точки с этим ромбом. Функция LineDDA позволяет передать координаты каждой точки, которую GDI собирается выводить, функции косвенного вызова, указанной приложением: BOOL LineDDAOnt nXStart, int nYStart. int nXEnd. int nYEnd. LINEDDAPROC IpLineProc. LPARAM IpData): Прототип LineDDA не производит особого впечатления. Среди параметров функции нет ни контекста устройства, ни логического пера. Следовательно, LineDDA возвращает точки в одной системе координат с параметрами, без учета специфики физической системы координат и стилей пера. В листинге 8.3 показано, как функции LineTo, Polyline и LineDDA используются для рисования равносторонних треугольников. Результат изображен на рис. 8.9. Поскольку мы используем толстое геометрическое перо, соединения на первом рисунке (функция LineTo) выглядят уродливо. На втором рисунке (функция Polyline с тремя отрезками) нарушено лишь последнее соединение. На третьем рисунке добавлен четвертый отрезок, обеспечивающий идеальную стыковку во всех точках. На четвертом рисунке показано, как при помощи функции LineDDA разместить вдоль базовой оси треугольника несколько маленьких треугольников. Функция косвенного вызова LineDDAProc рисует маленький треугольник при каждом 32-м вызове. Листинг 8.2. Рисование линий функциями LineTo, Polyline и LineDDA
(
void CALLBACK LineDDAProc(int x. int y. LPARAM IpData) { HDC hDC = (HDC) IpData: 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. Линии и кривые
cur.x ++; MoveToExChDC. cur.x, cur.у, NULL):
Предположим, у нас имеются точки Р1 и Р2 на плоскости и отрезок, соединяющий эти точки. Пусть переменная t принимает значения от 0 до 1; точка P12(t) на отрезке Р1-»Р2 определяется по формуле
void KMyCanvas::TestLine2(HDC hDC)
P12(t) = (l-t)Pl + tP2
LOGBRUSH logbrush = { BS_SOLID. RGB(0, 0. OxFF), 0 }: HPEN hPen = ExtCreatePen(PS_GEOMETRIC | PSJOLID | PSJNDCAPJLAT | PS_JOIN_MITER. 15, & logbrush. 0. NULL): HGDIOBJ hOld = SelectObjecUhDC. hPen); // Нарисовать треугольник несколькими вызовами LineTo SetViewportOrgEx(hDC. 100. 50. NULL): LinethDC, 0. 0. 50. 86): LineTo(hDC. -50. 86): LineTo(hDC. 0. 0); // Использование функции Polyline SetViewportOrgEx(hDC. 230. 50. NULL); TrianglethDC. 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): SelectObjecUhDC, hOld); DeleteObjectChPen); hPen = ExtCreatePen(PS_GEOMETRIC | PS_DOT SelectObjecUhDC. hPen):
447
Кривые Безье
Листинг 8.2. Продолжение
{
Кривые Безье
PS_ENDCAP_ROUND, 3, & logbrush, 0, NULL):
LineDDA( 0. 0. 50. 86. LineDDAProc. (LPARAM) hDC); LineDDAC 50. 86. -50, 86, LineDDAProc, (LPARAM) hDC); LineDDA(-50. 86. 0, 0. LineDDAProc. (LPARAM) hDC): SetViewportOrgExthDC. 50. 150, NULL): SelectObjecUhDC. hOld): DeleteObjecUhPen);
ЛАА
A-"."' •••••••'.
Рис. 8.9. Рисование треугольников функциями LineTo, Polyline и LineDDA
Отрезок Р1->Р2 образуется значениями функции P12(t) при изменении t от О до 1. Если добавить на плоскость еще одну точку РЗ, мы можем определить P12(t) как точку между Р1 и Р2, а Р23(£) — как точку между Р2 и РЗ. Если теперь применить аналогичный метод для определения P1223(f) как точки между P12(t) и Р23(0, мы получим: P12(t) = (l-t)Pl + tP2 P23(t) - (l-t)P2 + tP3 P1223(t) = (l-t)(l-t)Pl +• tP2) + U(l-t)P2 + tP3) A = (l-tr2Pl + 2t(t-l)P2 + t 2P3 Точки, описываемые функцией P1223(t) rfpn изменении t от О до 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 P1223(t) = (l-t)*2Pl + 2t(t-l)P2 + tx2P3 P2334(t) = (l-t)A2P2 + 2t(t-l)P3 + Г2Р4 P(t) = (1-trSPl + 3(l-tT2tP2 + 3(1-1)Г2РЗ + ГЗР4 Процесс, проиллюстрированный на рисунке, обычно называется алгоритмом де Кастело. Точки Р1, Р2, РЗ и Р4, задающие кривую Безье, называются ее определяющими точками. Точки Р1 и Р4 называются конечными, а точки Р2 и РЗ — контрольными. Кривые Безье обладают рядом интересных свойств, из-за которых они широко используются в системах автоматизированного проектирования и в производстве. О Аффинная инвариантность. Кривые Безье в результате аффинных преобразований, используемых GDI при отображении из мировой системы координат в страничную, переходят в кривые Безье. Следовательно, графическому механизму остается лишь преобразовать определяющие точки и нарисовать кривую в координатах устройства по преобразованным точкам.
448
Глава 8. Линии и кривые
рг
449
Кривые Безье
double A = y4 - yl; double В = xl - x4; double С = yl * (x4-xl) - xl * ( y4-yl); // Ax + By + С = 0 - линия (xl.yl) - (x4,y4)
Р23
double AB = A * A + B * B ; // Расстояние от (х2.у2) до линии меньше 1 // Расстояние от (хЗ.уЗ) до линии меньше 1
P1Z
Р34
MoveToEx(hDC. (int)xl. (int)yl. NULL): LineTo(hDC. (int)x4. (int)y4); return:
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)
double x!2 double у 12 double x23 double y23 double x34 double y34
= xl+x2: = yl+y2; = x2+x3: = y2+y3: = x3+x4: - y3+y4:
double xl223 double у 1223 double x2334 double y2334
= = = =
double x double у
= x!223 + = y!223 +
x!2+x23: y!2+y23: x23+x34: y23+y34;
BezierthDC. xl. yl. x!2/2. y!2/2. X1223/4. y!223/4. x/8. y/8); Bezier(hDC, x/8. у/8. Х2334/4. y2334/4. x34/2. y34/2, x4. y4): Давайте познакомимся с двумя функциями GDI, обеспечивающими поддержку кривых Безье. BOOL PolyBezier (HDC hDC. CONST POINT * Ippt. DWORD cPoirrts); BOOL PolyBezierTo(HDC hDC. CONST POINT * Ippt. DWORD cCount):
Обе функции рисуют несколько кривых Безье за один вызов. Для рисования п кривых функция PolyBezier получает 3 x п + 1 точек в массиве, на который ссылается указатель Ippt; при этом параметр cPoints должен быть равен 3 x n + 1. Первые четыре точки lppt[0], lppt[l], lppt[2] и lppt[3] определяют первую кривую, lppt[3] со следующими тремя точками — вторую кривую и т. д. Функция PolyBezier не использует и не обновляет текущей позиции пера. Функция PolyBezierTo рисует, начиная с текущей позиции пера, и переводит ее в последнюю точку, переданную в параметрах функции. Для рисования п кривых функция PolyBezierTo должна получить 3 x n точек. На рис. 8.10 изображена обычная кривая Безье с двумя контрольными точками, расположенными по одну сторону от линии Р1—»Р4; координаты х всех четырех контрольных точек упорядочены по возрастанию. Изменение положения контрольных точек приводит к кардинальному изменению внешнего вида кривой. В следующем фрагменте (листинг 8.4) рисуется последовательность из пяти кривых Безье с использованием функции PolyBezier.
450
Глава 8. Линии и кривые
С точки зрения применения перьев функции PolyBezier и PolyBezierTo аналогичны Polyline и PolylineTo. Простые перья используют режим заполнения и цвет фона для стилевых линий, геометрические и косметические перья всегда рисуют в прозрачном режиме, не обращая внимания на цвет и режим заполнения. При рисовании нескольких кривых внешний вид стыков определяется атрибутом соединения, конечные точки снабжаются завершениями, а весь вывод выполняется за одну операцию, при этом никакие части изображения не рисуются дважды.
Листинг 8.4. Рисование серии кривых Безье с использованием функции PolyBezier HPEN hRed = CreatePen(PS_DOT. 0, RGB(OxFF. 0. 0 ) ) ; HPEN hBlue = CreatePen(PS_SOLID, 3, RGB(6. 0. OxFF)): for (int z=0; z<=200: г+=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.
x+150.y}; x+150.y}; x+150,y}; x+150.yj; x+75.y};
x+=160: x+=180; x+=200: x+=180:
PolyDraw
SelectObjectfhDC. hRed): Polyline(hDC. p. 4); Polyline(hDC, q, 4 ) ; Polyline(hDC, r, 4); Polyline(hDC, s, 4): Polyline(hDC. t, 4);
Функции PolyBezier и PolyBezierTo рисуют только непрерывные кривые Безье. В Windows NT/2000 GDI появилась новая функция PolyDraw, обладающая расширенными возможностями — она позволяет рисовать разъединенные линии, кривые Безье и даже замкнутые фигуры.
SelectObjecKhDC. hBlue): Polyline(hDC, p, 4) : Polyline(hDC, q, 4): Po1yline(hDC, r, 4): Polyline(hDC. s, 4): Polyline(hDC. t. 4) :
BOOL PolyDrawtHDC hDC. CONST POINT * Ippt. CONST BYTE * IpbTypes. int nCount):
Функция PolyDraw получает два массива: параметр Ippt содержит массив точек, а параметр 1 pbTypes — массив типов точек (по одному элементу для каждой точки первого массива). Пять допустимых типов точек перечислены в табл. 8.4.
} SelectObject(hDC. GetStockObject(BLACK_PEN)): DeleteObject(hRed): DeleteObject(hBlue);
Этот фрагмент показывает, как определяются кривые Безье разной формы. У кривых первой группы контрольные точки лежат на одной стороне от линии, а у кривых второй группы они лежат на разных сторонах. В третьей группе значения координат х двух контрольных точек меняются местами, а четвертая группа совмещает признаки второй и третьей. Наконец, в последней группе позиции двух конечных точек совпадают. Результат работы этого фрагмента с пояснительными метками показан на рис. 8.11. ., РЗ
PZ,
РЗ...
, Р2
PI
РЗ
451
Кривые Безье
/ РЗ
Рис. 8.11. Галерея кривых Безье
Таблица 8.4. Типы точек, используемые функциями PolyDraw и GetPath Тип
Описание
РТ MOVETO
Начать новую отдельную фигуру. Текущая позиция пера перемещается в заданную точку
РТ LINETO
Провести линию от текущей позиции к заданной точке и обновить текущую позицию
PT_LINETO|PT_CLOSEFIGURE
То же, что и PT_LINETO, с замыканием фигуры от заданной точки к последней позиции PT_MOVETO
РТ BEZIERTO
Нарисовать кривую Безье, используя текущую позицию пера и три точки массива, начиная с заданной. У двух следующих точек также должен быть установлен флаг PT_BEZIERTO. Текущая позиция пера перемещается в третью точку
PT_BEZIERTO|PT_CLOSEFIGURE
Последняя точка PT_BEZIERTO соединяется с последней позицией PT_MOVETO
-РЗ.
Функция PolyDraw обладает некоторыми выдающимися возможностями. Вопервых, она позволяет комбинировать кривые Безье с прямыми линиями. Хотя любую прямую можно преобразовать в кривую Безье, добавив две контрольные точки на самой линии, подобные преобразования выглядят неестественно. Вовторых, PolyDraw позволяет нарисовать несколько отдельных фигур за один вызов. Как говорилось выше, рисование нескольких линий или кривых за один вызов функции гарантирует, что ни один пиксел не будет прорисован дважды,
452
Глава 8. Линии и кривые
что бывает существенно при рисовании сложных фигур с использованием режима R2_XORPEN в графическом пакете. В-третьих, функция PolyDraw позволяет замыкать фигуры автоматическим соединением конечной точки с начальной, что весьма удобно для приложений. При рисовании замкнутых фигур геометрическим пером завершения не используются; даже конечная точка оформляется соединением, чтобы фигура выглядела более гладкой. . Фрагмент кода, приведенный в листинге 8.5, двумя разными способами рисует фигуру, состоящую из горизонтальной «восьмерки» и ромба. Для вывода используется утолщенное однородное геометрическое перо с плоским завершением и заостренным соединением; рисование осуществляется в режиме R2_XORPEN. В первой части программы «восьмерка» рисуется функцией 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_MOVETO. PT_BEZIERTO, PTJEZIERTO, PT_BEZIERTO, PTJEZIERTO. PT_BEZIERTO. PT_BEZIERTO | PT_CLOSEFIGURE. PT_MOVETO. PTJ.INETO. PTJJNETO. PT_LINETO. PT LINETO PT CLOSEIGURE SetROP2(hDC. R2JORPEN): LOGBRUSH logbrush = { BS_SOLID, RGBCOxFF. OxFF, 0). 0 }: HPEN riPen = ExtCreatePen(PS_GEOMETRIC | PSJOLID | PS_ENDCAP_FLAT| PS_JOIN_MITER. 15. & logbrush, 0, NULL): SelectObject(hDC. hPen); PolyBezierfhDC. P. 7); // Две кривые Безье Polyline(hDC, P+7. 5); // Ромб SetViewportOrgExfhDC. 200. 0. NULL); PolyDraw(hOC, P. T, 12): // Обе фигуры SetViewportOrgExthDC, 0. 0, NULL): SelectObject(HOC, GetStockObject(BLACK_PEN)): DeleteObject(hPen): К сожалению, в Windows 95/98 эта замечательная функция не поддерживается. В статье Q135059 MSDN Knowledge Base приведена возможная реализация PolyDraw для Windows 95, основанная на использовании функций MoveToEx, LineTo и PolyBezierTo. Предлагаемый код обладает рядом недостатков — он не реа-
Кривые Безье
453
лизует PT_CLOSEFIGURE, использует множественные вызовы функций, приводящие к многократной прорисовке фрагментов изображения, и применяет завершения для геометрических перьев. В правильной реализации следует воспользоваться траекториями GDI, которые могут состоять из нескольких отдельных фигур с завершениями и соединениями. Объекты траекторий рассматриваются ниже в этой главе.
Рис. 8.12. Использование функции PolyDraw при рисовании замкнутых фигур
Альтернативное определение кривых Безье В стандартном определении кривой Безье используются две конечные и две контрольные точки. Такое определение весьма наглядно с геометрической точки зрения, поэтому оно хорошо подходит для интерактивных манипуляций. Однако с точки зрения программиста кривую было бы удобнее определять по точкам, расположенным на кривой, без контрольных точек. В частности, для кубических кривых Безье часто возникает вопрос — как определить по четырем точкам А, В, С, D кривую, которая бы проходила через точку А при t = О, через точку В при t = 1/3, через точку С при t = 2/3 и через точку D при t = 1? Проблема сводится к вычислению по известным А, В, С и D четырех точек Р1, Р2, РЗ и Р4, где точки Р1 и Р4 находятся на концах кривой, а точки Р2 и РЗ соответствуют контрольным точкам. В соответствии с параметрическим определением кривой Безье мы получаем следующую систему линейных уравнений: А В С D
= = = =
Р1 (2/ЗГЗ Р1 + 3(2/ЗГ2(1/3) Р2 + 3(2/3)(1/ЗГ2РЗ + Р4 (1/ЗГЗ Р1 + 3(1/2)^2(2/3) Р2 + 3(1/3)(2/ЗГ2РЗ + Р4 Р4
С точками Р1 и Р4 все просто. Уравнения для точек В и С преобразуются к виду: 12Р2 + 6РЗ - 27В - 8А - D 6Р2 + 12РЗ = 27С - А - 80
Решая эту систему уравнений для переменных Р2 и РЗ, мы получаем решение: PI = A
Р2 = (- 5А + 18В - 9С + 20)/6 ' РЗ = ( 2А - 9В + 18С - 5D) / б Р4 > D
454
Глава 8. Линии и кривые
Эти формулы также убедительно доказывают, что для любых четырех точек на плоскости существует кривая Безье, проходящая через них при t = О, 1/3, 2/3 и 1.
455
дуги
Arc, против часовой стрелки
АгсТо, по часовой стрелке
Слева, сверху
Слева, сверху (xStart, yStart)
(xStart, yStart)
ixEnd, yEnd):
(xEnd, yEnd)
AngleArc
StartAngle
Дуги Из всех кривых наибольшей известностью пользуется эллипс, частным случаем которого является окружность. В простейшем случае оси эллипса параллельны осям координат. Эллипс определяется следующим уравнением: (х-хОГ2/а А 2 + (у-уОГ2/Ь"2 = 1
Здесь (хО,уО) — центр эллипса, а а и b — главная и вспомогательная оси. Благодаря простоте этого уравнения эллипсы или любые фигуры, созданные на их основе, очень просто представляются в GDI. В GDI эллипс определяется ограничивающим прямоугольником (хО - а, уО - Ь, хО + а, уО + Ь). Дуга представляет собой полный периметр эллипса или его часть. Часть периметра эллипса проще всего определяется по значениям начального и конечного углов; в этом случае дуга определяется как совокупность точек эллипса, лежащих в интервале от начального до конечного угла. Чтобы избежать вычислений с плавающей точкой, но при этом обеспечить необходимую точность, GDI использует две опорные точки (xStart,yStart) и (xEnd,yEnd), по которым легко вычисляется начальная и конечная точки дуги. Начальная точка дуги является пересечением линии, соединяющей центр эллипса (xStart.yStart) с периметром эллипса; аналогично, конечная точка дуги находится в пересечении линии «центр эллипса — (xEnd,yEnd)» с периметром эллипса. Следующие три функции GDI предназначены для рисования дуг: BOOL Arc(HDC hDC, int nleft, int nTop. int nRight. int nBottom. int xStart. int nYStart. int nXEnd, int nYEnd); BOOL ArcTo(HDC hDC. int rteft. 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 направление дуги определяется флагом контекста устройства (GetArcDirection, SetArcDirection). Первые две дуги на рис. 8.13 поясняют смысл параметров Агс/АгсТо и показывают, как направление рисования влияет на вывод. Функция Arc не использует и не обновляет текущую позицию пера. С функцией АгсТо дело обстоит несколько иначе — она проводит линию из текущей позиции пера в настоящую начальную точку дуги, после чего рисует дугу. После завершения дуги текущая позиция пера перемещается в конечную точку дуги.
eStartAngle + eSweepAngle
Снизу, справа
Снизу, справа
Снизу, справа
Рис. 8.13. Определение дуг в GDI
Функции Arc и АгсТо позволяют нарисовать полный периметр эллипса; для этого достаточно задать конечную точку, совпадающую с начальной. Чтобы нарисовать часть эллипса, не входящую в заданную дугу, достаточно поменять местами начальную и конечную точки. На случай, если вам вдруг понадобится вычислить позицию начальной или конечной точек, ниже приведены формулы для начальной точки: ХО - (nLeft + nRight)/2; // Центр эллипса YO = (nTop + nBottom)/2;
DXs • nXStart - ХО: OYs - 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 градусов, дуга рисуется несколько раз. Отсюда следует, что в режиме R2JORPEN 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 pioverlSO - (FLOAT) 3.141592653586/180: if ( eSweepAngle >= 360) // Если угол больше 360 градусов eSweepAngle = 360; // оставить полную окружность else if (eSweepAngle <= -360 ) eSweepAngle = -360: FLOAT eEndAngle = (eStartAngle + eSweepAngle ) *.pioverlSO: eStartAngle = eStartAngle * pioverlSO; int dir; // Угловой размер дуги определяет направление if (eSweepAngle > 0) dir = SetArcDirection(hDC. AD_COUNTERCLOCKWISE); else dir = SetArcDirection(hDC. AD_CLOCKWISE); // Угол задается в системе координат устройства 800L 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))): SetArcDirection(hDC. dir): return rslt:
Функция удостоверяется в том, что размер рисуемой дуги не превышает полной окружности, преобразует градусы в радианы, задает направление дуги в соответствии со знаком углового размера, вычисляет начальную и конечную точки (при этом радиус круга умножается на 10 для повышения точности) и затем рисует дугу функцией АгсТо.
Рисование дуг пером со стилем PS_INSIDEFRAME Поскольку дуги ограничиваются прямоугольниками и внутренняя сторона дуги легко отличается от внешней, GDI учитывает стиль пера PS_INSIDEFRAME. Если перо с этим стилем используется для рисования дуги, центральная линия кривой смещается внутрь на половину толщины пера. Внешние пикселы линии соприкасаются с ограничивающим прямоугольником, а остальные пикселы рисуются внутри прямоугольника. При выводе дуг применение пера со стилем PS_ INSIDEFRAME реализуется очень просто — достаточно уменьшить ограничивающий прямоугольник на половину толщины пера. Сделать то же самое в общем случае (например, для замкнутой серии кривых Безье) гораздо сложнее.
457
Дуги
Следует заметить, что стиль пера PSJNSIDEFRAME определяется на том же уровне, что и PS_SOLID или PS_DOT и не является атрибутом линии, как, например, атрибуты завершения и соединения. В результате внутренние перья всегда являются сплошными. Вероятно, это свойство следовало бы реализовать как независимый атрибут пера.
Преобразование дуг в кривые Безье Вероятно, вы уже поняли, что рисование кривых является непростой задачей. Чтобы определить, какую часть периметра эллипса необходимо нарисовать, приходится использовать вычисления с плавающей точкой и разбираться с атрибутом направления дуг. Еще хуже то, что вы можете рисовать части только таких эллипсов, у которых оси параллельны осям логической системы координат. Если вы захотите нарисовать дугу после поворота или сдвига, вам придется вычислить нужное преобразование, причем в ОС, не входящих в семейство NT, эта возможность не поддерживается. На помощь приходят кривые Безье. Операции с ними очень просты, а вычисления в процессе рисования достаточно элементарны. В результате аффинных преобразований кривая Безье отображается в кривую Безье, причем результат всегда однозначен, поскольку четыре точки определяют ровно одну кривую. Остается единственный вопрос — как построить кривую Безье, аппроксимирующую эллиптическую дугу? Аппроксимация полной окружности одной кривой Безье обладает слишком высокой погрешностью. Даже представление половины окружности одной кривой Безье не гарантирует достаточной точности. Давайте попробуем вычислить позиции контрольных точек для кривой Безье, аппроксимирующей четверть окружности (90 градусов). Для четверти единичной окружности, расположенной в первом квадранте декартовой системы координат, нам известны конечные точки (0,1) и (1,0). 90-градусная дуга симметрична относительно линии X = Y, поэтому две контрольные точки тоже должны быть симметричными. Мы знаем, что линия, проведенная из начальной точки в первую контрольную точку, является касательной к дуге в начальной точке. Это означает, что линия должна проходить под углом 0 градусов, то есть первая контрольная точка должна иметь координаты (т,1) для неизвестной переменной т. По свойству симметрии вторая контрольная точка должна иметь координаты (\,т). Итак, нам известны все четыре контрольные точки Р1:(0,1), Р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-tr2tP2 + 3(l-t)t*2P3 + t"3P4 - (PI + 3P2 + 3P3 + P4)/8
458
Глава 8. Линии и кривые
459
Дуги
способ. Вторая функция, AngleArcToBezier, рисует дугу с произвольными начальным и конечным углами, аппроксимируя ее одной кривой Безье.
(0-1)
Листинг 8.6. Рисование периметра эллипса с использованием кривых Безье BOOL E11ipseToBezier(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); m=0.0, error(0.499) = -29.28900% m=0.2, error(0.499) = -18.68254% m=0.4, error(0.499) = -8.07605% m=0.6, error(0.499) = +2.53046% m=0.8, error(0.499] = +13.13699% m=1.0, error(0.499] = +23.74352% 1 [1-0)
m=0.552285, error(0.211] = +0.027253% Рис. 8.14. Преобразование 90-градусной дуги в кривую Безье
Если вычислить по этой формуле среднюю точку кривой (0,1), (тк,1), (1,т) и (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 определяющих точек для четырех кривых Безье, используя описанный выше
PC 0 ] . x = right; PC 0].y = (top+bottom)/2: PC l].x = right; PC 1].У = top + dy: PC 2].x = right - dx: PC 2].у = top; PC 3].x = (left+right)/2: PC 3].y - top:
// // // // // // // //
PC 4].x = left + dx; PC 4].у - top: PC 5].x = left; PC 5].у = top + dy; PC 6].x = left: PC 6].у = (top+bottom)/2:
//
4
3
2
5
1
6
0.12 11
7 8
9
10
PC 7].x = left: PC 7].у = bottom - dy; PC 8].x = left + dx; PC 8].у - bottom; P[ 9].x = (left+right)/2: PC 9].у = bottom:
P[10].x = right - dx; P[10].y = bottom: РСШ.х = right: P[ll].y - bottom - dy; PC12].x = right: PC12].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 XYC8]: POINT P[4]; \ i
// Рассчитать'кривую Безье для дуги.
Продолжение:
460
Глава 8. Линии и кривые
Листинг 8.6. Продолжение // симметричной относительно оси х // Против часовой стрелки: (О, -В), (х'. -у), (х.у). (О,В) double В = гу * sin(sweepangle/2): double С = гх * cos(sweepangle/2): double A = гх
- С:
XY[0] = С: XY[1] = - В; XY[2] = C+X XY[3] = - Y; XY[4] = C+X XY[5] = Y: XY[6] = C; B:
// Вернуться к исходному углу double s = sin(startangle + sweepangle/2); double с = cos(startangle + sweepangle/2);
for (int i=0; i<4; { P[i].x = xO (int) (int) P[i].y = yO
(XY[i*2] (XY[i*2]
461
лем PS_INSIDEFRAME работает так же, как и обычные перья со стилем PS_SOLID. Если вы хотите, чтобы линия размещалась внутри контура, уменьшите ограничивающий прямоугольник перед тем, как выполнять преобразование.
Траектории
double X = A*4/3: double Y = В - X (гх-А)/В;
XY[7] =
Траектории
- XY[i*2+l] * s): + XY[i*2+l] * c):
return PolyBezierthDC. P, 4 ) -
}
Метод преобразования дуги в кривую Безье, используемый функцией E l l i p s e ToBezier, не подходит для произвольных дуг; он предназначен для аппроксимации дуг с угловым размером, кратным 90°. Функция AngleArcToBezier использует новый метод аппроксимации произвольной дуги кривой Безье, хотя в тех случаях, когда угловой размер превышает 90°, следует ожидать увеличения ошибок. Функция сначала поворачивает дугу, обеспечивая ее симметричность относительно оси х; в результате половина дуги находится выше линии у = 0, а другая половина — ниже этой линии. Такое положение дуги существенно упрощает формулы для вычисления контрольных точек. После вычисления контрольные точки поворотом возвращаются в исходную позицию. С увеличением углового размера дуги значительно возрастает относительная погрешность аппроксимации. При угле 45° погрешность равна 0,00042 %; при 90° она возрастает до 0,02725 %, а при 180° ошибка составляет уже 1,835 %. Однако настоящая «прелесть» аппроксимации дуг кривыми Безье заключается в том, что с контрольными точками кривой можно выполнять преобразования и рисовать нестандартные дуги без применения мировых преобразований GDI, поддерживаемых только на платформах семейства Windows NT. Кроме того, свойство делимости кривых Безье позволяет легко реализовать новые стили линий, не поддерживаемые в GDI и в семействе Windows 95/98. Также обратите внимание на то, что при преобразовании дуги в кривые Безье перо со сти-
При выводе линий и кривых средствами GDI API необходимы два источника информации — перо и другие атрибуты контекста, определяющие внешний вид линий и кривых, а также геометрическое описание (то есть координаты точек, через которые проходит линия, и их типы). Хорошо спроектированная графическая система должна по отдельности обрабатывать эти два информационных компонента, чтобы обеспечить максимальную гибкость. Для работы с перьями используются объекты перьев GDI, а объекты контекста устройства управляют другими внешними атрибутами линий и кривых. Остается лишь найти средства для геометрического бписания линий. Так мы приходим к объектам траекторий. Траекторией (path) называется объект GDI, описывающий геометрические формы. Точнее, траектория описывает упорядоченную последовательность замкнутых или разомкнутых фигур, которые представляют собой упорядоченные последовательности линий и кривых. Объекты траекторий принадлежат к числу объектов GDI, поэтому каждой траектории соответствует манипулятор GDI и запись в таблице объектов GDI. Однако в отличие от других стандартных объектов GDI (логических перьев, контекстов устройств и т. д.), объекты траекторий остаются скрытыми от пользователей. На уровне GDI объект траектории всегда связывается с контекстом устройства и не может существовать отдельно от него. Создание, модификация, выбор, использование и уничтожение траектории выполняется GDI при вызове специальных функций. Для работы с траекториями в GDI существуют следующие функции: BOOL BeginPath(HDC 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 StrokePath(HDC hDC): (BOOL StrokeAndFillPath(HDC hDC): 'BOOL FillPath(HDC hDC): ' HRGN PathToRegion(HDC hDC): BOOL SelectClipPathtHDC hDC. int iMode);
Построение траектории Прежде чем использовать траекторию, сначала ее необходимо построить. Функция BeginPath инициирует построение траектории в контексте устройства. КонТекст переходит в реж«м построения траектории, сбрасывает объект траектории, неявно связанный с контекстом, и инициализирует его пустым объектом. Когда контекст устройства находится в режиме построения траектории, все функции,
462
Глава 8. Линии и кривые
которые могут потребоваться при построении траектории, не рисуют в контексте устройства; вместо этого их вызовы добавляются в объект траектории, связанный с контекстом устройства. При построении траектории могут использоваться только функции GDI, создающие линии и кривые; вызовы остальных функций передаются на поверхность устройства. В табл. 8.5 перечислены функции, используемые при конструировании траекторий. Обратите внимание: функции заполнения областей и даже функции вывода текста тоже генерируют линии и кривые, поэтому они включаются в траекторию. Таблица 8.5. Функции построения траекторий
Траектории
скольку траектория отражает лишь геометрическую форму линий и кривых; исключение составляют функции рисования дуг пером со стилем PS_INSIDEFRAME, при использовании которых ограничивающий прямоугольник дуги уменьшается на половину толщины пера. Обратите внимание: в системах, не входящих в семейство NT, функции PolyDraw, Arc, ArcTo и AngleArc либо не реализованы, либо их использование при построении траекторий недопустимо. Чтобы ваша программа работала на разных платформах, при включении таких фигур в траекторию их можно преобразовать в кривые Безье. Вторую категорию функций построения траекторий составляют функции GDI, выполняющие заливку областей, — например, функции Ellipse, Chord, Pie, Polygon, Rectangle и RoundRect. В общем случае эти функции закрашивают кистью замкнутую область и обводят ее контур пером. Подробности будут рассмотрены в главе 9, а пока достаточно запомнить, что эти функции определяют замкнутые геометрические фигуры (одну или несколько). Когда указанные функции вызываются в контексте устройства, находящемся в режиме построения траектории, эти геометрические фигуры включаются в текущую траекторию этого контекста. Третью категорию функций построения траекторий составляют функции вывода текста. В Windows используются три типа шрифтов: растровые (описываются растровыми изображениями), векторные (описываются прямыми линиями) и шрифты TrueType, описываемые линиями и кривыми Безье. Если в режиме построения траектории вызывается функция вывода текста для векторного шрифта или шрифта TrueType, контур глифа включается в текущую траекторию. Помимо этих трех категорий функций, в GDI существует специальная функция CloseFigure. Вся ее работа сводится к тому, что последняя точка помечается флагом замыкания. Пометка указывает на то, что эта точка должна быть соединена линией с начальной точкой фигуры, причем для геометрического пера стык должен быть оформлен в соответствии с атрибутом соединения, а не завершения. Рассмотрим небольшой пример построения траектории. В следующем фрагменте рисуются два эллипса с одинаковыми координатами; первый эллипс рисуется пером по умолчанию, а второй - пером толщиной в 21 единицу со стилем PSJNSIDEFRAME. Если перо по умолчанию не совпадает с внутренним пером толщиной в 21 единицу, построенная траектория состоит из двух концентрических кругов.
Функция
Описание
Ограничение
AngleArc, Arc, ArcTo
Добавляет в траекторию линию и дугу (функция Arc — только дугу)
Только в семействе NT
Ellipse, Chord, Pie
Добавляет полный периметр эллипса, сектор или сегмент
Только в семействе NT
CloseFigure
Последняя точка помечается флагом замыкания фигуры
ExtTextOut, TextOut
Контуры символов добавляются как отдельные замкнутые фигуры
LlneTo
Добавляет линию
MoveToEx
Начинает новую фигуру
PolyBezier, PolyBezierTo
Добавляет линию и несколько кривых Безье (функция PolyBezier — только кривые Безье)
PolyOraw
Добавляет серию фигур
Polygon, PolyPolygon
Добавляет один или несколько многоугольников как отдельные замкнутые фигуры
Polyline, PolylineTo, PolyPolyline
Добавляет одну или несколько ломаных
BeginPath(hDC): Ellipse(hDC. 0. 0. 100, 100):
Rectangle
Добавляет прямоугольник как новую замкнутую фигуру
HPEN hPen = CreatePen(PSJNSIDEFRAME. 21, RGBCOxFF, 0, 0)): HPEN hOld = (HPEN) SelectObjectthDC. hPen):
RoundRect
Добавляет замкнутую фигуру из четырех линий и четырех дуг
Не поддерживается для растровых шрифтов
Только в семействе NT
Только в семействе NT
Все функции, перечисленные в таблице, так или иначе выводят кривые и линии, включаемые в объект траектории. Функции вывода пикселов (например, SetPixel) линий не выводят, поэтому в таблице они отсутствуют. Понять, как при построении траектории используются такие функции, как LineTo, Polyline, PolylineTo, PolyPolyline, PolyBezier, PolyBezierTo, PolyDraw, Arc, ArcTo и AngleArc, очень просто. Объекты перьев обычно не влияют на построение траекторий, по-
463
Ellipse(hDC. 0. 0, 100, 100); SelectObject(hOC, 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 вызывается снова для получения настоящих данных траектории. Функция MarkPoints рисует рядом с каждой точкой небольшой маркер, обозначающий ее тип.
465
Траектории
class KPathData { public: POINT * m_pPoint: BYTE * m_pFlag: int mjiCount: KPathDataО
{
m_pPoint = NULL; mjpFlag = NULL: m nCount = 0;
-KPathDataО { if ( m_pPoint ) delete m_pPoint; if ( m_pFlag ) delete m_pFlag: } int GetPathDataCHDC 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 ) mjiCount = ::GetPath(hDC. m_pPoint. m_pFlag. mjiCount): else mjiCount = 0:
} return mjiCount; }
void MarkPoints(HDC hDC. boo! bShowLine=true);
}: Функция GetPath помогает разобраться в том, как в GDI реализованы различные функции вывода линий и кривых, поскольку с ее помощью можно увидеть данные траектории. Например, если вы хотите узнать, как AngleArc преобразуется в кривые Безье, попробуйте выполнить следующий фрагмент: BeginPath(hDC): MoveToEx(hDC. 0. 0. NULL); AngleArcChDC, 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. Начальная точка фигуры помечена треугольником, точки линий — прямоугольниками, точки кривых Безье — кругами, а точка замыкания фигуры обозначается закрашенным маркером.
О"
о
.„о
о
-о
д--
Рис. 8.15. Траектория и данные траектории, возвращаемые функцией GetPath
Из рисунка видно, что AngleArc проводит линию от текущей позиции пера (0,0) в начальную точку дуги (150,0); 135-градусная дуга представляется двумя кривыми Безье. Первая кривая Безье рисует начальную 90-градусную дугу, а вторая кривая рисует оставшиеся 45°. Также рисунок показывает, что функция CloseFigure не включает дополнительный отрезок в данные траектории, а всего лишь устанавливает флаг для последней точки. В документации Microsoft нет четкого определения того, где должен находиться флаг PT_CLOSEFIGURE. Иногда он ошибочно приписывается первой контрольной точке кривой Безье. Но если проанализировать данные, возвращаемые GetPath, все становится абсолютно ясно — флаг PT_CLOSEFIGURE должен устанавливаться для последней точки фигуры. Данные, полученные от GetPath, можно непосредственно передать функции PolyDraw, как это было сделано для построения левой части рисунка. Это чрезвычайно полезная возможность, особенно если учесть, что GDI не предоставляет приложениям манипулятора траектории. Приложение может сохранить данные траектории и использовать их в будущем. Если вы хотите нарисовать точки вместо кривых (например, с целью редактирования), возвращаемый массив точек можно передать функции Polyl ine, как это было сделано на правой части рисунка. Перед тем как передавать данные GDI, их можно подвергнуть любым преобразованиям. Помните о том, что GDI поддерживает только аффинные преобра-
Траектории
467
зования в системах семейства NT, а в других системах мировые преобразования вообще не поддерживаются. Реализовать преобразования для рисования линий и кривых несложно. Для аффинных преобразований, отображающих линии в линии, достаточно преобразовать все контрольные точки, а затем провести вывод с теми же флагами. Для не-аффинных преобразований, которые могут отображать линии в кривые, для повышения точности вам, возможно, придется разбить линии и кривые на сегменты меньших размеров.
Преобразование объекта траектории В GDI для объектов траекторий определены два преобразования: замена криволинейных участков прямолинейными и утолщение линий. Функция Fl attenPath преобразует все кривые объекта траектории в последовательность отрезков, обеспечивающих приемлемую аппроксимацию кривой в логическом координатном пространстве. Функция Fl attenPath ограничивается преобразованием кривых Безье. При этом используется рекурсивный алгоритм вроде приведенного в листинге 8.3: кривая делится надвое до тех пор, пока погрешность не станет незначительной. Название функции создает неверное впечатление, будто она приводит к значительному искажению кривой. На самом деле функция Fl attenPath обеспечивает наилучшую аппроксимацию в логических координатах. Конечно, целочисленное представление приводит к некоторой потере точности, но если результат не масштабируется с большим коэффициентом, различия практически незаметны. Если результат должен выводиться в контексте устройства с высоким разрешением, приложение может увеличить разрешение логического координатного пространства или реализовать собственную версию Fl attenPath с вещественными вычислениями. Функция Fl attenPath вызывается после того, как EndPath завершает построение действительной траектории. Она модифицирует текущий объект траектории в контексте устройства. Обновленный объект траектории остается выбранным в контексте и может использоваться другими функциями, работающими с траекториями (например, функцией GetPath). При достаточно сложной форме траектории аппроксимация может потребовать немалых расходов памяти. Пример использования функции Fl attenPath иллюстрирует рис. 8.16. Фигура слева была нарисована функцией PolyDraw по данным, полученным в результате вызова Fl attenPath; справа выведена та же фигура с маркерами точек. Левая фигура почти не отличается от фигуры на рис. 8.15, однако при взгляде на правую фигуру становится видно, что точки кривой были заменены множеством линейных точек, Довольно точно воспроизводящих форму траектории. Задача преобразования кривых в прямые часто встречается на практике, поскольку с линиями работать гораздо удобнее, чем с кривыми. Не существует простых формул, которые бы позволяли вычислить длину кривой Безье по определяющим точкам. В системах автоматизированного проектирования пользователи работают с шаблонами, созданными на основе кривых Безье; преобразование кривой в набор отрезков позволяет легко вычислить ее длину. В результате линейной аппроксимации замкнутая фигура практически превращается в многоугольник, а для вычисления площади многоугольника, его ограничивающего прямоугольника и проверки пересечений существует немало готовых алгорит-
468
Глава 8, Линии и кривые
мов. Информация о длине кривой также применяется при рисовании стилевых линий. Процесс рисования стилевой линии можно представить как многократное чередование вывода отрезков и промежутков фиксированной длины. В разделе «Пример: рисование нестандартных стилевых линий» показано, как линейная аппроксимация траекторий помогает строить нестандартные стилевые линии.
Two Regions
RGN_AND
RGN_OR
RGN_XOR
RGN_DIFF
Траектории
469
величину уходит внутрь. Несомненно, функция WidenPath должна преобразовывать открытую траекторию в одну замкнутую фигуру, окружающую исходную траекторию на расстоянии в половину толщины пера с каждой стороны. Если бы вместо сплошного пера использовалось стилевое перо, для каждого пунктирного отрезка или точки была бы сгенерирована отдельная замкнутая фигура.
RGN_COPV
Рис. 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 преобразует замкнутую фигуру в две замкнутые фигуры — первая на половину толщины пера выходит за границы траектории, а вторая на такую же
Рис. 8.17. WidenPath преобразует одну замкнутую фигуру в две замкнутые фигуры
Траектория, сгенерированная функцией WidenPath, уже разбита на прямолинейные отрезки. Но похоже, что GDI при этом не ограничивается тривиальным вызовом FlattenPath с последующим расширением траектории. Если сделать это в приложении, сгенерированная траектория будет содержать больше точек и «петель». Уродливые петли на левом рисунке появляются при соприкосновении двух толстых линий под углом менее 180°. Они соответствуют перекрывающимся зонам, которые прорисовываются двумя линиями по отдельности. Если бы результат вызова WidenPath использовался только для реализации утолщенных геометрических линий, петли были бы полностью скрыты при закраске замкнутых фигур. Но если приложение захочет воспользоваться этим результатом для рисования двух замкнутых фигур, внутренней и внешней, ему придется основательно потрудиться над чисткой траекторий. Возможно, функция WidenPath или какая-нибудь ее внутренняя реализация используется GDI для преобразования геометрических линий в закрашиваемые области. Это также объясняет, почему при определении геометрических логических перьев используется структура LOGBRUSH, как и при определении кисти. С точки зрения GDI рисование геометрическим пером является усложненной формой заливки областей. Функция WidenPath позволяет приложению получить данные траектории, которые будут использоваться GDI для рисования геометрических линий. Впрочем, Microsoft нигде не объясняет, зачем приложению могут понадобиться эти внутренние данные. Вспомните, что говорилось выше — в GDI поддерживаются только аффинные преобразования; непосредственная поддержка не-аффинных преобразований (таких, как 1-, 2- и 3-точечные преобразования перспективы) отсутствует. Отличительной особенностью геометрических линий является наличие заметной толщины. С другой стороны, нарисованная геометрическая линия имеет постоянную толщину. Трехмерное изображение должно имитировать
470
Глава 8. Линии и кривые
эффект перспективы, поэтому удаленные объекты, в том числе и геометрические линии, должны уменьшаться в размерах. Функция WidenPath обеспечивает необходимое «разделение труда» между GDI и приложениями. Приложение может получить данные траектории после применения WidenPath, применить к ним преобразование перспективы или любое другое преобразование, изменяющее толщину линий, и вернуть полученную траекторию для ее закраски кистью. В результате вы получите линии с переменной толщиной. В следующем фрагменте определяется абстрактный класс для выполнения двумерных преобразований K2DMap и производный от него класс KBiLinearMap, отображающий прямоугольное окно в произвольный четырехугольник. Метод K2DMap:: Map выполняет отображение отдельных точек в данные, относящиеся к траектории. class K2DMap public: virtual MapOong
px. long S py) = 0:
class KBiLinearMap : public K20Map { 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, F i l l Path и StrokeAndFil 1 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_ENOCAP_ROUND | PS_JOIN_ROUND, PSJNDCAPJLAT I PS_JOIN_BEVEL
const ThinStyle[] = const Color [] =
PS_ALTERNATE . PS_DOT, PS_SOLID }: RGB(OxFF. 0, 0). RGB(0. 0. OxFF). RGB(O.O.O)
KPen wide(PS_GEOMETRIC | PS_SOLID RGB(0. 0. OxFF). 0. NULL): wide.Select(hOC);
BeginPath(hDC): MoveToExthDC. 150. 0. NULL): LineTo(hDC. 0. 0): LineTo(hDC. 100. -100); EndPath(hOC); WidenPath(hDC): wide.UnSelect(hDC):
if (i—2 )
WideStyle[i]. 70.
472
Глава 8. Линии и кривые
KPathData pd: pd.GetPathData(hDC); pd.MarkPo1nts(hDC, false): KPen thin(PS_COSMETIC thin.Select(hDC): StrokePath(hDC): thin.UnSelecL(hDC);
ThinStyle[i], 1. Color[i]. 0, NULL);
Приведенный фрагмент выполняется трижды: для пера с квадратным завершением и заостренным соединением, с закругленными завершением и соединением и, наконец, для пера с плоским завершением и усеченным соединением. Каждый раз строится траектория из двух линий, расположенных под углом 45°. Траектории расширяются и обводятся разными косметическими линиями. Для последнего пера точки расширенной траектории помечаются в соответствии с их типами. Этот фрагмент наглядно показывает, как рисуются линии с применением геометрических перьев с различными атрибутами (рис. 8.18).
Пример: рисование нестандартных стилевых линий
473
словами, при вызове таких функций, как LineTo, Arc и BezierTo вне построения траектории, в этих системах атрибуты завершения и соединения геометрических перьев игнорируются, и вместо них используются значения по умолчанию. Функция F i l l Path закрашивает область, занимаемую траекторией, при помощи кисти. Функция StrokeAndFillPath закрашивает область, как F i l l Path, и рисует линии и кривые, как StrokePath. С другой стороны, следующие подряд вызовы F i l l Path и StrokePath не заменяют StrokeAndFillPath, поскольку все эти функции перед возвращением управления уничтожают объект траектории. Функции F i l l Path и StrokeAndFillPath рассматриваются в следующей главе.
Преобразование пути в регион Остается рассмотреть еще две функции GDI, предназначенные для работы с траекториями. Функция PathToRegion преобразует текущую траекторию в контексте устройства в независимый регион, который может использоваться приложением для различных целей. Функция SelectClipPath преобразует текущую траекторию контекста устройства в регион и задействует его для обновления региона отсечения, определенного приложением в данном контексте. Мы еще вернемся к этим двум функциям после более подробного описания регионов и отсечения.
Пример: рисование нестандартных стилевых линий
Рис. 8.18. Применение функций WidenPath и StrokePath
Рисунок также наглядно иллюстрирует процесс возникновения петли при использовании функции WidenPath. Для пера с плоским завершением и усеченным соединением (на рисунке обозначено сплошной черной линией) WidenPath строит замкнутую фигуру, которая начинается с правого нижнего угла, помеченного треугольником. Траектория следует к центру другого конца линии, где она встречается с другой линией. После этого траектория переходит на вторую линию, следует по всему периметру до соединения, рисует соединение и замыкает фигуру. Петля генерируется в том случае, если две линии пересекаются под углом меньше 180°. Вероятно, алгоритму GDI следовало бы вычислить область пересечения периметров двух линий и удалить петли вместо того, чтобы просто следовать линии периметра. Для платформ, не входящих в семейство NT, функция StrokePath особенно важна для геометрических перьев, поскольку их атрибуты завершения и соединения требуются только при включении линий и кривых в траекторию. Другими
На платформах, не входящих в семейство NT, утолщенные геометрические стилевые линии не поддерживаются. Иначе говоря, графические приложения, ориентированные на все платформы, не могут рассчитывать на поддержку утолщенных геометрических линий на уровне GDI. Впрочем, даже на платформах семейства NT поддержка стилевых линий в GDI обладает недостаточными возможностями. Вы не можете повторять произвольный пользовательский узор вдоль траектории, определять собственные типы завершений и соединений. Разбиение траектории на отрезки упрощает реализацию этих алгоритмов. С другой стороны, непосредственная работа с кривыми Безье экономит память и повышает точность. В листинге 8.7 приведены определения двух классов, обеспечивающих значительно большие возможности рисования стилевых линий по сравнению с GDI. Листинг 8.7. Классы для работы со стилевыми линиями class KDash
{ public: virtual double GetLength(int 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_pOash: public: KStyleCurve(KDash * pDash) { m_xl = 0: m_yl = 0: m_step = 0: m_pDash= pDash: BOOL MoveToCdouble 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 = ra_xl;
double yl = m_yl:
m_xl += (x2-m_xl) * length / curlen: m_yl += (y2-m_jyl) * 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 1=0: i
Задача рисования стилевых линий разделяется на две подзадачи, каждая из которых решается отдельным классом. Класс KDash управляет циклом чередования и выводом пунктирных отрезков. Он определяется в виде абстрактного класса с двумя виртуальными методами, позволяющими рисовать линии разных стилей для разных классов. Метод GetLength возвращает длину пунктирного отрезка по параметру step; в каком-то отношении он выполняет те же функции, что и массив с описанием пользовательского стиля. Если в стилевой линии используются одинаковые длины пунктирных отрезков -и промежутков, GetLength просто возвращает константу. Если цикл чередования определяется фиксированным массивом, GetLength может вернуть 1pSty1e[step % dwStyleCount]. Метод DrawDash рисует отрезки и промежутки. В качестве параметров ему передаются две точки с вещественными координатами (для повышения точности) и номер текущего сегмента step. По значению этого параметра функция определяет, является ли текущий сегмент отрезком или промежутком. Например, при рисовании пунктирной линии функция может рисовать каждый второй сегмент. Класс KStyl eCurve управляет делением линий и кривых на сегменты нужной длины. В приведенном варианте он содержит методы MoveTo для определения начальной позиции, LineTo для рисования линий и PolyDraw для рисования данных, возвращаемых GetPath. При необходимости класс легко расширяется для поддержки кривых Безье и эллиптических кривых без использования траекторий. Конструктор класса KStyleCurve получает указатель на объект класса KDash, который может потребоваться для получения информации. В классе KStyleCurve главная роль принадлежит функции KStyleCurve::LineTo. Функция получает от класса KDash информацию о текущей длине сегмента и пытается «отрезать» от
476
Глава 8. Линии и кривые
текущей линии сегмент указанной длины. Процесс повторяется до тех пор, пока остаток линии не окажется слишком коротким; в этом случае обработка откладывается до следующей линии. Реализация всегда возвращает сегмент необходимого размера; все остатки меньших размеров сливаются со следующей линией. Такой подход гарантирует, что сегменты будут иметь требуемую длину, но иногда он может приводить к срезанию углов. Более изощренное решение должно поддерживать изгибы отрезков, их равномерное распределение и отсечение. Взаимосвязь между классами KStyl eCurve и KDash очень похожа на отношения между функцией LineDDA и ее функциями косвенного вызова. Главное отличие заключается в том, что KStyl eCurve позволяет работать с кривыми, а в классе KDash косвенный вызов организуется при помощи виртуальных функций C++. Ниже приведен пример класса KDiamond, производного от класса KDash. Класс KDiamond рисует стилевые линии, состоящие из ромбов разных размеров и цветов. Функция GetLength возвращает значение 8 или 16 в зависимости от четности или нечетности номера текущего сегмента. Функция DrawDash рассматривает точки (х1,у1) и (х2,у2) как углы ромба и использует их для вычисления двух оставшихся углов, после чего рисует ромбы как многоугольники различных цветов. Рисование многоугольников рассматривается в следующей главе. class KDiamond : public KDash
477
Итоги
SelectObject(m_hDC, hOld); DeleteObject(hBrush); return TRUE;
}
На рис. 8.19 показано, как работает более гибкий класс, который вместо пунктирных отрезков рисует круги, квадраты, ромбы и треугольники.
•*
*»w
*'** Jilt
Рис. 8.19. Рисование нестандартных стилевых линий
HDC mJiDC; virtual double GetLength(int step) return 8 + (step & 1) * 8: virtual BOOL DrawDash(double xl, double yl. double x2, double y2. int step); public: KDiamond(HDC hDC) { m hDC = 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(mJiDC. 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);
Итоги В этой главе, основанной на материале предыдущих глав, подробно рассматривается процесс рисования линий и кривых в Windows GDI. В ней изучаются бинарные растровые операции, режимы заполнения фона, логические перья, линии, кривые Безье, дуги и траектории, в которых линии объединяются с кривыми. Бинарные растровые операции определяют способ объединения пикселов пера с пикселами приемника и формирования новых пикселов приемника. Самым распространенным случаем является режим R2_COPYPEN, при котором цвет приемника заменяется цветом пера. Инвертируемые растровые операции часто используются в интерактивной компьютерной графике — например, для рисования эластичных линий и перекрестий, а также временных контуров перетаскиваемых объектов. Набор из шестнадцати бинарных растровых операций не так уж велик. Альфа-наложение, которое также является разновидностью объединения Цвета выводимых пикселов с пикселами приемника, при рисовании линий не поддерживается. Применительно к линиям и кривым режим заполнения фона определяет, должны ли выводиться пикселы фона между отрезками. Режим заполнения фона относится только к простым перьям; косметические и геометрические перья рисуют только пикселы линий. Логические перья, определяемые в платформах на базе NT, обладают достаточно серьезными возможностями. Однако на платформах Win32, не входящих в семейство NT, поддержка перьев ограничена, что затрудняет написание уни-
478
Глава 8. Линии и кривые
версальных приложений. В системах Windows 95/98 для геометрических перьев не поддерживаются стилевые линии, утолщенные линии центруются неточно, завершения и соединения поддерживаются только функциями построения траекторий, а альтернативные и пользовательские стили перьев не поддерживаются. В разделе «Пример: рисование нестандартных стилевых линий» приведен пример класса, позволяющего рисовать нестандартные стилевые линии. Этот класс дает возможность имитировать несуществующие геометрические стилевые перья. В GDI предусмотрена достаточно солидная поддержка рисования линий, кривых Безье и эллиптических кривых. Тем не менее функции AngleArc, АгсТо и PolyOraw реализованы только в системах семейства 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 описываются общим типом HGDIOBJ; для манипуляторов логических кистей зарезервирован тип HBRUSH. С каждым контекстом устройства связывается атрибут логической кисти, для работы с которым используются функции GetCurrentObject, SelectObject, GetObject и EnumObjects. Эти функции, рассматривавшиеся в главе 8 применительно к объектам перьев, выполняют одни и те же операции с разнотипными объектами GDI. Объект логической кисти, как и любой другой объект GDI, расходует ресурсы пользовательского процесса и ядра, а также занимает по крайней мере один элемент в таблице объектов GDI, поэтому ненужные логические кисти следует удалять функцией Del eteObject.
Стандартные кисти GDI определяет несколько стандартных объектов кистей, которые могут легко использоваться любым приложением. Чтобы получить манипулятор стандартной кисти, достаточно вызвать функцию GetStockObject с одной из констант BLACK_BRUSH, DKGRAYJRUSH, GRAY_BRUSH, LTGRAYJRUSH, WHITE_BRUSH, NULL_BRUSH (то же, что и HOLLOW_BRUSH) или DC_BRUSH. Черная, темно-серая, серая, светло-серая и белая стандартные кисти представляют собой однородные кисти с различными уровнями интенсивности серого цвета. По умолчанию в контексте устройства выбирается белая кисть. При выборе пустой стандартной кисти (NULL_BRUSH или HOLLOW_BRUSH) внутренняя часть области не закрашивается (по аналогии с тем, как пустое перо не рисует линий). Поскольку функция GetStockObject работает с обобщенными объектами GDI, результат ее вызова для стандартных объектов кистей обычно преобразуется к типу HBRUSH. Стандартная кисть DC, возвращаемая вызовом GetStockObject(DC_BRUSH), принадлежит к числу новых средств Windows 98/2000. Кисти DC, как и перья DC, являются псевдообъектами GDI и могут изменять цвет после выбора в контексте устройства. Для работы с цветом кисти DC, выбранной в контексте устройства, используются следующие функции: COLORREF GetDCBrushColor(HDC hOC); COLORREF SetDCBrushColor(HDC hDC. COLORREF crColor): Функция GetDCBrushCol or возвращает текущий цвет кисти DC; функция SetDCPenColor назначает новый и возвращает старый цвет. Эти функции могут использоваться даже в том случае, если кисть DC не выбрана в контексте устройства, однако в этом случае они ни на что не влияют. Стандартные объекты заранее создаются операционной системой и совместно используются всеми процессами, работающими в системе. После завершения
481
Кисти
работы со стандартными объектами их манипуляторы удалять не нужно. Впрочем, вызов Del eteOb ject для манипулятора стандартной кисти абсолютно безопасен — функция просто возвращает TRUE, не выполняя никаких действий.
Пользовательские кисти Вряд ли кого-нибудь обрадует картина, нарисованная всего пятью оттенками серого цвета. Для создания или получения разноцветных пользовательских кистей с интересными узорами используются следующие функции: HBRUSH CreateSolidBrush(COLORREF crColor): HBRUSH CreateHatchBrushdnt fnStyle, COLORREF crRef); HBRUSH CreatePatternBrush(HBITMAP hbmp); HBRUSH CreateDIBPatternBrushPt(CONST VOID * IpPackedDIB. UINT iUsage): HBRUSH CreateDIBPatternBrush(HGLOBAL hglbDIBPacked. UINT fuColorSpec): HBRUSH GetSysColorBrushtint nlndex);
Однородные кисти Проще всего создаются однородные кисти — для этого достаточно указать цвет. Функция CreateSolidBrush создает только логическую кисть. Когда манипулятор кисти выбирается в контексте устройства, GDI и драйвер устройства должны согласовать между собой реализацию кисти. Если контекст устройства не использует палитру, описатель цвета без особых хлопот преобразуется в составляющие RGB. С другой стороны, для контекстов, использующих палитру, описатель цвета должен преобразовываться в индекс палитры. Если совпадение отыскивается, найденный индекс задействуется при выводе; в противном случае устройство имитирует пикселы нужного цвета, комбинируя доступные цвета путем так называемого смешения (dithering). Смешение позволяет воспроизводить дополнительные цвета на 16- и 256-цветных видеоадаптерах и даже имитировать вывод в оттенках серого на черно-белых принтерах. В следующем фрагменте показано, как создать однородную кисть и выбрать ее в контексте устройства. Программа рисует прямоугольник 8 x 8 каждого из 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); RectanglethDC. 235+x*10. y*10. 235+x*10+9. y*10+9): if ( x==y ) // увеличить цветные квадраты по диагонали
482
Глава 9. Замкнутые области
483
Кисти
ZoomRect(hDC, 235+х*10. у*10. 235+х*10+8. у*10+ 80*(х*8). 180+80*(х/8). 6): SelectObjectChDC, hOld): DeleteObject(hBrush);
} SelectObject(hDC. GetStockObject(BLACK_PEN)):
HS_HORIZONTAL
HS_VERTICAL
HS_FDIAGONAL
HS_BDIAGONAL
HS_CROSS
HS_DIAGCROSS
•••••••••••••••в тштттаттттияятвя тяяяштяявшттшткш яяяхяшяяяяшятияш яшткяяиетттшятяш.
Рис. 9.1. Смешение при рисовании однородной кистью на устройствах, использующих палитру
Для цветов, не входящих в текущую аппаратную палитру, смешение создает узоры, цвет которых в среднем приближается к исходным цветам. При использовании двух чистых цветов в узоре 8 x 8 возможно 64 уровня интенсивности цвета. На устройствах с низким разрешением смешение порождает довольно заметные скопления точек. Но на устройствах высокого разрешения (например, на современных принтерах) драйвер устройства обычно задействует изощренные полутоновые алгоритмы для имитации однородных оттенков цвета. Благодаря ничтожно малым размерам точек современные принтеры обеспечивают качество печати, близкое к качеству фотографических изображений.
Штриховые кисти Функция CreateHatchBrush создает логическую кисть с одним из шести стандартных узоров, образующих равномерный рисунок в виде повторяющихся линий. Тип штрихового узора определяется параметром fnStyle (рис. 9.2). В верхней части рисунка показан результат применения штриховых кистей для заполнения прямоугольных областей. Как нетрудно убедиться, рисунок создается многократным повторением маленьких «блоков», изображенных в нижней части рисунка. Обычно для реализации штриховых кистей драйверы устройств используют растры размером 8 x 8 пикселов, однако архитектура интерфейса DDI позволяет выбирать и другие реализации в зависимости от таких факторов, как разрешение.
Рис. 9.2. Стили штриховых кистей
При работе со штриховыми кистями необходимо учитывать размер штрихового узора, цвет, режим заполнения фона и выравнивание штрихового узора. Штриховые кисти обычно определяются растрами 8 х 8 в единицах устройства (то есть 8 пикселов на 8 пикселов). В отличие от большинства других средств GDI, размер узора штриховой кисти не определяется в логических единицах. Например, при выводе на экран штриховые кисти всегда используют шаблоны 8 x 8 пикселов независимо от действующих преобразований из логических координат в физические. На обычном экране такой узор смотрится нормально, но при сильном уменьшении он начинает выглядеть странно из-за искажения пропорций. С другой стороны, если приложение использует штриховые кисти при выводе на принтер с высоким разрешением, заливка, созданная с применением штриховой кисти, может вообще не порождать сколько-нибудь заметного узора. Какой размер соответствует блоку 8 x 8 пикселов при печати с разрешением 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 GetBrushOrgEx(HDC hDC. LPPOINT Ippt): BOOL SetBrushOrgEx(HDC hDC, int nxOrg. int nyOrg. LPPOINT Ippt):
Базовая точка кисти представляет собой точку (ЬхО, ЬуО)'в системе координат устройства, которая определяет привязку левого верхнего пиксела штрихового узора; остальные пикселы выстраиваются соответствующим образом. Выражаясь точнее, точке (х,у) в системе координат устройства соответствует точка узора Pattern[(x-bxO) % pattern_width. (y-byO) % patternjieight]
По умолчанию координаты базовой точки кисти равны (0,0). Чтобы обеспечить правильное выравнивание кистей после изменения преобразований или отображений, следует вызвать функцию SetBrushOrgEx. Следующий фрагмент обеспечивает выравнивание кистей по точке (0,0) в логической системе координат: POINT Р = { 0. О }: // Начало координат LPtoDP(hDC. &P, 1): // Отображение в координаты устройства SetBrushOrgEx(hDC, P.x. Р.у, NULL): Раньше штриховые кисти очень часто использовались в деловой графике — например, для выделения разных данных в гистограммах или круговых диаграммах. С появлением современных видеоадаптеров, отображающих миллионы цветов, и цветных принтеров штриховые кисти утратили свое значение — считается, что они приносят больше хлопот, чем пользы. Если приложение должно выводить масштабируемые рисунки, напоминающие штриховые узоры GDI, вместо штриховых кистей можно воспользоваться другими средствами (например, линиями или растрами).
Растровые кисти Шести стандартных штриховых кистей явно недостаточно, поэтому GDI позволяет приложениям создавать кисти на базе растровых изображений. В GDI поддерживаются два основных типа растров: аппаратно-зависимые и аппаратно-независимые. Оба типа подробно рассматриваются в следующих трех главах, а сейчас будет лишь показано, как создать на основе растра растровую кисть1 (bitmap brush) и воспользоваться ею. Функция CreatePatternBrush получает манипулятор аппаратно-зависимого растра (DDB) и создает растровую кисть. Многочисленные экземпляры кисти «выкладываются» наподобие мозаики и заполняют область в операциях заливки. GDI создает копию растра, поэтому манипулятор растра не используется логической кистью после ее создания. Функция CreateDIBPatternBrushPt создает кисть по указателю на упакованный аппаратно-независимый растр (DIB); функция CreateDIBPatternBrush создает кисть по глобальному манипулятору блока памяти, содержащему данные DIB. В программировании Win32 глобальный манипулятор блока памяти ресурса (HGLOBAL) в действительности представляет собой указатель в 32-разрядном линейном адресном пространстве. Однако манипулятор глобального блока, возвращаемый функцией Global All ос, отличается от соответствующего указателя, который может быть получен при вызове функции Global Lock. Различия между манипулятором блока и указателем на блок унаследованы из парадигмы 16-разрядного программирования. Также встречается термин «узорная кисть» (pattern brush). — Примеч. перев.
485
Кисти
В приведенном ниже фрагменте показано, как эти три функции используются для создания растровых кистей. Прежде всего мы должны где-то найти готовые растровые изображения, не создавая собственных ресурсных файлов. Вам доводилось играть в карточные игры под Windows (например, раскладывать пасьянс, входящий в комплект поставки системы)? Изображения карт хранятся в библиотеке cards.dll и могут использоваться другими приложениями. В приведенном фрагменте DLL загружается функцией LoadLibrary. Функция LoadBitmap создает манипулятор DDB, а функции FindResource и LoadResource создают манипулятор глобального блока. HINSTANCE hCards - LoadLibraryC'cards.dll"):
for (int i=0; i<3: i++) { HBRUSH hBrush: int width, height: switch ( i ) {
case 0: {
HBITMAP hBitmap = LoadBitmap(hCards. MAKEINTRESOURCE(52)): BITMAP bmp;
GetObject(hBitmap. sizeof(bmp), & bmp); width - bmp.bmWidth: height = bmp.bmHeight; hBrush - CreatePatternBrush(hBitmap): DeleteObject(hBitmap);
} break; case 1:
{
HRSRC hResource = FindResource(hCards. MAKEINTRESOURCE(52-14). RT_BITMAP); HGLOBAL hGlobal = LoadResource(hCards. hResource):
hBrush = CreateDIBPatternBrushPt(
LockResource(hGlobal), DIB_RGB_COLORS): width - ((BITMAPCOREHEADER *) hGlobal)->bcWidth: height = ((BITMAPCOREHEADER *) hGlobal)->bcHeight:
} break: case 2: { HRSRC hResource = FindResourcethCards. MAKEINTRESOURCE(52-28), RT_BITMAP): HGLOBAL hGlobal = LoadResource(hCards, hResource): hBrush = CreateDIBPatternBrush(hGlobal. DIB_RGB_COLORS): width = ((BITMAPCOREHEADER*) hGlobal)->bcWidth; height =• ((BITMAPCOREHEADER *) hGlobal )->bcHeight;
486
Глава 9. Замкнутые области
HGDIOBJ hOld = SelectObjecUhDC, hBrush); POINT P = { 1*140+20 + width*i/4. 250 + height*i/4 }; LPtoDP(hDC. &P, 1): SetBrushOrgEx(hDC. P.x. P.y, N U L L ) ; / / Выровнять изображения карт // 6 прямоугольнике
Rectangle(hDC. 1*140+20. 250. 1*140+20+w1dth*3/2+l. 250+helght*3/2+l); SelectObject(hDC. hOld); DeleteObject(hBrush):
} Программа в цикле перебирает три возможных случая. В первом случае король пик загружается как DDB-растр, на основе которого создается растровая кисть. Во втором случае дама червей загружается и фиксируется в памяти; полученный указатель на упакованный DIB-растр используется для создания растровой кисти DIB. В третьем случае валет бубен загружается и фиксируется в памяти для получения манипулятора блока, требующегося при создании другой растровой кисти DIB. Созданные кисти обеспечивают закраску трех прямоугольников, размеры которых примерно в 1,5 раза превышают размеры карт по каждой из сторон. Чтобы обеспечить совмещение растров с левым верхним углом прямоугольника, мы используем функцию LPtoDP для отображения логических координат в координаты устройства и вызываем функцию SetBrushOrg, которая и производит непосредственное выравнивание. Результат показан на рис. 9.3.
487
Кисти
Во-вторых, размер растровых кистей, как и размер штриховых кистей, задается в единицах координат устройства. Узоры, нарисованные растровыми кистями, всегда имеют одинаковую ориентацию и размеры в системе координат устройства. Чтобы создавать узоры, масштабируемые в соответствие с разрешением устройства, приложению приходится задействовать несколько разных растров. Но самой серьезной является третья проблема: на платформах, не входящих в семейство NT, максимальный размер растровой кисти ограничивается величиной 8 x 8 пикселов. Например, если вы попытаетесь использовать в Windows 95/98 растр больших размеров, выведен будет только левый верхний угол. В следующей главе описаны решения, позволяющие добиться нужного эффекта при помощи растровых функций GDI. Растровые узорные кисти часто используются для рисования горизонтальных и вертикальных пунктирных линий. Вспомните прошлую главу — в псевдоточечных линиях PS_DOT одна точка изображается тремя пикселами, а настоящий точечный стиль PS_ALTERNATE поддерживается только в семействе NT. Если вам не хочется рисовать пунктирную линию пиксел за пикселом, существует простое решение — воспользоваться растровой кистью. Приложение может создать кисть с шахматным узором и рисовать прямоугольники с шириной или высотой, равной одному пикселу, или же закрашивать области, в результате отсечения сведенные к ширине или высоте в один пиксел. В приведенном ниже фрагменте создается кисть с шахматным узором, которая используется для обводки контура прямоугольника, имитируя стиль PS_ALTERNATE. Узор генерируется на основе черно-белого растра 8 x 8 , созданного функцией CreateBitmap. Для рисования линий толщиной в один пиксел применяется функция PatBH, работающая в режиме отображения MMJIXT. В других режимах отображения или в расширенном графическом режиме требуется отсечение, которое гарантирует, что толщина нарисованной линии окажется равной одному пикселу. Этот фрагмент также иллюстрирует второе распространенное применение шахматной кисти — рисование полупрозрачных узоров. Допустим, в текущем контексте устройства выбран черный цвет текста, белый цвет фона, пиксел 0 соответствует черному цвету (RGB(0,0,0)), а пиксел 1 соответствует белому цвету (R6B(255,255,255)). Цвет кисти объединяется с цветом приемника растровой операцией R2_MASKPEN. Таким образом, черные пикселы кисти остаются черными, а белые пикселы не изменяют содержимого приемника. При закраске половина пикселов приемника затемняется, и возникает эффект «полупрозрачности». void FrameCHDC hDC. int xO. int yO. int xl. int yl) unsigned short ChessBoard[] OxAA, 0x55. OxAA. 0x55 }
Рис. 9.3. Растровые кисти и совмещение базовой точки
Обратите внимание: в Windows 95/98 cards.dll является 16-разрядной библиотекой DLL и не может напрямую загружаться приложениями Win32. При работе с растровыми кистями необходимо помнить о некоторых обстоятельствах. Во-первых, на уровне GDI растровые кисти не обеспечивают полноценной замены штриховых кистей. Для растровой кисти каждый пиксел интерпретируется как пиксел основного цвета; фоновых пикселов не существует.
OxAA. 0x55. OxAA, 0x55,
HBITMAP hBitmap - CreateBitmap(8, 8, 1, 1. ChessBoard): HBRUSH hBrush = CreatePatternBrush(hBitmap); DeleteObject(hBitmap); HGDIOBJ hOld = SelectObjectthDC, hBrush); // Прямоугольник PS_ALTERNATE PatBlt(hDC. xO. yO. xl-xO. 1. PATCOPY): PatBltChDC. xO. yl. xl-xO. 1. PATCOPY);
488
Глава 9. Замкнутые области
PatBlt(hDC. xO. yO. I. yl-yO. PATCOPY); PatBltdiDC. xl, yO, I. yl-yO. PATCOPY): int old = SetROP2(hDC. R2_MASKPEN); RectangleChDC, xO+5. yO+5. xl-5, yl-5): SetROP2(hDC. old): SelectObject(hDC. hOld): DeleteObject(hBrush);
}
На рис. 9.4 показан эффект от применения шахматного узора. Пожалуй, разработчикам из Microsoft следовало бы включить этот узор в состав штриховых кистей. ПНиНПНПНиНПИПНПНОНПИПИ
опапвпвдвпвпвпвпвавпвавпвпва апававававвввввввввввввввввв юпаавававввпвававпвавававава аавававввпвававпвавава " авававввпвпвавпвпвавав! рапвавовдвдвпвдвпвовова
Рис. 9.4. Применение шахматного узора: пунктирные линии и полупрозрачность
Кисти системных цветов В системе управления окнами используются десятки цветов, предназначенных для вывода различных частей окна — строк заголовков, рамок, меню, полос прокрутки, кнопок и т. д. Эти цвета называются системными и настраиваются в специальном приложении панели управления или на программном уровне, при помощи функций API GetSysColor и SetSysColor. Для каждого системного цвета система создает стандартную кисть. Приложения могут получать кисти системных цветов при помощи функции GetSysColorBrush, передавая ей значения в интервале от COLOR_SCROLLBAR до COLOR_GRADIENTACTIVECAPTION. Кисти системных цветов применяются для закраски областей, цвета которых должны соответствовать областям, закрашиваемым системой. Если окно самостоятельно управляет прорисовкой неклиентской области, кисти системных цветов оказываются исключительно полезными. Эти кисти принадлежат к числу стандартных объектов GDI, которые не нужно удалять после использования. Вызовы функции DeleteObject для кистей системных цветов игнорируются.
489
Кисти
Кисти системных цветов могут указываться в качестве фоновых кистей при регистрации классов окон, при этом допускается использование индексов системных цветов в формате (HBRUSH)(COLOR_WINDOW + 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 максимальный размер используемой части растра равен 8 x 8 пикселов, что существенно снижает полезность этого удобного средства. Все три типа кистей описываются структурой LOGBRUSH, которая может передаваться функции CreateBrushlndirect при создании логической кисти. Соответствующие определения приведены ниже. typedef struct tagLOGBRUSH { UINT IbStyle: COIORREF IbColor; LONG IbHatch: }: HBRUSH CreateBrushlndirecUCONST LOGBRUSH * Iplb); Основные трудности при работе со структурой LOGBRUSH связаны с тем, что при выборе стиля BS_PATTERN поле 1 bHatch содержит манипулятор DDB, а для стилей BS_DIBPATTERN и BS_DIBPATTERNPT в этом поле указывается манипулятор блока DIB или указатель, а значение LOWORD(lbColor) равно либо DIB_PAL_COLORS, либо DIB_RGB_COLORS.
Структура LOGBRUSH может использоваться для получения информации об объекте кисти GDI при помощи функции GetObject: LOGBRUSH logbrush: GetObjectChBrush. sizeof(LOGBRUSH). & logbrush):
490
Глава 9. Замкнутые области
Не рассчитывайте найти в структуре LOGBRUSH, возвращаемой GetObject, действительный манипулятор или указатель на растр узорной кисти. При создании узорной кисти GDI создает копию 'растра во внутренней структуре данных, скрытой от пользовательских приложений. Структура LOGBRUSH также используется при создании расширенных перьев. При рисовании линий и кривых геометрическим пером на самом деле задействуется кисть, поэтому здесь также могут потребоваться смешение, штриховка и растры. Объект логической кисти находится под управлением 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 SetRectCLPRECT Iprc. int xLeft. int yTop. int xRight. int yBottom); BOOL SetRectEmptyCLPRECT Iprc); BOOL IsRectEmpty(CONST RECT * Iprc);
BOOL EqualRect(CONST RECT *lprcl, CONST RECT *1prc2): BOOL CopyRecttLPRECT IprcDst, CONST RECT * IprcSrc): BOOL OffsetRecttLPRECT Iprc, int dx, int dy); BOOL PtInRect(CONST RECT * Iprc. POINT pt);
BOOL InflateRecttCONST LPRECT Iprc. int dx. int dy); BOOL IntersectRectCCONST LPRECT IprcOst. CONST RECT * IprcSrcl. CONST RECT * 1prcSrc2); BOOL SubtractRect(LPRECT lprcDst2. CONST RECT * IprcSrcl, CONST RECT * IprcSrcZ): • 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 применяются преобразования или отображения, вы должны принять дополнительные меры предосторожности и убедиться в том, что прямоугольник остается нормализованным и сохраняет параллельность осям.
493
Прямоугольники
Если толщина пера равна один пикселу в координатах устройства, рисуются только пикселы периметра. Если перо имеет толщину в п пикселов, один пиксел рисуется на центральной линии, (и-1)/2 пикселов рисуются снаружи прямоугольника и еще (и-1)/2 — внутри прямоугольника. При использовании пера со стилем PS_INSIDEFRAME один пиксел рисуется на центральной линии, а еще (й-1) пикселов — внутри прямоугольника. Другим особым случаем является пустое перо, которое не обводит периметр прямоугольника. При использовании пустого пера ширина и высота прямоугольника уменьшаются на один пиксел.
'R Пустое перо
Рисование прямоугольников В Win32 API предусмотрено несколько функций, которые закрашивают внутреннюю область прямоугольника кистью, обводят его контуры пером или делают то и другое: BOOL Rectangle(HDC hDC. int nLeftRect. int nTopRect. int nRightRect. int nBottomRect); int Fi meet (HDC hDC. CONST RECT * Iprc, HBRUSH hbr); int FrameRect(HDC hDC. CONST RECT * Iprc, HBRUSH hbr); BOOL InvertRecttHDC hDC, CONST RECT * Iprc); BOOL DrawFocusRecUHDC hDC. CONST RECT * Iprc);
Черное перо, расширенный режим
Перо с толщиной 3 пиксела, рисование внутри контура
Синее перо, R2_NOTCOPYPEN
Рис. 9.5. Различные стили прямоугольников
Rectangle Функция Rectangle рисует прямоугольник, определяемый четырьмя координатами. На процесс рисования влияет достаточно большое количество атрибутов контекста. Поскольку функция Rectangle принадлежит к числу базовых функций GDI, мы рассмотрим ее более подробно. Рисунок 9.5 иллюстрирует результат применения функции Rectangl e при разных атрибутах контекста устройства. Если контекст находится в совместимом графическом режиме, правая и нижняя стороны прямоугольника в системе координат устройства не рисуются (что соответствует традиционным правилам включения/исключения сторон). Обратите внимание на то, что правая сторона прямоугольника может и не соответствовать nBottomRect; она определяется максимальным значением nTopRect и nBottomRect при отображении на систему координат устройства. Но если контекст устройства находится в расширенном графическом режиме, выводятся все четыре стороны прямоугольника, как показывает нижний рисунок в левом столбце. Вероятно, это изменение неизбежно, поскольку возможность применения произвольных аффинных преобразований в расширенном режиме затрудняет определение правой и нижней сторон (по сравнению с верхней и нижней). Периметр прямоугольника обводится текущим объектом пера, выбранным в контексте устройства.
'R Перо с толщиной 3 пиксела
Текущий объект кисти закрашивает ту область, которая не была закрашена пером, а в случае пустого пера — весь прямоугольник, уменьшенный на один пиксел. На результаты применения кисти также влияет режим заполнения фона, цвет фона и базовая точка кисти. Текущая растровая операция в контексте устройства распространяется как на периметр, так и на внутреннюю часть прямоугольника. Например, если выбрать операцию R2_NOP, функция Rectangle не выполняет никаких действий.
FillRect Функция FillRect закрашивает кистью прямоугольник, определяемый структурой RECT. Правая и нижняя стороны в системе координат устройства исключаются всегда, даже в расширенном графическом режиме. В отличие от вызова Rectangl e с пустым пером, приводящим к рисованию уменьшенного прямоугольника, функция FillRect закрашивает весь прямоугольник. Она не использует атрибут бинарной растровой операции в контексте устройства. Параметр-кисть, передаваемый FillRect, также может содержать индекс системного цвета в формате (HBRUSH)(индекс +1). Различия между FillRect и Rectangle
494
Глава 9. Замкнутые области
объясняются тем, что в реализации FillRect используются средства GDI для работы с растрами. FillRect вызывает недокументированную функцию PolyPatBlt GDI, работа которой основана на вызове PatBlt (см. следующую главу).
FrameRect Функция FrameRect закрашивает периметр прямоугольника кистью (а не пером!). При этом контур нарисованного изображения имеет те же размеры, как и при вызове FillRect. Толщина периметра равна одной логической единице, а его реальная толщина в пикселах определяется мировым преобразованием и режимом отображения.
495
Прямоугольники
после прокрутки содержит только вновь открывшиеся области, поэтому вызов DrawFocusRect при обработке сообщения WM PAINT не приводит к стиранию прямоугольника. Но если контекст устройства был получен функцией GetDC, которая не настраивает системный регион, проще стереть прямоугольник перед прокруткой. На рис. 9.6 продемонстрирован результат вызова функций FillRect, FrameRect, InvertRect и DrawFocusRect, которые не относятся к числу функций GDI, хотя и используют функции GDI в своей работе. Эти функции поддерживаются системой управления окнами (user32.dll) и относятся именно к ней.
Прорисовка периметра прямоугольника позволяет создавать интересные эффекты, которые трудно выполнить при помощи пера GDI. В частности, кисть позволяет использовать смешанные цвета, не создавая геометрическое перо, или рисовать полноценные точечные контуры прямоугольников растровой кистью с шахматным узором. Функция FrameRect также реализуется с применением недокументированной функции PolyPatBlt.
InvertRect Функция InvertRect инвертирует цвет каждого пиксела прямоугольника по аналогии с тем, как перо в режиме R2_NOT инвертирует пикселы линии. В устройствах, использующих палитру, инвертируются индексы палитры и цвет определяется расположением цветов в палитре. В устройствах без палитры черный цвет переходит в белый, белый цвет переходит в черный, а RGB-значение каждого пиксела инвертируется. При двукратном вызове InvertRect с одинаковыми параметрами восстанавливается исходное содержимое контекста устройства. Функция InvertRect реализуется функцией PatBlt, поэтому она подчиняется тем же правилам включения/исключения сторон, что и функции FrameRect и FillRect.
DrawFocusRect Функция DrawFocusRect напоминает функцию FrameRect. Она рисует периметр прямоугольника шахматной узорной кистью с применением растровой операции «исключающего ИЛИ». Как и в случае с функцией InvertRect, повторный вызов DrawFocusRect восстанавливает исходное содержимое контекста устройства. Функция DrawFocusRect реализуется функцией PolyPatBlt с узорной кистью и растровой операцией «исключающего ИЛИ». Название DrawFocusRect связано с применением этой функции в модуле управления окнами. Например, в диалоговых окнах функция DrawFocusRect рисует точечный контур прямоугольника на кнопке, получающей фокус ввода с клавиатуры. Когда фокус переходит к другой кнопке, прямоугольник стирается повторным вызовом DrawFocusRect, после чего вызывается функция рисования прямоугольника на кнопке, получившей фокус. Функция DrawFocusRect также может использоваться при выводе «эластичных» прямоугольников. Применяя эту функцию в интерактивном взаимодействии, проследите за правильностью определений прямоугольников. MSDN преувеличивает проблему и предупреждает программистов, что нарисованные функцией DrawFocusRect прямоугольники не могут прокручиваться. На самом деле с прокруткой проблем нет: обновляемый регион
FillRect
FrameRect
InvertRect
DrawFocusRect
Рис. 9.6. Функции рисования прямоугольников, используемые системой управления окнами
Прорисовка границ и элементов управления В Win32 API входит ряд функций, с помощью которых система управления окнами рисует разнообразные границы и элементы управления (controls) и которые имеют непосредственное отношение к рисованию прямоугольников. Эти функции могут использоваться при реализации элементов, прорисовка которых осуществляется владельцем, при нестандартном выводе неклиентской области или при имитации внешнего вида окон и элементов управления. Ниже приведены прототипы двух важнейших функций, DrawEdge и DrawFrameControl. BOOL DrawEdgeCHDC hDC. LPRECT Iprc. UINT edge. UINT grFTags); BOOL DrawFrameControl(HDC hDC, LPRECT Iprc. UINT uType, UINT uState): Обе функции получают манипулятор контекста, структуру RECT с описанием рисуемой области и два флага. Флаги и их смысл описаны в документации MSDN, а мы лишь приведем примеры их использования. Следующий фрагмент показывает, как рисовать различные границы (рис. 9.7). for (int e=0:
e<4;e++)
{ const int Edge[] = {EDGEJAISED. EDGEJUNKEN. EDGEJTCHED. EDGE JUMP): int Edge[] = { EDGEJAISED. EOGEJUNKEN. EDGEJTCHED. EDGE JUMP }: int F1ag[] - { BF_MIDDLE | BFJOTTOM. BF_MIDDLE | BFJOTTOMLEFT. BF_MIDDLE | BFJOTTOMLEFT | BFJOP. BF_MIDDLE I -BFJECT. BF_MIDDLE | BFJECT | BFJLAT. BF_MIDDLE I BFJECT | BF_MONO. BF MIDDLE | BF RECT | BF SOFT.
496
Эллипсы, секторы, сегменты и закругленные прямоугольники
Глава 9. Замкнутые области
Показанные на рисунке элементы управления нарисованы в прямоугольниках 40 х 40 пикселов, что значительно превышает стандартные размеры, используемые в системе. Обратите внимание на отсутствие неровных краев, которые обычно появляются при увеличении мелких растровых изображений. Возможно, вы и не подозревали, что при рисовании этих крестиков, стрелок, вопросительных знаков и т. д. Windows использует символы шрифта TrueType Marlett, чтобы изображение было полностью масштабируемым.
BF_MIDDLE | BF_RECT | BF_DIAGONAL. BF_MIDDLE | BF_RECT | BF_ADJUST };
for (int f=0: 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): // Увеличить фон FillRect(hDC. & rect. GetSysColorBrush(COLOR_BTNFACE)): InflateRect(&rect. -3. -3); // Восстановить размер DrawEdge(hDC. & rect. Edge[e]. Flag[f]);
Эллипсы, секторы, сегменты и закругленные прямоугольники
Рис. 9.7. Использование функции DrawEdge для рисования границ
Функция DrawFrameControl рисует всевозможные элементы управления, обычно встречающиеся в строке заголовка, строке меню, полосах прокрутки, кнопках и всплывающих меню. Некоторые примеры приведены на рис. 9.8. За подробным описанием обращайтесь к MSDN.
^ж] ?^rJ У 4 ] .->•{ : Vl \<% •*Mt*ne»wJ.
.ямйчи^ц!
&*им**«**ч*
-чп&АмимД
'MwwMimJ
•• ^jF*"
Рис. 9.8. Рисование различных элементов управления функцией DrawFrameControl
497
,* I
В GDI предусмотрено несколько функций для рисования эллипса, его частей и даже гибрида прямоугольника с эллипсом — закругленного прямоугольника. Прототипы этих функций приведены ниже. BOOL Ellipse(HOC hDC, int nleftRect, int nTopRect. int nRightRect. int nBottomRect); BOOL ChorcKHDC hDC. int nLeftRect, int nTopRect. int nRightRect, int nBottomRect, int nXRadiall. int nYRadiall. int nXRadia!2. int nYRadia!2); BOOL Pie(HDC hDC, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect. int nXRadiall, int nYRadiall, int nXRadia12. int nYRadia!2): BOOL RoundRect(HDC hDC, int nLeftRect. int nTopRect. int nRightRect. int nBottomRect. int nWidth. int nHeight): Эти четыре функции используют такие же ограничивающие прямоугольники, как и описанная выше функция Rectangle. Сходство проявляется и в принципе рисования: контур обводится текущим объектом пера в контексте устройства, а внутренняя область закрашивается текущей кистью. В совместимом графическом режиме, если толщина пера в системе координат устройства равна одному пикселу, правая и нижняя стороны не рисуются в соответствии с правилами включения/исключения. Однако в расширенном графическом режиме рисуются все четыре стороны. В документации Microsoft лишь упоминается тот факт, что в двух графических режимах прямоугольники рисуются по-разному. Линии, нарисованные пером со стилем PSJNSIDEFRAME, полностью находятся в ограничивающем прямоугольнике. На рис. 9.9 показано, как нарисованный эллипс выглядит на уровне пикселов. Первый эллипс нарисован однородным пером толщиной в один пиксел в совместимом графическом режиме; нижняя и правая стороны в ограничивающий прямоугольник не входят. Второй эллипс нарисован в расширенном графическом режиме с включением правой и нижней стороны. Третий эллипс нарисован пером толщиной в два пиксела со стилем PS_INSIDEFRAME, в результате чего образовалась уродливая несимметричная фигура. Возможно, нарушение симметрии связано с аппроксимацией кривых Безье и выводом кривых Безье в виде набора
498
Глава 9. Замкнутые области
отрезков. Хотя выше говорилось о том, что такая аппроксимация обеспечивает минимальную погрешность, для такой крошечной фигуры даже разница в один пиксел становится очень заметной. .Left
•Left
Left
Bottom
Эллипсы, секторы, сегменты и закругленные прямоугольники
Кривые, нарисованные этими функциями, являются замкнутыми. Таким образом, при использовании геометрического пера на всех стыках применяется его атрибут соединения, а атрибут завершения не применяется. Периметры, нарисованные этими функциями, также могут включаться в объекты траекторий. В примере с функцией Chord задействовано косметическое перо со стилем PS_ALTERNATE, а для функции Pie — утолщенное геометрическое перо с заостренным соединением и стилем PS_INSIDEFRAME. Обратите внимание: дуга полностью расположена внутри ограничивающего прямоугольника, но на радиусах пикселы распределяются симметрично с обеих сторон. Для функции Ellipse на рисунке использовано узорное геометрическое перо и узорная кисть. Следующая функция рисует простейшие круговые диаграммы.
void DrawPieCharUHDC hDC. int xO. int yO, int xl. int yl, double data[]. CQLORREF color[], int count) { double sum • 0;
Right Bottom Однородное перо
Однородное перо, расширенный режим
PS INSIDEFRAME
Right
for (int i-0; i
Рис. 9.9. Нарисованный эллипс (в увеличении) На рис. 9.10 сравниваются результаты вызова функций Ellipse, Pie и Chord. Функция E l l i p s e рисует полный периметр эллипса текущим пером и закрашивает его внутреннюю часть текущей кистью. Окружность является частным случаем эллипса, ширина и высота которого выражена в физических единицах. Функция Pie рисует сектор — клиновидную фигуру, образованную частью периметра и двумя радиусами. Функция Chord рисует сегмент, образованный частью периметра эллипса и секущей, соединяющей две точки периметра. В обеих функциях, Pis и Chord, начальный и конечный угол задаются двумя точками (по аналогии с функцией Arc). Таким образом, в совместимом графическом режиме окончательный вид фигуры зависит от атрибута направления дуг контекста. В расширенном режиме дуги всегда рисуются против часовой стрелки в логической системе координат.
double angle • 0; for (i-0; i
:i! :'£' •:%-•.
$?•' *X' ": • '•'• Chord, против часовой стрелки (Left,Top) (xEnd, уЁпо1),,-
"
,
(Bottom, Right)
angle +- a; SelectQbjecUhOC. hOld); DeleteObject(hBrush);
Pie, по часовой стрелке
} }
(Left, Top) (xEnd, yEnd) .-••'" (xStart, yStartJjj
499
(xStart, yStart)
(Bottom, Right)
Рис. 9.10. Функции Ellipse, Pie и Chord
(Bottom, Right)
Функция RoundRect, предназначенная для рисования прямоугольников с закругленными углами, позволяет нарисовать прямоугольник, эллипс или любую промежуточную фигуру. При вызове ей передаются те же параметры ограничивающего прямоугольника, как и при вызове Rectangle или Ellipse. Последние два параметра, nWidth и nHeight, определяют размеры закругленных углов. Закругленные углы можно рассматривать как четыре четверти эллипса с шириной nWidth и высотой nHeight, соединенные прямыми линиями. Если оба параметра nWidth и nHeight равны нулю, функция Rou ndRect рисует прямоугольник. Если параметр nWidth равен ширине ограничивающего прямоугольника, а параметр nHeight совпадает с его высотой, функция RoundRect рисует эллипс. Если приложение передает в параметре nWidth или nHeight отрицательное число, GDI использует
500
Глава 9. Замкнутые области
в вычислениях его абсолютную величину (модуль). Размеры ограничивающего прямоугольника также ограничивают размеры углов. Смысл параметров функции RoundRect иллюстрирует рис. 9.11.
I
(nLeft nTop)
__
InLeft + nWidth
*ЙЙ
nTop + nHeight f |
jf I
(nBottom, nRight) Рис. 9.11. Функция RoundRect: от прямоугольника к эллипсу
Многоугольники Прямоугольник является частными случаем многоугольника — замкнутой фигуры, состоящей из двух и более вершин, соединенных прямыми линиями. Многоугольник может представлять собой треугольник, параллелограмм, прямоугольник, квадрат, восьмиугольник и т. д. В GDI API многоугольник представляется массивом структур POINT, определяющих координаты вершин. При одном вызове функции GDI позволяет нарисовать один или сразу несколько многоугольников. Данные нескольких многоугольников передаются в двух массивах — в одном содержится количество вершин каждого многоугольника, а в другом — координаты всех вершин. Ниже приведены прототипы функций GDI, предназначенных для рисования многоугольников. int GetPolyFil1Mode(HDC hDC); int SetPolyFillMode(HDC hDC. int IPloyFillMode); BOOL Polygon(HDC hDC. CONST POINT * IpPoints. int nCount); BOOL PolyPolygontHDC hDC. CONST POINT * IpPoints, CONST int * IpPolyCounts, int nCount);
Принцип рисования многоугольников функцией Polygon напоминает рисование ломаных линий функцией Polyline. Параметр IpPoints указывает на массив структур POINT, содержащих координаты вершин. Параметр nCount определяет количество вершин в массиве (не менее двух). Функция Polygon автоматически замыкает фигуру и при использовании геометрического пера оформляет каждую вершину в соответствии с атрибутом соединения. Контур многоугольника обводится текущим пером, а его внутренняя часть закрашивается текущей кистью.
Многоугольники
501
На этом сходство не заканчивается: рисование нескольких многоугольников функцией PolyPolygon напоминает процедуру рисования нескольких ломаных функцией PolyPolyline. В параметре nCount передается количество многоугольников. Параметр IpPolyCounts указывает на массив с количествами вершин для каждого многоугольника (не менее двух). Параметр IpPoints указывает на массив структур, содержащих координаты вершин. Функция PolyPolygon автоматически замыкает каждый многоугольник. Контур каждого многоугольника обводится текущим пером, а внутренняя часть закрашивается текущей кистью. В отличие от функций с ограничивающими прямоугольниками (таких, как Rectangle, Ellipse и Arc), исключающих правую и нижнюю стороны в совместимом графическом режиме, функции Polygon и PolyPolygon рисуют все свои вершины, причем это поведение сохраняется как в совместимом, так и в расширенном графическом режиме. Следовательно, прямоугольник, нарисованный как многоугольник по четырем углам, несколько отличается от обычного прямоугольника, нарисованного в совместимом режиме. В частности, это объясняет различия между поведением функции Rectangle в двух графических режимах. В результате применения аффинных преобразований с поворотами и сдвигом прямоугольник может превратиться в параллелограмм или утратить параллельность осям координат, поэтому графический механизм GDI в общем случае не может нарисовать преобразованный прямоугольник исходными средствами.
Режим заполнения многоугольников Для простого (например, выпуклого) многоугольника внутренняя область определяется достаточно четко. Однако невыпуклый прямоугольник может состоять из нескольких частей, что затрудняет определение его внутренней области. В Windows GDI внутренняя область многоугольника определяется при помощи двух правил, которые в терминологии GDI называются «режимом заполнения многоугольников» (polygon fill mode). Режим заполнения многоугольников принадлежит к числу атрибутов контекста устройства, и для работы с ним используются функции GetPolyFillMode и SetPolyF i l l Mode. Существует два допустимых значения режима — ALTERNATE и WINDING. Режим ALTERNATE, используемый в контекстах устройств по умолчанию, очень прост и нагляден. Принадлежность точки внутренней области многоугольника в режиме ALTERNATE проверяется следующим образом: из этой точки проводится луч в бесконечность в любом направлении и подсчитывается количество пересечений этого луча с контуром многоугольника. При нечетном количестве пересечений точка считается находящейся внутри, а при четном — снаружи. Примеры использования режима ALTERNATE приведены на рис. 9.12. На первом рисунке изображен простой ромб, являющийся выпуклым многоугольником. Каждая строка развертки внутри ромба дважды пересекается с периметром; точки между пересечениями образуют внутреннюю область многоугольника. На втором рисунке слева при соединении вершин многоугольника получается фигура в виде восьмиконечной звезды. Каждая строка развертки Пересекается с периметром до шести раз, поэтому все точки между вторым
502
Глава 9. Замкнутые области
503
Многоугольники
и третьим, а также четвертым и пятым пересечением не считаются принадлежащими многоугольнику. На двух последних рисунках фигура состоит из двух многоугольников (меньший прямоугольник находится внутри большего). В режиме ALTERNATE эти два примера выглядят одинаково.
Рис. 9.13. Режим заполнения многоугольников WINDING for (int t=0; t<2: t++)
{
Рис. 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.
logbrush.lbColor = RGB(0. 0. OxFF); KGDIObject pen (hDC. ExtCreatePen(PS_GEOMETRIC | PSJOLID | PS_JOIN_MITER. 3. & logbrush. 0. NULL)): if ( t==0 ) SetPolyFillMode(hOC. ALTERNATE); else SetPolyFillModethDC, WINDING);
for (int m=0: m<4; m++) { SetViewportOrgEx(hDC. 120+m*220. 350+t*220. NULL); const int sO[] = { 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]; // Количество точек // Количество вершин // каждого многоугольника const int * s = spec[m]+2: // Индекс вершины int n = spec[m][0]; int d = spec[m][l];
for (i-0: i
int V[2] - { 5. 5 };
504
Глава 9. Замкнутые области
PolyPolygon(hDC. P. V, 2);
Замкнутые траектории
505
На рис. 9.14 приведены примеры использования функций FillPath и F i l l AndStrokePath, а также иллюстрируются последствия вызова WidenPath и режима заполнения многоугольников.
Замкнутые траектории Траекторией в GDI называется объект, состоящий из нескольких линий, дуг и кривых Безье. Траекторию, построенную вызовами функций рисования линий и кривых между вызовами BeginPath и EndPath, можно обвести пером, закрасить кистью или сделать то и другое одновременно. Процесс обводки траектории пером рассматривается в главе 8, а сейчас нас больше интересует закраск£ траекторий кистью. В GDI эта задача решается двумя функциями: BOOL FillPath(HDC hDC); BOOL StrokeAndFillPathfHDC hDC):
Функция F i l l Path замыкает все незамкнутые фигуры в текущей траектории, неявно связанной с контекстом устройства, и закрашивает их текущей кистью, выбранной в контексте устройства. Как было сказано выше, траектория состоит из одной или нескольких групп линий или кривых. В результате аппроксимации, кривых траектория фактически превращается в совокупность многоугольников. Функция FillPath практически эквивалентна вызову PolyPolygon с пустым пером. Как и при вызове PolyPolygon, при определении принадлежности точек внутренней области закрашиваемой траектории учитывается режим заполнения многоугольников. Перед тем как вернуть управление, функция F i l l Path освобождает объект траектории в контексте устройства. Функция StrokeAndPath замыкает все незамкнутые фигуры текущей траектории, закрашивает их текущей кистью и обводит контуры текущим пером. Эта функция очень похожа на функцию PolyPolygon. Как и функция F i l l P a t h , она тоже освобождает объект траектории в контексте устройства перед возвратом управления. Учтите, что StrokeFillPath в общем случае нельзя заменить последовательными вызовами StrokePath и F i l l P a t h , что объясняется двумя причинами. Во-первых, каждая из этих функций освобождает траекторию, поэтому следующий вызов завершится неудачей, если только вы не позаботитесь о сохранении и восстановлении контекста. Во-вторых, при раздельных вызовах StrokePath и FillPath генерируются перекрывающиеся области, что приводит к возникновению нежелательных эффектов при использовании некоторых растровых операций. Многоугольник или совокупность многоугольников всегда можно без потери точности преобразовать в траекторию. Траектория, не содержащая кривых, легко преобразуется в совокупность многоугольников. Траекторию с кривыми можно аппроксимировать функцией Fl attenPath, а затем преобразовать в совокупность многоугольников, однако линейная аппроксимация кривых приводит к потере точности и увеличению объема данных, обрабатываемых GDI. Следовательно, там, где это возможно, функциям траекторий следует отдавать предпочтение перед функциями многоугольников.
Рис. 9.14. Функции FillPath, StrokeAndFillPath и режимы заполнения многоугольников
Мы имеем дело с восемью разными случаями. В каждом случае траектория образуется двумя перекрывающимися эллипсами, повернутыми на 45°. Изображения в первом ряду были получены в режиме WINDING, во втором — в режиме ALTERNATE. В первом столбце использовалась функция FillPath с кистью светлого оттенка, а изображения второго столбца были построены функцией StrokeAndFill с темным толстым пером. Третий столбец был создан функцией WidenPath с толстым пером, после чего была вызвана функция FillPath. Изображения последнего столбца были построены функцией WidenPath с толстым пером и последующим вызовом StrokeAndFillPath с тонким пером. Вспомните, о чем говорилось в главе 8, — функция WidenPath генерирует новую траекторию по периметру области, которая была бы закрашена при обводке траектории текущим пером, и петли в новой траектории возникают в результате соединения линий и кривых. Ниже приведен фрагмент кода, при помощи которого был построен рис. 9.14. Программа строит траекторию из двух эллипсов, получает ее данные функцией GetPath, поворачивает на 45° и с помощью результата строит траектории, используемые непосредственно при рисовании. void KMyCanvas::TestFillPath(HDC hDC) { const int nPoint = 26; POINT Point[nPoint]: BYTE Type[nPoint]; // Построение траектории из двух эллипсов BeginPath(hDC): Ellipse(hDC. -100. -40. 100. 40); EllipsethDC. -40. -100. 40. 100); EndPath(hDC);
// Получение данных траектории и поворот на 45 градусов GetPathChDC. Point. Type. nPoint);
506
Глава 9. Замкнутые области
for (int i=0; i
double x = Point[i].x * 0.707:' double у = Point[i].y * 0.707; Point[1].x = (int) (x - y); Point[1].y - (int) (x + y);
KGDIObject brushthOC. OeateSolidBrush(RGB(OxFF. OxFF. 0))); KGDIObject pen (hDC. CreatePen(PS_SOLID, 19. RGB(0. 0, OxFF))):
for (int t-0; t<8: t++) { SetViewportOrgEx(hDC. 120+(Ш)*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: FillPath(hDC); break; case 1: StrokeAndFillPath(hDC): break; case 2: WidenPath(hDC); FillPath(hDC): break; case 3: WidenPath(hDC); { KGDIObject thinChDC, CreatePen(PS_SOLID. 3. RGB(0. 0. OxFF))): StrokeAndFillPath(hDC);
SetViewportOrgExthDC. 0. 0, NULL);
Регионы В главе 7 мы в общих чертах познакомились с регионами, уделяя основное внимание их использованию при отсечении. В Win32 GDI регионы важны не только в качестве структур данных, но и при выводе. В этом разделе подробно рассматриваются регионы и основные области их применения. Ниже перечислены важнейшие области применения регионов в Windowsпрограммировании (некоторые из них уже упоминались в главе 7).
Регионы
507
О Определение формы окна: SetWi ndowRgn. О Хранение информации об участках окна, нуждающихся в перерисовке: InvalidateRgn, GetUpdateRgn. О Отсечение: SelectClipRgn, SetMetaRgn. О Графический вывод: регион можно непосредственно воспроизвести на экране. О Проверка принадлежности: регионы могут использоваться для представления геометрических фигур. О DirectDraw: структура данных региона используется интерфейсом I DirectClipper. С точки зрения GDI регион определяет совокупность точек в координатном пространстве. Эта совокупность может быть пустой, а может занимать все координатное пространство; иметь прямоугольную или любую неправильную форму. Объект региона находится под управлением GDI и представляет некоторый регион в системе. Как и в случае с другими объектами GDI, после создания объекта региона приложение получает лишь его манипулятор, который может передаваться GDI при ссылках на этот объект. Манипуляторы регионов в GDI относятся к типу HRGN. Внутренняя структура данных, представляющая объект региона, достаточно сложна и может иметь весьма внушительные размеры. Когда объект региона станет ненужным, его следует удалить функцией Del eteObject.
Создание объекта региона При создании новых объектов регионов используются следующие функции: HRGN CreateRectRgn(int nLeftRect. int nTopRect. int nRightRect. int nBottomRect); HRGN CreateRectRgnlndirecUCONST RECT * Iprc); HRGN CreateRoundRectRgn(int nLeftRect.int nTopRect. int nRightRect. int nBottomRect. int nWidthEllipse. int nHeightEllipse): HRGN CreateEllipticRgn(int nLeftRect. int nTopRect. int nRightRect, int nBottomRect); HRGN CreateEllipticRgnlndirecUCONST RECT * Iprc): HRGN CreatePolygonRgn(CONST POINT * Ippt, int cPoints. int fnPolyFillMode): HRGN CreatePolyPolygonRgntCONST POINT * Ippt. CONST INT * ' IpPolyCounts. int nCount. int fnPolyFillMode); HRGN PathToRegion(HDC hDC); Все функции этой группы, за исключением PathToRegion, не зависят от контекста устройства, пера или кисти. Объект региона является независимым объектом, представляющим геометрическую фигуру. В другом контексте координаты региона интерпретируются как логические координаты или координаты устройства. Функция CreateRectRgn создает регион, содержащий все точки прямоугольной области, которая обычно определяется своими левым верхним и правым нижним углами. Две точки, определяющие прямоугольник, не обязательно должны быть правильно упорядочены; GDI нормализует их по правилам внутреннего представления GDI (левая координата меньше правой, верхняя координата
508
Глава 9. Замкнутые области
меньше нижней). Функция CreateRectRgnlndirect представляет собой упрощенную разновидность CreateRectRgn, которая получает параметры через структуру RECT. В Windows NT/2000 реализация CreateRectRgnlndirect сводится к простому вызову CreateRectRgn. При использовании прямоугольного объекта региона он всегда интерпретируется по правилу исключения нижней и правой сторон. Это правило действует как в совместимом, так и в расширенном графических режимах. Например, вызов CreateRectRgn(0,0,0,0) создает пустой регион (вместо региона, содержащего единственную точку (0,0)). В системе координат устройства вызов CreateRectRgn (0,0,1,1) создает регион, содержащий единственную точку (0,0), и в этом смысле он эквивалентен вызову CreateRectRgn(l,l,0,0). Функция CreateRoundRectRgn(0,0,0,0) создает регион, состоящий из всех точек прямоугольника с закругленными углами. Каждый из четырех углов соответствует одной четверти эллипса nWidthEllipsexnHeightEllipse. По каким-то неизвестным причинам при создании региона в виде прямоугольника с закругленными углами его нижняя и правая стороны исключаются из внутренней структуры данных, представляющей регион. Обратите внимание: ситуация отличается от прямоугольного региона, у которого нижняя и правая стороны включаются во внутреннее представление. Таким образом, при использовании в контексте устройства прямоугольника с закругленными углами с правой и нижней стороны исключаются по два ряда пикселов. При использовании в логической системе координат ширина исключаемых краев равна одной логической единице плюс одной единице устройства. Функция 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, E l l i p s e и RouhdRectangl e исключают правую и нижнюю стороны, а в расширенном режиме эти стороны включаются в расчеты. Функция CreateRectRgn всегда исключает
509
Регионы
правую и нижнюю стороны, а функции CreateRoundRectRgn и CreateEllipticRgn исключают их дважды. Однако из рисунка не видно, что фигура, нарисованная функцией CreateEllipticRgn, несколько отличается по форме от эллипса, нарисованного функцией Ellipse, даже если учесть поправку и увеличить ее размер на единицу. Если вам нужна стопроцентная точность (например, если созданный регион требуется для отсечения результата вызова Ellipse), Microsoft рекомендует использовать функцию региона.
Прямой вызов функций 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 и CreateETlipticRgnlndirect уменьшают координаты правой и нижней сторон ограничивающего прямоугольника на единицу. Вероятно, это обстоятельство следует считать дефектом реализации, а не сознательным архитектурным решением. Во-вторых, при использовании региона в контексте устройства (с целью отсечения или при рисовании) его правая и нижняя стороны всегда исключаются. В-третьих, функции создания регионов одинаково ведут себя в обоих графических режимах, а функции рисования прямоугольников, эллипсов и прямоугольников с закругленными углами включают правую и нижнюю стороны в расширенном графическом режиме.
511
Регионы
В листинге 9.1 приведен класс KButton для работы с интерактивными кнопками, а также два производных класса для работы с прямоугольными и эллиптическими кнопками. Функция DefineButton задает ограничивающий прямоугольник кнопки. Виртуальная функция DrawButton создает объект региона и рисует кнопку в зависимости от того, был ли на ней сделан щелчок мышью. Функция IsOnButton при помощи PtlnRegion проверяет, находится ли точка (х,у) внутри кнопки. Функция UpdateButton обновляет изображение кнопки в соответствии с текущей позицией курсора мыши. Листинг 9.1. Класс KButton class KButton
{
Операции с объектами регионов Регион представляет собой множество точек двумерного пространства, поэтому определение операций над множествами для объектов регионов выглядит вполне естественно. В GDI предусмотрен богатый ассортимент функций для получения информации, перемещения, преобразования, сброса и объединения регионов. Прототипы этих функций приведены ниже. BOOL PtInRegion(HRGN hrgn, int X. int Y): BOOL RectlnRegiorKHRGN hrgn. CONST RECT * Iprc): BOOL EqualRgn(HRGN hSrcRgnl, HRGN hSrcRgn2); int GetRgnBox(HRGN hrgn. LPRECT Iprc); int
CombineRgntHRGN hrgnDest, HRGN hrgnSrcl. hrgnSrc2. int fnCombineMode);
int OffsetRgn(HRGN hrgn.int nXOffset. int nYOffset): DWORD GetRegionData(HRGN hRgn. DWORD dwCount, LPRGNDATA IpRgnData); HRGN ExtCreateRegion(CONST XFORM * IpXForm. DWORD nCount. CONST RGNDATA * IpRgnData):
Получение информации о регионе Функция PtlnRegion проверяет, принадлежит ли точка (х,у) множеству точек региона. Считается, что правая и нижняя стороны региона ему не принадлежат. Например, для пустого региона, созданного вызовом CreateRectRgnCO,0,0,0), функция PtlnRegion всегда возвращает FALSE; для региона из одной точки, созданного вызовом CreateRectRgnCO,0,1.1), функция PtlnRegion возвращает TRUE только для точки (0,0). Функция PtlnRegion чрезвычайно полезна при реализации экзотических разновидностей кнопок или интерактивных областей, изменяющих цвет под курсором мыши (что говорит о том, что щелчок на этой области обрабатывается каким-то особым образом). Приложение должно лишь создать объект региона, соответствующий интерактивной области, и вызвать функцию PtlnRegion при обработке сообщения WM_MOUSEMOVE для изменения изображения. Аналогичные действия следует включить и в обработку сообщений мыши, чтобы обнаружить щелчок внутри интерактивной области.
protected: HRGN mJiRegion; boo! 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 PtInRegion(m_hRegion. LOWORD(xy). HIWORD(xy)) != 0: Продолжение
512
Глава 9. Замкнутые области
Листинг 9.1. Продолжение class KRectButton : public KButton { public:
void DrawButtonCHDC hDC) { RECT rect = { m_x. m_y. m_x+m_w. m_y+m_h };
513
Регионы
case WM_CREATE:
rbtn.DefineButtondO. 10. 50. 50); ebtn.DefineButtondO, 70, 50, 50); return 0;
case WM_PAINT: {
PAINTSTRUCT pS;
if С mJiRegion == NULL ) mJiRegion = CreateRectRgnIndirect(& rect); InflateRect(&rect. 2. 2): FillRectChOC. & rect. GetSysColorBrush(COLOR_BTNFACE));
InflateRect(&rect. -2. -2):
HDC hDC = BeginPaint(m_hWnd. &ps): rbtn.DrawButton(hDC); ebtn.DrawButton(hDC); EndPaint(m_hWnd, &ps):
} return 0;
DrawFrameControKhDC. Srect. DFCJZAPTION. DFCS_CAPTIONHELP | (m_bOn ? 0 : DFCSJNACTIVE));
case WM_MOUSEMOVE:
{
class KEllipseButton : public KButton { public:
HDC hDC = GetDC(hWnd): rbtn.UpdateButton(hDC. IParam); ebtn.UpdateButton(hDC. IParam); ReleaseDCthWnd. hDC):
} return 0:
void DrawButton(HDC hDC)
{
case WMJ.BUTTONDOWN: RECT rect = { m_x, m_y, m_x+m_w. m_y+mji }; if ( mJiRegion == NULL ) m_hRegion = CreateEllipticRgnIndirect(& rect); if ( m_bOn ) { FillRgn(hDC. mJiRegion, GetSysColorBrush(COLOR_CAPTIONTEXT)); FrameRgrKhDC. m_hRegion. GetSysCo1orBrush(COLOR_ACTIVEBORDER), 2. 2 ) : else FillRgnChDC. mJiRegion. GetSysColorBrush(COLORJNACTIVECAPTIONTEXT)); FrameRgnthDC. m_hRegion. GetSysColorBrush(COLOR_INACTIVEBORDER). 2. 2):
Код следующего фрагмента отображает клиентскую область с двумя интерактивными кнопками, изменяющими цвет при наведении на них курсора мыши. Если щелкнуть мышью на любой из этих кнопок, на экране появляется окно с сообщением. LRESULT KMyCanvas::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam) { switch( uMsg )
if ( rbtn.IsOnButton(IParam) ) MessageBoxChWnd. "Rectangle Button Clicked". NULL. MB_OK); if ( ebtn.IsOnButton(lParam) ) MessageBox(hWnd. "Ellipse Button Clicked". NULL. MBJK): return 0: default: Ir = DefWindowProc(hWnd, uMsg, wParam. IParam):
Еще одна функция, RectlnRegion, проверяет, принадлежат ли какие-либо из точек прямоугольника, заданного параметром Iprc (за исключением правой и нижней сторон), заданному региону. Обратите внимание: функция не проверяет, находится ли весь прямоугольник внутри региона. Возможно, ее следовало бы переименовать в RectTouchRegion. Функция Equal Region сравнивает два региона и проверяет, содержат ли они одинаковые множества точек. Если в двух параметрах передаются одинаковые манипуляторы, несомненно, регионы совпадают. Но даже разные манипуляторы могут соответствовать одинаковым множествам точек. Например, вызовы CreateRectRgn(0,0,0,0) и CreateRectRgnd, 1,1.1) создают пустые регионы, которые с точки зрения функции Equal Rect являются равными. По наличию функции Equal Region можно сделать обоснованное предположение о том, что объекты регионов обладают однозначным внутренним представлением, то есть каждому множеству точек соответствует ровно одно представление. В противном случае функция Equal Region работала бы очень медленно.
514
Глава 9. Замкнутые области
Функция GetRgnBox возвращает ограничивающий прямоугольник региона. Для пустого множества точек ограничивающий прямоугольник всегда определяется квартетом {0,0,0,0}. Для других прямоугольных регионов ограничивающий прямоугольник представляет собой исходный прямоугольник региона, нормализованный таким образом, чтобы поле left было меньше right, a top — меньше bottom. Как говорилось выше, для регионов в форме эллипса или прямоугольника с закругленными углами GDI удаляет по одной единице с правой и нижней сторон, поэтому ограничивающий прямоугольник получается меньше прямоугольника, указанного при определении региона. Например, CreateEllipticRgn(10,10,l,l) возвращает регион с ограничивающим прямоугольником {1,1,9,9}. По ограничивающему прямоугольнику региона можно быстро узнать, принадлежит ли отдельная точка или какие-либо точки области заданному региону; это особенно важно при критических требованиях по быстродействию. Прямоугольник, возвращаемый функцией GetRgnBox, позволяет приложению выполнить быструю проверку без применения функций GDI и переходов из пользовательского режима в режим ядра. Прямоугольник rcPaint в структуре PAINTSTRUCT, заполняемой функцией BeginPaint, содержит данные ограничивающего прямоугольника для системного региона окна. При помощи этого прямоугольника многие приложения определяют, нужно ли перерисовывать те или иные объекты при обработке сообщения WM_PAINT.
Операции с множествами Функция CombineRgn позволяет выполнять с объектами регионов некоторые полезные операции, позаимствованные из теории множеств. Функция получает три объекта регионов hrgnDest, hrgnSrcl и hrgnSrc2, а также целочисленный параметр fnCombineMode. При вызове CombineRgn параметр hrgnDest должен содержать манипулятор действительного объекта региона. Функция заменяет объект региона, представленный этим манипулятором, объектом, сгенерированным при вызове функции. Параметр fnCombi neMode определяет операцию, выполняемую с регионами hrgnSrcl и hrgnSrc2, — копирование, пересечение, объединение, вычитание или симметричная разность. Операции с регионами, обеспечивающие пять различных режимов комбинирования регионов, были перечислены в главе 7 (см. табл. 7.1), а графическое представление этих операций приведено на рис. 9.16.
Два региона
RGN_AND
RGN_OR
RGN_XOR
Рис. 9.16. Операции с регионами
RGN_DIFF
RGN COPY
515
Регионы
Функции GetRgnBox и CombineRgn возвращают целочисленный код сложности сгенерированного региона или код ошибки. Возможные результаты перечислены в табл. 9.1. Таблица 9.1. Результаты вызовов функций GetRgnBox и CombineRgn Константа
Описание
NULLREGION
Пустой регион
SIMPLEREGION
Регион определяется одним прямоугольником
COMPLEXREGION
Регион определяется несколькими прямоугольниками
ERROR
Ошибка — недопустимые значения параметров или нехватка памяти. Регион не создастся
Если регион представляет собой множество точек, как должно выглядеть универсальное множество (то есть множество, содержащее все возможные точки)? В Win32 GDI логические координаты задаются в виде 32-разрядных целых чисел, а координаты устройства в системах семейства NT задаются 27-разрядными неотрицательными целыми числами. В системах, не входящих в семейство NT, координаты усекаются до 16-разрядных целых чисел. Следовательно, универсальное множество для регионов должно определяться ограничивающим прямоугольником [Ox80000000,Ox80000000,Ox7FFFFFFF,Ox7FFFFFFF]. Однако в системах семейства NT, похоже, GDI усекает эти числа до 28-разрядных целых, поэтому ограничивающий прямоугольник универсального множества уменьшается до [-(1«27),-(1«27),(1«27)-1,(1«27)-1]. Действует и другое недокументированное ограничение: при использовании функций регионов значения логических координат ограничиваются 28-разрядными целыми числами со знаком вместо 32-разрядных целых чисел со знаком. Операции с множествами очень полезны при геометрических вычислениях. Если вы хотите узнать, перекрываются ли две замкнутые траектории, можно преобразовать их в многоугольники функцией FlattenPath и самостоятельно реализовать алгоритм проверки перекрытия для многоугольников, но сделать это не так уж просто. Существует другое решение: преобразовать траектории в регионы и вычислить их пересечения функцией CombineRgn(RGN_AND). Если пересечение не пусто, значит, две исходные траектории пересекаются друг с другом. Такие проверки часто встречаются при программировании игр, где соприкосновение двух объектов обычно сопровождается теми или иными действиями. Используя операции с множествами, можно легко определить, содержится ли регион внутри другого региона. Выше уже говорилось о том, что функция RectlnRegion проверяет лишь факт соприкосновения, то есть наличия общих точек у прямоугольника и региона. Приведенная ниже функция проверяет, содержится ли прямоугольник внутри региона. Для этого она вычисляет объединение прямоугольника с регионом функцией CombineRgn и затем при помощи функцш EqualRgn проверяет, совпадает ли объединенный регион с исходным. BOOL RectContainedlnRegionCHRGN hrgn. CONST RECT * Iprc) {
HRGN hCombine - CreateRectRgnlndirect(lprc);
516
Глава 9. Замкнутые области CombineRgnthCombine, hrgn. hCombine. RGN_OR): BOOL rslt = EqualRgn(hCombine. hrgn); DeleteObject(hCombine); return rslt;
Преобразования данных регионов •GDI поддерживает преобразования смещения, зеркального отражения и масштабирования между страничной системой координат и системой координат устройства. В системах семейства NT интерфейс GDI поддерживает более общие аффинные преобразования между мировыми и страничными координатными пространствами, обеспечивающие возможность поворота и сдвига. Все эти преобразования поддерживаются и для объектов регионов с одним логичным ограничением — повороты и сдвиг поддерживаются напрямую только в системах семейства NT. Функция OffsetRgn обеспечивает простейшее преобразование — смещение. Она получает величины смещений по осям х и у, прибавляет их ко всем координатам объекта региона и возвращает код сложности региона. Функция OffsetRgn может применяться для многократного рисования объектов на поверхности устройства (прорисовкой самого региона или его применением для отсечения). Приложение может использовать регион, переместить его в другое место функцией OffsetRgn и воспользоваться им снова. Кроме того, при помощи этой функции, можно отслеживать движущиеся объекты в игре или анимационном ролике. Если, например, регион описывает контуры гоночной машины, то при движении машины должен перемещаться и регион, используемый для обнаружения столкновений. Более общие преобразования выполняются двумя функциями: GetRegionData и ExtCreateRegion. Функция GetRegionData преобразует внутреннюю структуру данных региона в структуру RGNDATA, которая может использоваться в программе. Функция ExtCreateRegion получает структуры RGNDATA и XFORM (определение аффинного преобразования), преобразует данные и создает новый регион. Важнейшая структура 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 регионы представляются более эффективной структурой данных — динамическим массивом REGIONOBJ, содержащим массив структур SCAN. Структу-
регионы
517
ра SCAN описывает «строку развертки региона», то есть пересечение региона с областью, ограниченной двумя горизонтальными линиями, при условии, что пересечение контура региона с этой областью состоит только из вертикальных отрезков. В структуре SCAN хранится массив координат х этих отрезков, причем количество элементов массива всегда четно. Нет никаких фактов, которые бы подтверждали, что регионы представляются трапециевидными фигурами, как заявлено в документации Microsoft. Описание региона массивом структур SCAN позволяет хранить для каждого пересечения только координату х, поскольку координаты у у них одинаковые; тем самым обеспечивается экономия памяти. Кроме того, упорядоченность массива структур SCAN сверху вниз и слева направо обеспечивает однозначное представление регионов и эффективные операции с ними. Например, при объединении нескольких мелких регионов в один большой регион функцией CombineRgn внутреннее представление итогового региона не должно зависеть от порядка объединения регионов. За подробностями обращайтесь к разделу «WinDbg и расширение отладчика GDI» в главе 3. В системах, не входящих в семейство NT, вместо 32-разрядных координат используются 16-разрядные. Объем памяти, занимаемой структурой REGIONOBJ, зависит от сложности региона. Предположим, регион делится на N строк развертки и среднее количество пересечений на строку равно М. Минимальная высота строки развертки равна единице, но может быть равна и нескольким единицам. Объем структуры REGIONOBJ вычисляется по формуле: sizeof(REGIONOBJ) = (4*М + 16)*(М+2)+40
Для прямоугольного региона одна строка развертки занимает весь прямоугольник, поэтому N = 1, М = 2; объем структуры равен всего 112 байтам. GDI может хранить только ограничивающий прямоугольник и специальный флаг, который указывает на то, что это простой регион. Для эллиптического региона количество строк развертки приближается к 2/3 высоты эллипса, М = 2. При создании региона для эллипса, занимающего всю страницу на принтере с разрешением 600 dpi, N = 2/3 х 11 х 600 = 4400, si zeof (REGIONOBJ) = 111 Кбайт. Благодаря регионам приложение может реализовать цветовые ключи; для этого регион создается на базе всех пикселов растра, цвет которых отличается от цветового ключа. Этот регион используется для отсечения при выводе растра, в результате чего все пикселы, цвет которых совпадает с цветом ключа, не выводятся. В худшем случае значение N равно высоте растра, М — половине ширины растра, а структура REGIONOBJ содержит по 2 байта на каждый пиксел. Если ваше приложение интенсивно работает с регионами, не забывайте о затратах памяти. В действительности графический механизм выделяет больше памяти, чем необходимо для представления региона; излишек предназначен для возможного Увеличения размера растра. Аналогичная стратегия используется при работе с Динамическими массивами для сведения к минимуму затрат на динамическое 'Выделение памяти и копирование данных. Структура REGIONOBJ фактически является двумерной; первое измерение Представляет собой массив структур SCAN, упорядоченных по возрастанию коорДИнаты у, а второе измерение — упорядоченный массив координат х. Такая Архитектура обеспечивает приемлемое быстродействие операций с регионами.
518
Глава 9. Замкнутые области
Предположим, требуется узнать, принадлежит ли точка региону. Если точка прошла проверку на принадлежность ограничивающему прямоугольнику, ее координату у можно сравнить с координатой у каждой структуры SCAN методом линейного поиска. Размер структуры SCAN хранится в ее начале и в конце, что значительно упрощает переход к следующей структуре. После нахождения нужной структуры SCAN производится следующий линейный поиск по координате х. . Таким образом, время выполнения PtlnRegion имеет порядок 0 ( N ) + 0(М). Оптимальный алгоритм с применением бинарного поиска обеспечивает порядок OClog(N)) + Odog(M)), но это требует усложнения структуры данных. GDI по возможности старается использовать небольшие структуры данных, объединяемые указателями, чтобы свести к минимуму динамическое выделение памяти. Функция CombineRgn для объединения, пересечения и вычитания регионов обладает аналогичной сложностью в отношении количества необходимых сравнений. Впрочем, копирование данных в новый регион требует дополнительного времени. Таким образом, при объединении п регионов функцией CombineRgn в худшем случае сложность имеет порядок О(п2), то есть с удвоением количества регионов затраты времени возрастают в четыре раза. Подобных алгоритмов следует по возможности избегать. Структура RGNDATA обеспечивает единый интерфейс для работы с регионами в приложениях Win32, работающих на разных платформах. Кроме того, структура RGNDATA используется интерфейсом IDirectDrawClipper DirectDraw. Эта структура содержит заголовок фиксированного размера с информацией о размере региона, типе и ограничивающем прямоугольнике, а также массив структур RECT. В RGNDATA двумерное внутреннее представление региона в GDI преобразуется в одномерную структуру данных. Для представления региона с N строками развертки и средним количеством пересечений на строку, равным М, необходимо М/2 х N прямоугольников. Общие затраты памяти вычисляются по следующей формуле:
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 окрашены попеременно в темно-серый и светло-серый цвета.
SizeofCRGNDATA} = 8 * M * N + 32
Для региона, состоящего из одной выпуклой фигуры (например, прямоугольника, эллипса или прямоугольника с закругленными углами) М = 2, поэтому структура RGNDATA занимает примерно 2/3 от объема REGIONOBJ. При больших значениях М объем структуры RGNDATA почти вдвое превышает объем REGIONOBJ. Структура RGNDATA (как и структура REGIONOBJ) генерируется GDI, и ее элементы всегда располагаются в строго определенном порядке. Входящие в нее структуры RECT упорядочиваются слева направо, сверху вниз. Все структуры RECT нормализованы, то есть поле left меньше right, a top меньше bottom. Поскольку структура RGNDATA представляет собой линейный массив прямоугольников, приложение может ускорить работу некоторых алгоритмов. Например, проверку наличия общих точек у прямоугольника с регионом, представленным структурой RGNDATA, можно осуществить с применением бинарного поиска в массиве вместо линейного, что позволяет уменьшить сложность до OOogCM x N)). С другой стороны, при операциях с множествами, использующими CombineRgn, производится копирование данных, при этом затраты времени связаны линейной зависимостью с объемом данных. Функция GetRegionData записывает структуру RGNDATA в буфер, предоставленный приложением. Впрочем, перед вызовом GetRegionData точный размер струк-
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, определяется в системе координат устройства. Если регион используется непосредственно для рисования, а не для отсечения, приложение должно преобразовать его в логическую систему координат. Для простейшего смещения достаточно функции OffsetRgn, но для более общих преобразований следует воспользоваться функцией ExtCreateRegion. Структура RGNDATA представляет регион в целочисленных координатах; кривые аппроксимируются отрезками, как при вызове FlattenPath для траектории. Следовательно, при масштабировании часто возникают «зазубрины». Если регион можно определить в виде траектории, преобразование траектории с последующим переходом к региону обеспечит более точный результат. По имеющейся информации функция ExtCreateRegion в системах, не входящих в семейство NT, не может одновременно работать более чем с 4000 прямоугольников. Существует обходное решение — разделить большую структуру RGNDATA на несколько меньших, вызвать ExtCreateRegion для каждой структуры, а затем объединить результаты функцией CombineRgn. В листинге 9.2 приведен простой класс для работы с функциями GetRegionData и ExtCreateRegion. Листинг9.2. Класс KRegion: работа сданными региона class KRegion {
public: int i nt RECT * RGNDATA *
ResetO: BOOL GetRegionData(HRGN hRgn): HRGN CreateRegionUFORM * pXForm);
BOOL KRegion::GetReg1onData(HRGN hRgn) { ResetO: mjiRegionSize = ::GetRegionData(hRgn. 0. NULL); if ( mjiRegionSize==0 ) return FALSE; mjjRegion = (RGNDATA *) new char[mjiRegionSize]; if ( m_pRegion==NULL ) return FALSE; ::GetRegionData(hRgn, mjiRegionSize. m_pRegion); mjiRectCount = m_pRegion->rdh.nCount; mj>Rect = (RECT *) & m_pRegion->Buffer: return TRUE:
HRGN KRegion::CreateRegion(XFORM * pXForm) mjnRegionSize; m_nRectCount; m_pRect; m_pRegion:
KRegion() { mjiRegionSize = 0: mjnRectCount = 0: m_pRegion = NULL: mjjRect = NULL: void Reset(void) { if ( m_pRegion ) delete [] (char *) m_pReg1on: m_pRegion = mjnRegionSize = mjiRectCount = m_pRect = -KRegionО
521
Регионы
NULL; 0: 0: NULL:
return ExtCreateRegiontpXForm, mjiRegionSize. m_pRegion); } Функции GetRegionData и ExtCreateRegion также позволяют приложениям самостоятельно строить и преобразовывать структуры RGNDATA и передавать их GDI для создания регионов. Такая возможность может пригодиться для реализации поворотов или сдвигов в системе, не входящей в семейство NT, или для преодоления нежелательных затрат О(и2) при объединении я регионов функцией CombineRgn.
Прорисовка регионов В GDI предусмотрено несколько функций для прорисовки области, занимаемой регионом с заданным манипулятором: BOOL FillRgn(HDC hDC. HRGN hrgn. HBRUSH hbr): BOOL PaintRgntHDC hDC. HRGN hrgn): BOOL FrameRgn(HDC hDC. HRGN hrgn. HBRUSH hbr. int nWidth. int nHeight): BOOL InvertRgn(HDC hDC. HRGN hrgn); Все эти функции получают манипулятор контекста устройства и манипулятор региона. Координаты региона задаются в логической системе координат,
522
Глава 9. Замкнутые области
а не в системе координат устройства, как координаты регионов отсечения. Следовательно, этим функциям нельзя непосредственно передать манипулятор региона, возвращаемый функцией PathToRegion (разве что логические координаты идентичны координатам устройства или же вы действуете сознательно). Графический механизм преобразует объект региона в координаты устройства с исключением правой и нижней сторон. Функция F i l l R g n закрашивает регион кистью, определяемой параметром hbr. Функция PaintRgn делает то же самое, но задействует текущую кисть контекста устройства. В GDI для этих двух функций используется одна и та же реализация. Функция FrameRgn обводит контур региона кистью, ширина и высота которой определяются при вызове функции. Это позволяет создавать контуры переменной толщины; при использовании обычного пера, которое всегда рисует линии постоянной толщины, это невозможно. Функция FrameRgn интерпретирует свое «перо» как прямоугольник, параллельный осям, причем весь вывод осуществляется только внутри региона и никогда не выходит за его пределы. Функция InvertRgn инвертирует пикселы кадрового буфера устройства так же, как при использовании растровой операции R2_NOT. По принципу работы она напоминает функцию InvertRect. Первые три функции, FillRgn, PaintRgn и FrameRgn, используют текущую бинарную растровую операцию и учитывают действующий режим заполнения фона. Функция InvertRgn инвертирует весь регион операцией R2_NOT. На рис. 9.18 показано, как перечисленные функции трансформируют закругленный прямоугольник, нарисованный функцией RoundRect. PaintRgn
RoundRect
FrameRgn(1,1)
FillRgn
FrameRgn(10,1)
FrameRgn|l,10)
InvertRgn
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 JRIVERTEX { LONG x; LONG y: COLOR16 Red: COLOR16 Green; COLOR16 Blue; COLOR16 Alpha; } TRIVERTEX. * PTRIVERTEX. * LPTRIVERTEX: typedef Struct _GRADIENT_TRIANGLE { ULONG Vertexl; ••/ ULONG Vertex2: ULONG VertexS; } GRADIENT TRIANGLE. *PGRADIENT_TRIANGLE. "LPGRADIENT TRIANGLE; 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); '" Функция GradientFill обладает рядом отличительных особенностей. Во-первых, 'перед типами указателей отсутствуют префиксы long и far, унаследованные от Winl6. Во-вторых, к традиционному 3-канальному формату RGB добавился новый альфа-канал. В-третьих, 8-разрядных цветовых каналов оказывается недостаточно, поэтому используются 16-разрядные каналы. Все это наглядно свидетельствует о постепенном совершенствовании API. • Функция GradientFill заполняет один или несколько прямоугольников (или треугольников - в зависимости от последнего параметра dwMode). В настоящий Момент параметр dwMode может принимать три допустимых значения, перечисленных в табл. 9.2.
522
Глава 9. Замкнутые области
а не в системе координат устройства, как координаты регионов отсечения. Следовательно, этим функциям нельзя непосредственно передать манипулятор региона, возвращаемый функцией PathToRegion (разве что логические координаты идентичны координатам устройства или же вы действуете сознательно). Графический механизм преобразует объект региона в координаты устройства с исключением правой и нижней сторон. Функция FillRgn закрашивает регион кистью, определяемой параметром hbr. Функция PaintRgn делает то же самое, но задействует текущую кисть контекста устройства. В GDI для этих двух функций используется одна и та же реализация. Функция FrameRgn обводит контур региона кистью, ширина и высота которой определяются при вызове функции. Это позволяет создавать контуры переменной толщины; при использовании обычного пера, которое всегда рисует линии постоянной толщины, это невозможно. Функция FrameRgn интерпретирует свое «перо» как прямоугольник, параллельный осям, причем весь вывод осуществляется только внутри региона и никогда не выходит за его пределы. Функция InvertRgn инвертирует пикселы кадрового буфера устройства так же, как при использовании растровой операции R2_NOT. По принципу работы она напоминает функцию InvertRect. Первые три функции, FillRgn, PaintRgn и FrameRgn, используют текущую бинарную растровую операцию и учитывают действующий режим заполнения фона. Функция InvertRgn инвертирует весь регион операцией R2_NOT. На рис. 9.18 показано, как перечисленные функции трансформируют закругленный прямоугольник, нарисованный функцией RoundRect. FrameHgn(1.1]
PaintRgn
Г/
'\N
FrameRgn(10,1)
InvertRgn
Градиентные заливки
523
Градиентные заливки До недавнего времени средства GDI позволяли закрасить замкнутую область одноцветной однородной кистью, двуцветной штриховой кистью или узорной кистью, количество цветов в которой определялось количеством цветов в растре. Но с распространением видеоадаптеров и принтеров, обладающих повышенной цветовой глубиной, приложения начали использовать большее количество цветов, чтобы изображение выглядело более привлекательно. Одной из разновидностей цветовых эффектов являются градиентные заливки - заполнение области многочисленными цветами, сгенерированными по определенному правилу. Поддержка градиентных заливок впервые была реализована в профессиональных приложениях (таких, как Photoshop, CorelDraw и Microsoft Office). Начиная с Windows 98 и Windows 2000, градиентные заливки стали частью GDI. В Win32 GDI поддержка градиентных заливок обеспечивается одной функцией, работа которой определяется тремя новыми структурами данных.
typedef struct JRIVERTEX { LONG x: LONG y; COLOR16 Red; COLOR16 Green: COLOR16 Blue; COLOR16 Alpha; } TRIVERTEX. * PTRIVERTEX, * LPTRIVERTEX; typedef struct _GRADIENT_TRIANGLE { ULONG Vertexl:
ULONG Vertex2:
£££ШШШШ& ж*т»жття*
ULONG VertexS; } GRADIENT TRIANGLE. *PGRADIENT_TRIANGLE. tPGRADIENTJRIANGLE: RoundRect
FillRgn
FrameRgn(1,10)
FrameRgn(10,10)
Рис. 9.18. Функции PaintRgn, FillRgn, FrameRgn и InvertRgn
На первый взгляд функции регионов принципиально не отличаются от других функций GDI, однако создание объектов регионов и операции с ними связаны со значительными затратам времени и памяти, особенно при усложнении формы региона. Если форму региона можно легко воспроизвести другими средствами GDI (функциями рисования прямоугольников, эллипсов, закругленных прямоугольников и траекторий), предпочтение следует отдать этому способу. Функции прорисовки регионов следует использовать для замены более дорогостоящих операций — например, функций вывода отдельных пикселов или заливок. Допустим, приложение рисует на экране два перекрывающихся круга и хочет закрасить общую область некоторой кистью. В современных реализациях GDI получить определения двух дуг, ограничивающих эту область, не так просто, зато средствами GDI можно легко вычислить пересечение двух круговых регионов.
typedef struct _GRADIENT_RECT { ULONG Upper-Left; ULONG LowerRight; } GRADIENT RECT. *PGRADIENT_RECT, "LPGRADIENT RECT; BOOL GradientFilKHDC hDC, CONST PTRIVERTEX pVertex. DWORD dwNumVertex. CONST PVOID pMesh. DWORD dwNumMesh. DWORD dwMode); ' Функция GradientFill обладает рядом отличительных особенностей. Во-первых, перед типами указателей отсутствуют префиксы long и far, унаследованные от WinlG. Во-вторых, к традиционному 3-канальному формату RGB добавился новый альфа-канал. В-третьих, 8-разрядных цветовых каналов оказывается недостаточно, поэтому используются 16-разрядные каналы. Все это наглядно свидетельствует о постепенном совершенствовании API. Функция GradientFill заполняет один или несколько прямоугольников (или треугольников - в зависимости от последнего параметра dwMode). В настоящий момент параметр dwMode может принимать три допустимых значения, перечисленных в табл. 9.2.
524
Глава 9. Замкнутые области
Таблица 9.2. Режимы функции GradientFill
Значение параметра dwMode
Смысл
GRADIENT FILL RECT H
Прямоугольник заполняется цветами, изменяющимися слева направо. По вертикали цвет остается постоянным
• GRADIENT FILL RECT V
Прямоугольник заполняется цветами, изменяющимися сверху вниз. По горизонтали цвет остается постоянным
GRADIENT FILL RECT TRIANGLE
525
Градиентные заливки
Градиентная заливка прямоугольников Чтобы изучить использование функции GradientFill на конкретном примере, давайте попробуем создать разные градиентные заливки для одного прямоугольного региона. Сколько вариантов вы сможете изобрести? 14 самых распространенных комбинаций изображены на рис. 9.19.
Треугольник заполняется цветами, интерполированными по трем вершинам
Отдельный прямоугольник или треугольник называется «ячейкой» (mesh) — этот жаргонный термин пришел из программирования компьютерных игр. Количество ячеек передается в параметре dwNumMesh; указатель pMesh ссылается на массив структур (либо GRADIENT_RECT, либо GRADIENTJRIANGLE). Структура GRADIENT^ RECT содержит индексы левого верхнего и правого нижнего угла прямоугольника. Структура GRADIENT_TRIANGLE содержит индексы трех вершин треугольника. Индексы относятся к массиву TRIVERTEX, на который ссылается параметр pVertex. Параметр dwNumVertex определяет количество элементов в массиве TRIVERTEX. Итак, для вершины каждого прямоугольника или треугольника существует структура TRIVERTEX, определяющая ее позицию и цвет. Позиция задается в логической системе координат с использованием 32-разрядных значений. Их цвет определяется четырьмя 16-разрядными каналами (красный, зеленый, синий и альфа-канал). Для горизонтальной градиентной заливки прямоугольника, если левый верхний угол имеет координаты (хО,уО), а правый нижний — (х1,у1), цвет точки (х,у) вычисляется по формуле: С(х.у) = ( C(xl.yl) * (х-хО) + С(хО.уО) * (xl-x) ) / (xl-xO) Здесь С(х,у) означает интенсивность одного из цветовых каналов в точке (х,у). При вертикальной градиентной заливке прямоугольников используется аналогичная формула, зависящая от координаты у: С(х.у) = ( C ( x l , y l ) * (у-уО) + С(хО.уО) * (yl-y) ) / (yl-yO) С градиентными заливками треугольников дело обстоит несколько сложнее. Если три вершины имеют координаты (хО,уО), (х1,у1) и (х2,у2), то из внутренней точки (х,у) можно провести три отрезка, разделяющие треугольник на три меньших треугольника. Если т — площадь треугольника, противолежащего по отношению к точке (хг,уг), цвет в точке (х,у) вычисляется по формуле: С(х.у) = ( С(хО.уО) * аО + C ( x l . y l ) * al + С(х2.у2) * а2 ) / (аО + al + a2) Для прямоугольников формула интерполяции зависит от расстояния, поэтому вполне естественно, что формула интерполяции для треугольников зависит от площади. Для прямоугольников цвет образует прямую линию на плоскости, образованной одной из осей и каждым цветовым каналом. При градиентной заливке треугольника цвет образует плоскость в трехмерном пространстве, образованном осями х, у и каждым цветовым каналом.
Рис. 9.19. Градиентная заливка прямоугольной области
В верхних четырех заливках цвет изменяется в одном направлении — слева направо, сверху вниз или по диагонали. В следующем ряду прямоугольник делится на две части, и заполнение осуществляется от центра в противоположных направлениях. В нижнем ряду заливка распространяется из одного угла на весь прямоугольник. В двух последних примерах (справа) заливка идет от наружного контура в центр. Программный код, при помощи которого был построен этот рисунок, частично приведен в листинге 9.3. Листинг 9.3. Градиентная заливка прямоугольных областей inline COLOR16 R16(COLORREF с) { return GetRValue(c)«8: } inline COLOR16 G16(COLORREF с) { return GetGValue(c)«8; } inline COLOR16 B16(COLORREF c) { return GetBValue(c)«8: } inline COLOR16 R16(COLORREF cO. COLORREF cl) { return ((GetRValue(cO)+GetRValue(cl))/2)«8; } inline COLOR16 G16(COLORREF cO. COLORREF cl) { return ((GetGValue(cO)+GetGValue(cl))/2)«8; } inline COLOR16 B16(COLORREF cO. COLORREF cl) { return ((GetBVa1ue(cO)+GetBValue(cl))/2)«8: } BOOL GradientRectangle(HDC hDC, int xO. int yO. int xl. int yl COLORREF cO. COLORREF cl, int angle) { TRIVERTEX vert[4] = { { xO yO. R16(cO). G16(cO). B16(cO), 0 }. { xl yl R16(cl). G16(cl), B16(cl). О }.
Продолжение^
526
Глава 9. Замкнутые области
Листинг 9.3. Продолжение { xO. yl. { xl. yO.
R16(cO. cl). G16(cO. cl). B16(cO. c l ) . О }. R16(cO, cl). G16(cO. cl). B16(cO. cl). 0 }
}:
ULONG Index[] = { 0. 1. 2. 0. 1. 3}: switch ( angle * 180 ) {
case
0:
return GradientFilKhDC. vert. 2, Index. 1, GRADIENTJILLJECTJO: case 45: return GradientFilKhDC. vert, 4, Index.2, GRADIENTJILLJRIANGLE); case 90: return GradientFilKhDC. vert. 2. Index. 1. GRADIENTJILLJECTJ):
}
Градиентные заливки
527
Второй и четвертый примеры нарисованы посредством треугольной заливки. функция CornerGradientRectangle рисует четыре заливки в третьем ряду, во всех случаях используется комбинация двух треугольников. В приведенном фрагменте определено несколько подставляемых функций для преобразования 8-разрядных значений RGB в 16-разрядные, используемые в структуре TRIVERTEX, и вычисления усредненных цветов. Обратите внимание: в приведенном примере не задействованы структуры GRADIENT_RECT и GRADIENT_TRIANGLE; вместо этого мы напрямую работаем с массивами длинных беззнаковых индексов.
Применение градиентных заливок для создания объемных кнопок Комбинация нескольких градиентных заливок создает интересные эффекты. Благодаря механизму отсечения градиентные заливки можно применять и к непрямоугольным областям; например, это позволяет имитировать объемный вид кнопок. На рис. 9.20 изображены три объемные кнопки, созданные при помощи функции GradientRectangle.
case 135: vert[0].x = xl: vert[3].x = xO: vert[l].x = x O : vert[2].x = xl; return GradientFilKhDC. vert. 4, Index, 2, GRADIENTJILLJRIANGLE):
return FALSE:
BOOL CornerGradientRectangletHDC 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(cO): vert[corner].Green = G16(cO): vert[corner].Blue = B16(cO): ULONG Index[] = { corner. (согпег+Ш4, (corner+2)X4. corner. (corner+3)X4.. (согпег+2Д4 }:
}
return GradientFilKhDC. vert. 4. Index, 2. GRADIENTJILLJRIANGLE):
Функция 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. Замкнутые области
Практическое использование заливок
. . У. Р[2].х-Р[0].х. Р[2].у-Р[0].у): SelectClipRgn(hDC, hRgn); DeleteObject(hRgn); ! if ( i==0 ) GradientRectangle(hDC. xO. yO. xl. yl. cl. cO, 45): else
}
GradientRectangle(hDC. xO+d. yO+d. xl-d, yl-d. cO, cl. 45):
SelectClipRgn(hDC. NULL):
}
Функция в цикле выполняет две градиентные заливки с разными размерами. Отсечение несколько усложняется тем, что для создания правильной области отсечения функция должна определять ее размеры и положение в системе координат устройства. Впрочем, эти небольшие дополнительные усилия позволяют использовать функцию в любой логической системе координат.
Практическое использование заливок Заливки являются важным аспектом любых графических приложений, от простейших текстовых и графических редакторов до сложных пакетов профессиональной графики. В GDI поддерживаются три основных средства для создания заливок: О кисти и градиенты, определяющие цвет и узор заливки; О функции заливки, позволяющие непосредственно закрашивать простые геометрические фигуры; О механизм отсечения, обеспечивающий свободу выбора границ закрашиваемой области. Поддержка заливок в GDI достаточно близка к возможностям, поддерживаемым в современных графических пакетах: О О О О О
одноцветные однородные заливки (включая полупрозрачные); градиентные заливки; текстурные заливки; узорные заливки; растровые заливки.
Полупрозрачная заливка Одноцветные однородные заливки легко создаются при помощи однородных кистей GDI. При создании полупрозрачной заливки каждый второй пиксел закрашивается определенным цветом, а остальные пикселы приемника остаются без изменений. Для решения этой задачи можно воспользоваться шахматной узорной кистью и двумя бинарными растровыми операциями. Следующая функция создает полупрозрачную заливку в прямоугольнике.
529
void SemiFillRecttHDC hDC. Int left, int top. int right, int bottom. COLORREF color) { int nSave = SaveDC(hOC): 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): HGOIOBJ hOldBrush = Se1ectObject(hDC. hBrush): HGDIOBJ hOldPen = SelectObjectChDC. GetStockObject(NULL_PEN)); SetROP2(hDC. R2_MASKPEN); SetBkColorChOC. RGBCOxFF. OxFF. OxFF)); // Без изменений цвета SetTextColorChDC, RGB(0, 0. 0)): // Черный цвет Rectangle(hDC. left. top. right, bottom); SetROP2(hDC. R2_MERGEPEN); SetBkColor(nDC. RGBCOxO. 0x0, 0x0)); // Без изменений цвета SetTextColor(hDC. color); // Заданный цвет Rectangle(hDC. left. top. right, bottom); SelectObjectChDC, hOldBrush): SelectObjecUhDC. hOldPen); DeleteObject(hBrush); RestoreDC(hDC, nSave): } Функция SemiFillRect создает узорную кисть с шахматным узором. При первом вызове функции Rectangle используется растровая операция R2_MASKPEN, в результате чего основные пикселы окрашиваются в черный цвет (0), а фоновые пикселы остаются без изменений. При втором вызове функции Rectangle операция R2_MERGEPEN окрашивает основные пикселы в заданный цвет, а фоновые пикселы по-прежнему остаются неизмененными. Тернарные растровые операции (см. следующую главу) позволяют обойтись всего одним вызовом функции при использовании шахматной кисти.
Реализация градиентных заливок в цветовом пространстве HLS В Windows 98 и Windows 2000 на уровне GDI реализована неплохая поддержка градиентных заливок, и все же без проблем не обошлось. По имеющейся информации градиентные заливки в Windows 98 приводят к утечке ресурсов, поэтому часто пользоваться ими не рекомендуется. В Windows NT 4.0 и Windows 95 градиентные заливки на уровне GDI не поддерживаются, если не считать линейной интерполяции в пространстве RGB. По этим причинам в приложениях иногда возникает необходимость в самостоятельной реализации градиентных заливок. Заливки треугольников лучше выполняются при помощи операций с растрами, но градиентные заливки прямоугольников легко имитируются закраской проме-
532
Глава 9. Замкнутые области
Текстурные и растровые заливки Текстурной заливкой (texture fill) называется заполнение области растром, изображающим текстуру конкретного материала — скажем, бумаги, мрамора, гранита, песка или дерева. Для реализации текстурных заливок можно было бы воспользоваться узорными кистями GDI, но при этом возникает пара проблем. Во-первых, в системах, не входящих в семейство NT, узорные кисти ограничиваются размерами 8 x 8 пикселов. Во-вторых, в GDI узоры определяются в координатах устройства и не масштабируются в соответствии с разрешением устройства. Растры 8 x 8 годятся разве что для очень мелких текстур, отображаемых на экране. Текстурный растр, который хорошо смотрится на экране с разрешением 96 dpi, будет практически неразличим на принтере с разрешением 1200 dpi. Например, текстура, имитирующая деревянную поверхность, сильно зависит от разрешения устройства. Под растровой заливкой (bitmap fill) понимается растяжение растра по размерам заполняемой области. Текстурные и растровые заливки связаны с растрами, подробно описанными в следующей главе, поэтому мы оставляем эту тему на будущее.
LineTo(hDC. x+wldth/2. y+height/2);
Функция использует только логические координаты, поэтому построенный узор легко преобразуется. В приведенном примере не поддерживается отсечение по определяющему прямоугольнику, непрозрачная закраска фона и выравнивание базовой точки кисти. Пример вывода иллюстрирует рис. 9.22.
Рис. 9.22. Смешение при рисовании однородной кистью на устройствах, использующих палитру
Узорные заливки GDI предоставляет в распоряжение программиста несколько стандартных штриховых узоров для закраски замкнутых фигур двумя цветами. Штриховые кисти первоначально ориентировались на экранный вывод. Хотя в NT интерфейс DDI позволяет драйверам принтеров предоставлять собственные масштабированные растры для реализации штриховых кистей, эта возможность используется лишь немногими драйверами принтеров. Если приложение задействует Б1триховые кисти для вывода на экран, штриховой узор не масштабируется в режимах с разрешением 72, 96 или 120 dpi. При изменении масштаба изображения узор остается прежним. Если приложение задействует штриховые кисти при печати, разглядеть полученный узор удастся разве что под микроскопом. Узорные заливки довольно просто имитируются последовательностью линий, причем это дает возможность задавать переменную толщину пера и координаты в логическом координатном пространстве. Простая функция, приведенная ниже, с использованием линий строит наклонный узор в виде «кирпичной кладки». void BrickPatternFilKHDC hDC. int x O . int yO. int xl, int yl. int width, int height) { width = abs(width): height = abs(height): if ( xOxl ) { int t = x O : xO = xl; xl = t: } if ( yOyl ) { int t = y O ; yO - yl: yl = t; } for (int y=yO; y
533
Итоги
height ) width ) y. NULL); y+height): y. NULL):
Итоги В этой главе рассматриваются средства GDI, предназначенные для заполнения замкнутых областей — кисти, заливки, регионы и модные градиентные заливки. В отличие от перьев, обладающих собственными геометрическими размерами, кисть определяет только способ размещения цветового шаблона внутри замкнутой области. Мы достаточно подробно изучили ситуации, при которых ограниченные возможности кистей GDI не соответствуют требованиям современных приложений, разобрались в проблемах несовместимости между операционными системами и рассмотрели некоторые обходные пути, решения и общие рекомендации. Кисть, создаваемая средствами GDI, представляет собой логическую спецификацию реальной кисти, которая задействуется драйвером устройства при рисовании и обычно является аппаратно-зависимой. При первом использовании Новой логической кисти графический механизм обращается к драйверу устройства с требованием реализовать логическую кисть, то есть создать физическую структуру данных на основании логической кисти. После этого реализованный объект кисти передается всем функциям драйвера, использующим кисть. Дополнительная информация о внутренней структуре данных кисти приведена при описании других объектов GDI в главе 3 (раздел «WinDbg и расширение отладчика GDI»). В GDI предусмотрено довольно много функций для рисования простых геометрических фигур (прямоугольников, закругленных прямоугольников, эллипсов и многоугольников). Контуры таких фигур обводятся пером, а внутренняя Часть закрашивается кистью. Более сложные фигуры строятся с использова-
Глава 9. Замкнутые области
534
нием траекторий, объединяющих разные типы кривых. На уровне DDI практически все контуры преобразуются в траектории, а большая часть вызовов заполнения замкнутых областей обрабатывается внутренней реализацией функции StrokeAndFillPath. Даже многоугольники и совокупности многоугольников представляют собой траектории, состоящие только из прямых линий. Единственным исключением являются прямоугольники в совместимом графическом режиме; для них используется более простая точка входа 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). В следующих главах будут рассматриваться практические применения - прозрачный вывод растров, альфа-наложение на фоновый рисунок, постепенная «проявка» и исчезновение, повороты растров и т. д.
Аппаратно-независимые растры При вводе оцифрованной графики с устройств данные изображения необходимо преобразовать в формат, подходящий для хранения на жестком диске^ компьютера или другом носителе, а также для передачи на удаленное устройство. 3 наши дни с форматами графических изображений возникает немало хлопот. Разные операционные системы, фирмы-разработчики аппаратуры и даже при-
536
Глава 10. Основные сведения о растрах
ложения работают с графикой, хранящейся в разных форматах. К числу наиболее распространенных растровых форматов относятся следующие:
Заголовок растрового файла (BITMAPFILEHEADER)
О 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 обычно называются аппаратно-независимыми (DeviceIndependent Bitmap, DIB). Определение «аппаратно-независимый» означает, что формат содержит полную информацию об изображении и позволяет воспроизвести его на разных устройствах. Первоначально этот термин не означал, что растр кодируется в аппаратно-независимом цветовом пространстве, хотя новые версии операционных систем Microsoft включают в формат BMP данные цветовых профилей, чтобы компенсировать зависимость от цветовых устройств. Аппаратно-независимым растрам противопоставляется другой формат изображений, используемых во внутренней работе графической системы Windows — аппаратно-зависимые растры (Device-Dependent Bitmaps, DDB). В этом разделе мы сначала рассмотрим DIB как фундаментальный графический формат Windows, а затем перейдем к DDB и другому растровому формату — DIB-секциям. Аппаратно-независимый растр, или DIB, хранящийся в файле на диске, состоит из трех основных компонентов: заголовка растрового файла, блока описания растра и массива пикселов. Блок описания растра может дополнительно делиться на заголовок, массив масок и цветовую таблицу (в зависимости от цветовой глубины растра). На рис. 10.1 показана структура изображения формата DIB в дисковом файле.
Bitmap Information
О JPEG (разработчик — Joint Photographic Experts Group) — 24-разрядный цветной формат со сжатием и потерей данных; О TIFF (разработчик — Aldus ) — очень гибкий графический формат с поддержкой различных субформатов, порядка байтов MAC и PC, а также сжатия LZW;
537
Аппаратно-независимые растры
Заголовок блока с информацией о растре (BITMAPCOLORHEADER, BITMAP1NFOHEADER, BITMAPV4HEADER, orBITMAPVSHEADER) Битовая маска (DWORD Ц) Цветовая таблица (RGBTRIPLE[], RGBQUAD [])
Массив пикселов (Pixel [][])
Рис. 10.1. Файловый формат BMP
Заголовок растрового файла Заголовок растрового файла содержит простую информацию, по которой приложения идентифицируют BMP-файлы. Он состоит из трех основных компонентов: сигнатуры BMP-файла, поля длины файла и смешения массива пикселов. Заголовок определяется в структуре BITMAPFILEHEADER. typedef struct tagBITMAPFILEHEADER // Сигнатура BMP-файла WORD bfType; // Общий размер файла DWORD bfSize: // О DWORD bfReservedl: // О DWORD bfReserved2: // Смещение массива пикселов от начала файла DWORD bfOffBits: } BITMAPFILEHEADER; Сигнатура bfType в BMP-файлах состоит из двух ASCII-символов, «В» и «М», поэтому файл всегда начинается со значения Ox4D42, или "М" * 256 + "В 1 . В поле bfSize хранится общий размер графического файла (это удобно при загрузке файлов с удаленного компьютера). В последнем поле структуры хранится смещение массива пикселов от начала графического файла. Следует помнить о том, что структура BITMAPFILEHEADER изначально проектировалась для 16-разрядных версий Windows, поэтому она выравнивается по границе слов, а не двойных слов. Общий размер структуры равен 14 байтам, в результате чего заголовок блока описания растра тоже не выравнивается по границе двойного слова. Это обстоятельство может вызвать проблемы при попытке сохранения DIB-секции в растровом файле, отображаемом на память (memorymapped).
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 MYPelsPerMeter; // вертикальное разрешение DWORD biClrllsed; // общий размер цветовой таблицы DWORD biClrlmportant; // количество цветов, необходимых для вывода } 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. В структуре версии 5 добавились новые типы цветовых пространств, рекомендации по воспроизведению и данные
539
Аппаратно-независимые растры
цветового профиля, ориентированные на поддержку ICM 2.O. Подробные описания этих структур приведены в 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 Значение
Максимальное количество цветов
О
Зависит от внедренного изображения
2(2')
Размер пиксела, байт
Описание
Поддерживается только в Windows 98/2000; используется для внедрения изображений в формате JPEG или PNG 1/8
Монохромное изображение Продолжение
540
Глава 10. Основные сведения о растрах
Таблица 10.1. Продолжение Значение Максимальное количество цветов
4 (2')
Размер пиксела, байт
Описание
1/4
4-цветное изображение, используемое в WinCE
1/2
16-цветное изображение
4
16 (24)
8
8
256 (2 )
1
256-цвстное изображение
16
32 768 (215) или 65 536 (216)
2
High Color
24
3
True Color
м
4
True Color
24 32
166 777 216 (2 ) 166 777 216 (2 )
Если количество бит на пиксел меньше либо равно 8, после заголовка в BMPфайле следует цветовая таблица. Используемая в OS/2 структура BITMAPCOREHEADER заканчивается полем bcBitCount, а в других структурах присутствуют дополнительные поля. Базовый формат DIB следует интерпретировать в приложении таким образом, чтобы отсутствующим полям присваивались значения по умолчанию. В поле biCompression хранится информация об алгоритме сжатия, применяемом к массиву пикселов. Допустимые значения перечислены в табл. 10.2. Таблица 10.2. Алгоритмы сжатия DIB Значение
Описание
BI_RGB
Несжатое изображение
BI_RLE8
Изображение с кодировкой 8 бит/пиксел, сжатое с использованием алгоритма RLE. Только для перевернутых DIB-растров
BI_RLE4
Изображение с кодировкой 4 бит/пиксел, сжатое с использованием алгоритма RLE. Только для перевернутых DIB-растров
BI_BITFIELDS
Несжатые изображения с кодировкой 16 и 32 бит/пиксел. В изображение включаются три битовые маски, определяющие способ хранения компонентов RGB
BI_JPEG
Массив пикселов содержит внедренное изображение в формате JPEG. Поддерживается только в Windows 98/2000
BI_PNG
Массив пикселов содержит внедренное изображение в формате PNG. Поддерживается только в Windows 98/2000
Чаще всего поле bi Compress ion равно BI_RGB (сжатие отсутствует). 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 обеспечивает наилучший результат в том случае, если каждая строка развертки состоит из одинаковых пикселов. В худшем случае результат занимает больше места, чем исходное несжатое изображение. Ниже приведено описание формата сжатого изображения с применением метода BI_RLE8: <изображение> ::= <серия_пикселов> { <серия_пикселов> } <серия_пикселов> ::= <кодированные_данные> | <непосредственные_данные<@062> <кодированные_данные> ::= <конец_строки> | <конец_изображения> <дельта> | <повторение> <конец_строки> ::= О О <конец_изображения> ::= О 1 <дельта> ::= 0 2 dx dy <непосредственные_данные> ::= 0 счетчик { байт } <повторение> ::= счетчик_повторений повторяемый_байт Сжатое изображение в формате RLE кодируется в виде последовательности серий пикселов, существующих в пяти формах. Если первый байт серии пикселов отличен от нуля, он является счетчиком повторений (от 1 до 255) и указывает, сколько раз повторяется следующий байт. Если первый байт равен нулю, следующий байт либо должен быть равен 0 (конец строки), 1 (конец изображения), 2 (дельта) или 3-255 (несжатые пикселы). Дельта-серия смещает текущую позицию в декодированном изображении с заданными смещениями по осям х и у, что позволяет быстро преодолеть несколько строк развертки. Признаки конца строки и конца изображения также позволяют преждевременно обрывать строки развертки или изображения, однако значения пропущенных пикселов считаются неопределенными. Таким образом, на практике дельта-серии Практически не встречаются; признаки конца строки и конца изображения обычно размещаются за последним пикселом строки или всего изображения. Другими словами, каждая строка развертки обычно кодируется отдельно от остальных без пропуска пикселов, что позволяет избежать неопределенных значений пикселов. Непосредственные серии описывают последовательности неповторяющихся пикселов в изображении. Серия начинается с 0 и счетчика, принимающего значения из интервала 3-255, поскольку значения 0-2 зарезервированы для Признаков конца строки, конца изображения и дельта-серий. За счетчиком слеДует точное количество байт. Каждая непосредственная серия должна состоять Из четного количества байт, поэтому счетчик должен быть четным. Это гарантирует, что каждая серия пикселов в закодированном изображении всегда вырав-
542
Глава 10. Основные сведения о растрах
нивается по границе слова, что заметно упрощает процессы кодирования/восстановления информации и повышает их эффективность. Если в изображении в кодировке RLE пикселы не пропускаются, его синтаксис представляется в следующем упрощенном виде: <изображение> ::= <строка_развертки> { <строка_развертки> } [ <конец_изображения<@062> ] <строка_развертки> ::= <серия_пикселов> { <серия_пикселов> } [ <конец_строки> ] <серия_пикселов> ::= <счетчик_повторений> | <непосредственная_серия> Четырехбитные изображения в кодировке BI_RLE4 имеют такую же базовую структуру, как и изображения в формате BI_RLE8. Однако в этом случае каждый пиксел представляется только 4 битами. Счетчик непосредственных данных содержит количество пикселов, поэтому следующие за ним данные должны состоять из значений (счетчик+1)/2 байт. Счетчик повторений тоже содержит количество пикселов; следующий за ним байт содержит два пиксела, которые используются попеременно для заполнения заданного количества пикселов. Для 16- и 32-разрядных DIB-растров поле biCompression должно быть равно BI_BITFIELDS. Это значение не соответствует реально существующему методу сжатия; оно всего лишь позволяет задать размер и порядок следования красной, зеленой и синей составляющих в пикселе. Если поле biCompression содержит BI_FIELDS, после заголовка блока описания растра и перед цветовой таблицей добавляются три двойных слова — маски для извлечения красной, зеленой и синей составляющих из 16- или 32-разрядных упакованных пикселов. На первый взгляд кажется, что маски обладают чрезвычайной гибкостью и позволяют создавать всевозможные экзотические форматы DIB, но в действительности они должны подчиняться целому ряду ограничений. В системах семейства NT маски должны состоять из смежных пикселов и не должны перекрываться. Впрочем, вы все же можете создать DIB с нестандартным порядком каналов RGB. В системах, не входящих в семейство NT, поддерживается всего три разновидности масок. Для 16-разрядных изображений поддерживаются только форматы RGB 5-5-5 и 5-6-5. В формате 5-5-5 синяя маска равна Ox IF, зеленая маска равна ОхЗЕО, а красная маска равна ОхУСОО, и каждый канал занимает 5 бит. В формате 5-6-5 синяя маска равна OxlF, зеленая маска равна Ох7ЕО, а красная маска равна OxFSOO; из этих трех каналов только зеленый канал занимает 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
При виде типов BI_JPEG и 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. Тип BI_JPEG рассматривается в главе 17. Давайте вернемся к структурам заголовка блока описания растра. За признаком сжатия следует поле biSizelmage, в котором хранится размер массива пикселов изображения. При использовании значения BI_RGB поле biSizelmage может быть равно 0; GDI вычисляет размер изображения по ширине, высоте и количеству бит на пиксел. Но при сжатии RLE, JPEG или PNG это поле должно содержать фактический размер данных изображения. Два последних поля структуры BITMAPINFOHEADER содержат информацию о цветовой таблице. В поле biClrUsed хранится количество элементов в цветовой таблице. Для 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-растре поле biCompression равно BI_BITFIELDS, за заголовком блока описания растра следуют битовые маски, хранящиеся в виде массива 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 tagRGBQUAO { BYTE rgbtBlue; BYTE rgbtGreen: BYTE rgbtRed; BYTE rgbtReserved: } RGBQUAD: GDI определяет две дополнительные структуры BITMAPCOREINFO и BITMAPINFO, в которых заголовок блока описания растра объединяется с цветовой таблицей. typedef struct tagCOREINFO { BITMAPCOREHEADER bmciHeader: RGBTRIPLE bmciColors[l]; } BITMAPCOREINFO: typedef struct tagBITMAPINFO { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[l]: } BITMAPINFO: При использовании этих структур необходима осторожность, поскольку в обеих структурах резервируется место лишь для одного элемента цветовой таблицы.
Аппаратно-независимые растры
545
Следовательно, для хранения заголовка описания растра и цветовой таблицы приложение должно выделить дополнительную память за пределами этих структур. Поле bmiHeader структуры BITMAPINFO может относиться к типу BITMAPINFOHEADER, BITMAPV4HEADER или BITMAPV5HEADER, поэтому смещение поля bmi Colors не является фиксированной величиной. Даже если вы ограничиваетесь структурой BITMAPINFOHEADER, место для битовых масок в 16- и 32-разрядных DIB-растрах не резервируется. При поиске цветовой таблицы растра приложение не должно полагаться на содержимое структуры BITMAPINFO. Вместо этого смещение цветовой таблицы следует вычислять во время работы программы на основании данных из заголовка блока описания растра.
Массив пикселов Пикселы изображения хранятся в массиве пикселов. Обычно изображение представляет собой последовательность строк развертки, дополненных до ближайшей 32-разрядной границы. По умолчанию строки развертки хранятся в обратном порядке, если только поле bi Height заголовка описания не является отрицательной величиной. Обратный порядок следования строк развертки означает, что первый пиксел массива в действительности соответствует первому пикселу последней строки развертки при выводе на экран в режиме ММ_ТЕХТ. Внутри строк развертки пикселы упаковываются для экономии места. Строки дополняются битами до границы двойного слова. Количество байт на строку развертки, одна из важных характеристик DIB, вычисляется следующей функцией: int inline ScanlineSize(int width, int bitcount) { return (width * bitcount + 3D/32: }
Для DIB с режимом сжатия BI_RGB обращение к отдельным пикселам массива является простой операцией, которая реализуется достаточно эффективно. Прямой доступ к пикселам играет важную роль при реализации графических алгоритмов и при усовершенствовании средств вывода растров, поддерживаемых в GDI. Кроме того, эта методика чрезвычайно важна в программировании DirectDraw, где графическая поверхность фактически представляет собой DIB. Подробности будут рассмотрены ниже.
Упакованный аппаратно-независимый растр Файловый формат BMP предназначен для хранения DIB-растров в виде файлов на диске. Как упоминалось выше, BMP-файл состоит из заголовка файла, блока описания растра и массива пикселов. Заголовок файла содержит информацию, используемую при загрузке DIB в память. Но после того, как файл окажется в памяти, необходимость в заголовке отпадает. Если требуется, заголовок файла можно восстановить по заголовку блока описания растра. DIB без заголовка файла называется упакованным (packed) DIB-растром. Термин «упакованный» в данном случае не имеет никакого отношения к упаковке пикселов в строке развертки. Он лишь указывает на то, что остальные компоненты DIB следуют друг за другом в смежных блоках памяти.
546
Глава 10. Основные сведения о растрах
Упакованный DIB-растр начинается с заголовка блока описания растра, за которым следуют массив масок, цветовая таблица и массив пикселов. В качестве указателя на упакованный DIB-растр в Win32 API обычно используется указатель на структуру BITMAP INFO. Хотя эта структура не содержит ссылок на массивы масок и пикселов, из нее по крайней мере можно узнать о наличии цветовой таблицы. . Достаточно большое количество функций API получает и возвращает упакованные DIB-растры. Если DIB входит в исполняемый файл в виде ресурса, для получения указателя на упакованный DIB-растр можно воспользоваться функциями FindResource, LoadResource и LockResource. Функция CreateDIBPatternBrushPt использует упакованный DIB-растр для создания узорной кисти. Кроме того, упакованные DIB-растры используются и в работе буфера обмена Windows. В упакованном DIB-растре, хранящемся в памяти, цветовая таблица может содержать индексы логической палитры. Например, если параметр i Usage функции CreateDIBPatternBrushPt равен DIB_PAL_COLORS, цветовая таблица является массивом индексов. Однако такой DIB-растр не следует передавать другим приложениям или записывать на диск, если только другая сторона не осведомлена об этом факте. В файловом формате DIB не существует флага, который бы сообщал, что цветовая таблица содержит индексы неизвестной палитры.
Разделенный аппаратно-независимый растр Содержимое упакованного DIB-растра можно разделить на две части: массив пикселов и информация о формате растра (заголовок блока описания растра, маски и цветовая таблица). Хранить эти две части вместе неудобно. Например, графический редактор для поддержки многоуровневой отмены операций может хранить несколько промежуточных DIB-растров; все они содержат абсолютно одинаковую форматную информацию и отличаются только массивом пикселов. Встречаются и другие ситуации — например, приложение может получать изображение в другом формате (PCX, TIFF или GIF), создавать структуру BITMAPINFO в памяти, строить массив пикселов по восстановленным данными и затем пытаться передавать эти два блока в виде DIB. Многие графические функции GDI обладают достаточной гибкостью и не требуют обязательной передачи упакованного DIB-растра. Вместо этого функции передаются два параметра — указатель на структуру BITMAPINFO и указатель на массив пикселов. Подобное решение обеспечивает необходимую гибкость при построении DIB в памяти. Иногда разделенный аппаратно-независимый растр (неупакованный DIBрастр) для экономии места содержит неполную цветовую таблицу. Например, при использовании DIB с 64 оттенками серого цвета можно выделить память для 64 элементов цветовой таблицы вместо 256.
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 DIB_1BPP. DIB_2BPP. DIB_4BPP. DIBJBPPRLE, DIB_8BPP. DIB_8BPPRLE. OIBJ6RGB555.
Класс для работы с DIB Хотя BMP считается одним из самых простых графических форматов, приведенное в предыдущем разделе описание не выглядит простым. Сложность
547
Класс для работы с DIB
DIBJ6RGB565. DIB_24RGB888. DIB 32RGB888.
// // // // // // // // // //
2-цветное изображение с палитрой 4-цветное изображение с палитрой 16-цветное изображение с палитрой 16-цветное изображение с палитрой. сжатие RLE 256-цветное изображение с палитрой 2-цветное изображение с палитрой. сжатие RLE 15-разрядное цветное изображение RGB. 1 бит не используется 16-разрядное цветное изображение RGB. 24-разрядное цветное изображение RGB. 32-разрядное цветное изображение RGB. 8 бит не используются
5-5-5.
5-6-5 8-8-8 8-8-8. Продолжение
548
Глава 10. Основные сведения о растрах
Листинг 10.1. Продолжение DIB_32RGBA8888, // 32-разрядное цветное изображение RGBA. DIB_16RGBbitfields. // // DIB 32RGBbitfields. // // OIB_JPEG. DIB_PNG } OIBFormat:
16-разрядное цветное нестандартные маски, 32-разрядное цветное нестандартные маски,
изображение RGB. только в NT изображение RGB. только в NT
// внедренное изображение в формате JPEG // внедренное изображение в формате PNG
typedef enum DIBJMIJEEDFREE DIB_BMI_READONLY DIB_BITS_NEEDFREE DIB BITS READONLY
= = =
Класс для работы с DIB
549
BOOL Creatednt width, int height, int bitcount): bool boo! bool void
AttachDIB(BITMAPINFO * pDIB. BYTE * pBits. int flags): LoadFile(const TCHAR * pFileName); LoadBitmap(HMODULE hModlue. LPCTSTR pBitmapName): ReleaseDIB(void): // освобождение памяти
int GetWidth(void) const { return mjiWidth; } int GetHeight(void) const { return mjiHeight: } int GetDepth(void) const { return mjiColorDepth: } BITMAPINFO * GetBMKvoid) const { return m_pBMI: } BYTE * GetBits(void) const { return m_pBits: } int GetBPS(void) const { return m_nBPS: } bool IsCompressed(void) const { return (mjiImageFormat == DIB_4BPPRLE) || (m_nImageFormat == DIB_8BPPRLE) || (mjiImageFormat == DIB_JPEG) || (m nlmageFormat == DIB PNG):
1. 2. 4. 8
Class KDIB public: DIBFormat m_nImageFormat; int m_Flags: BITMAPINFO * m_pBMI: BYTE RGBTRIPLE RGBQUAD
int int DWORD
m_pBits: * ra pRGBTRIPLE; * m_pRGBQUAD: m_nCl rllsed ; mjiClrlmpt: * m_pBHFields;
int int
m nWidth; mjiHeight;
int int int int
m nPlanes: mjiBitCount: m_nColorDepth mjnlmageSize:
int
mjiBPS:
BYTE int
m_pOrigin; т nDelta:
KDIB() : virtual -KDIBO:
// формат массива пикселов // DIB_BMI_NEEDFREE // BITMAPINFOHEADER + маска + // цветовая таблица // массив пикселов // // // // // //
цветовая таблица DIB OS/2 в m_pBMI цветовая таблица DIB V3.4.5 в m_pBMI количество цветов в таблице количество используемых цветов маски для 16 и 32 бит/пиксел в m_pBMI
// // // // // // //
ширина изображения в пикселах высота изображения в пикселах (положительная) количество плоскостей бит на плоскость цветовая глубина размер массива пикселов
// // // // //
Заранее вычисляемые значения: размер строки развертки в байтах Сна каждую плоскость) указатель на логическое начало растра смещение следующей строки развертки
// конструктор по умолчанию, создает // пустое изображение // виртуальный деструктор
Первые четыре переменные класса KDIB содержат важнейшие сведения о растрах, по которым можно получить значения всех остальных переменных. DIB-растры существуют в разных растровых форматах, зависящих от количества бит/пиксел, признака сжатия и даже битовых масок. Наличие одного значения, однозначно определяющего растровый формат, заметно упростит реализацию графических алгоритмов. По этой причине мы и определяем перечисляемый тип DIBFormat. Из определения ясно видно, что формат BMP поддерживает 15 разновидностей растровых форматов. В Windows NT/2000 DDK используется аналогичный подход к определению растровых форматов. Например, функции EngOeateBitmap (создание поверхности, находящейся под управлением GDI) передаются такие константы, как BMF_4RLE, BMF_8BPP и BMF_32BPP. Однако помимо структуры BITMAPINFOHEADER, класс-KDIB содержит множество других полей. Экономия десятка-другого байтов в данном случае несущественна; быстродействие гораздо важнее. Экземпляры класса KDIB будут находиться в памяти компьютера, поэтому они представляют DIB-растр, хранящийся в памяти, а не на диске; следовательно, структура BITMAPFILEHEADER не понадобится. Переменная m_pBMI содержит указатель на блок описания растра. В переменной m_pBits хранится указатель на массив пикселов растра. Раздельное хранение указателей на блок описания растра и массив пикселов позволяет классу KDIB поддерживать как упакованные, так и неупакованные растры. Растры поступают из разных источников — это загрузка из файла или ресурса, вставка из буфера и даже построение на программном уровне. Класс растра должен знать, доступны ли эти два указателя только для чтения, или же данные, на которые они ссылаются, должны удаляться из памяти при удалении экземпляра класса KDIB. Для этого в класс была включена переменная m_F1ags, значение которой представляет собой комбинацию четырех флагов: О DIB_BMI_NEEDFREE — указатель га_рВМ1 ссылается на блок памяти, выделенный из кучи, который должен освобождаться в деструкторе;
550
Глава 10. Основные сведения о растрах
О DIB_BMI_READONLY — указатель m_pBMI ссылается на данные, доступные только для чтения; О DIB_BITS_NEEDFREE — указатель m_pBits ссылается на блок памяти, выделенный из кучи, который должен освобождаться в деструкторе; О DIB_BITS_READONLY — указатель m_pBits ссылается на данные, доступные только для чтения. Во второй группе из пяти переменных хранятся указатели на цветовые таблицы в формате RGBTRIPLE или RGBQUAD, общее количество цветов и количество значащих цветов. Мы не поддерживаем цветовую таблицу с индексами палитры, которые не являются частью формата DIB. Только один из указателей m_pRGBTRIPLE и m_pRGBQUAD может быть отличен от NULL. Переменная m_pBitsFields указывает на битовые маски (если они используются). Первая часть класса KDIB содержит прямые указатели на заголовок блока описания растра, цветовую таблицу и битовые маски. Третья группа переменных класса подробно описывает формат изображения. Здесь хранится ширина и высота изображения (всегда положительная), количество плоскостей, количество бит/пиксел, цветовая глубина и размер изображения. Обратите внимание: в заголовке высота растра может быть отрицательной величиной, если растр хранится в памяти не в перевернутом виде. Из-за отрицательной высоты возникают проблемы во многих графических алгоритмах, поэтому в классе KDIB высота нормализуется, а ориентация растра в памяти отражается в одной из переменных следующей группы. Четвертая группа переменных содержит часто используемые значения, вычисленные на основании упоминавшихся выше переменных. Переменная njiBPS содержит количество байт в строке развертки, дополненной до ближайшей границы DWORD. Переменная m_pOrigin указывает на логическое начало растра, то есть на пиксел (0,0), соответствующий первому байту массива пикселов для прямого (не перевернутого) растра. Переменная m_nDelta содержит смещение между строками развертки. Для растров с прямым порядком строк развертки переменная m_nDel ta всегда положительна, в противном случае она отрицательна. По этим трем переменным всегда можно быстро вычислить адрес строки развертки, по которому легко найти адрес отдельного пиксела. Независимая переменная m_nDelta также может использоваться для хранения шага (pitch) поверхности DirectDraw, который может и не совпадать с mjiBPS. Класс KDIB содержит простой конструктор по умолчанию и виртуальный деструктор. Метод ReleaseDIB отвечает за освобождение ресурсов, выделенных для представления текущего изображения. Кроме того, мы определяем несколько функций, возвращающих геометрические характеристики изображения в виде констант. Метод AttachDIB выполняет основную работу по инициализации класса KDIB на основании данных упакованного или неупакованного DIB-растра. Метод LoadBitmap загружает ресурс BMP из модуля Win32 и инициализирует объект растра, доступного только для чтения, вызовом функции AttachDIB. Метод LoadFile загружает BMP-файл с диска и инициализирует экземпляр KDIB вызовом AttachDIB. Эти три метода приведены в листинге 10.2.
551
Класс для работы с DIB
Листинг 10.2. Инициализация класса KDIB по BMP-файлу или ресурсу
bool KDIB::AttachDIB(BITMAPINFO * pDIB BYTE * pBits int flags) { if ( IsBadReadPtr(pDIB. sizeof(BITMAPCOREHEADER)) ) return false; ReleaseDIBO: m_pBMI m_Flags
= pDIB; = flags;
DWORD size = * (DWORD *) pDIB: // Размер size всегда равен DWORD int compression: // Сбор информации из структур заголовка блока описания растра switch ( size ) { case sizeof(BITMAPCOREHEADER): { BITMAPCOREHEADER * pHeader = (BITMAPCOREHEADER *) pDIB: mjiWidth = pHeader->bcWidth; mjiHeight = pHeader->bcHeight; mjiPlanes = pHeader->bcPlanes: mjiBitCount - pHeader->bcBitCount: mjiImageSize- 0; compression - BI_RGB: if ( mjiBitCount <= 8 ) {
mjiClrUsed = 1 « mjiBitCount: mjiClrlmpt = mjiClrUsed: m_pRGBTRIPLE - (RGBTRIPLE *) ((BYTE *) m_pBMI + size):
m_pBits } else m_pBits break;
= (BYTE *) & m_pRGBTRIPLE[m_nClFUsed]: = (BYTE *) m_pBMI + size;
case sizeof(BITMAPINFOHEADER): case sizeof(BITMAPV4HEADER): case sizeof(BITMAPVSHEADER): (
BITMAPINFOHEADER * pHeader - & tn_pBMI->bmiHeader; pHeader->biWidth: m_nWidth pHeader->biHeight; mjiHeight pHeader->biPlanes: mjiPlanes mjiBitCount • pHeader->biBitCount: m_nImageSize= •! pHeader->biSize!mage; compression • pHeader->biCompression;
Продолжение
552
Глава 10. Основные сведения о растрах
Класс для работы с DIB
553
Листинг 10.2. Продолжение m_nClrUsed - pHeader->biClrllsed; mjiClrlmpt = pHeader->biClrIiiiportant: if ( m_nBitCount<=8 ) if ( mjiClrUsed==0 ) // 0 - полная цветовая таблица mjiClrllsed = 1 « mjiBitCount;
if ( m nClrUsed )
// Имеется цветовая таблица
if ( m_nClrImpt=-0 ) // 0 - все цвета являются значимыми m_nClrImpt = mjiClrllsed: 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): mjpBits = (BYTE *) & m_pRGBQUAD[m_nClrUsed]: else if ( compression==BI_BITFI ELDS ) { m_pBitFields - (DWORD *) ((BYTE *)pDIB+size); m_pBits = (BYTE *) m_pBMI + size + 3 * sizeof(DWORD): else m_pBits
(BYTE *) m_pBMI + size;
break:
if ( pBits ) m_pBits = pBits: // Вычисление основных параметров DIB mjiColorDepth = m_nPlanes * mjiBitCount: mjiBPS = (m_nWidth * mjiBitCount + 31) / 32 * 4: // Прямой порядок строк развертки
m_nHeight = - mjiHeight; // Перейти к положительной величине mjiDelta = m_nBPS: // Смещение вперед m_pOrigin - m_pBits: // scanO .. scanN-1 else
if ( m_nImageSize-=0 ) mjiImageSize = m_nBPS * m_nPlanes * mjiHeight: // Определить формат изображения по режиму сжатия switch ( m nBitCount ) case if ( compression==BI_JPEG ) mjnlmageFormat = 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 = DIBjBBPPRLE: else mjiImageFormat = DIBJBPP: break;
default: return false:
if (mjiHeight < 0 )
mjiDelta = - mjiBPS: // Смещение назад m_pOrigin = m_pBits + (m_nHeight-l) * mjiBPS * m_nPlanes: // scanN-1..scanO
case 16: if ( compression==BI_BITFIELDS ) mjiImageFormat = DIB_16RGBbitfields; else mjiImageFormat - DIB_16RGB555; // См. ниже break; case 24: mjiImageFormat - DIB_24RGB888: break: case 32: if ( compression == BIJ3ITFIELDS )
Продолжение
554
Глава 10. Основные сведения о растрах
Класс для работы с DIB
555
if ( handle == INVALIDJANDLEJALUE ) return false;
Листинг 10.2. Продолжение mjiImageFormat - DIB_32RGBbitfields: else mjiImageFormat = DIB_32RGB888: // См. ниже break:
BITMAPFILEHEADER bmFH; DWORD dwRead = 0: ReadFile(handle. & bmFH. sizeof(bmFH). & dwRead. NULL);
default:
return false:
if ( (bmFH.bfType =•= Ox4D42) && (bmFH.bfSize<=GetFileSize(handle NULL)) ) { BITMAPINFO * pDIB = (BITMAPINFO *) new BYTE[bmFH.bfSize]:
// Разобраться с битовыми полями if ( compression==BI_BITFIELDS ) { DWORD red - m_pBitFields[0]:
if ( pDIB ) { ReadFile(handle. pDIB. bmFH.bfSize. & dwRead. NULL); CloseHandle(handle);
DWORD green = m_pBitFlelds[l];
DWORD blue = m_pBitFields[2]: if ( (blue==Ox001F) && (green==Ox03EO) && (red«Ox7COO) ) m_nImageFormat = DIB_16RGB555: else if ( (blue==Ox001F) && (green==Ox07EO) && (red==OxF800) ) mjiImageFormat = DIB_16RGB565; else if ( (b1ue==OxOOFF) && (green==OxFFOO) && (red==OxFFOOOO) ) mjiImageFormat = DIBJ2RGB888:
return AttachDIBCpDIB, NULL. DIB_BMI_NEEDFREE); CloseHandle(handle): return false;
}
return true: bool KDIB::LoadBitmap(HMODULE hModule. LPCTSTR pBitmapName) { HRSRC hRes - FindResource(hModule, pBitmapName. RT_BITMAP): 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 AttachDIBtpDIB. NULL. DIB_BMI_READONLY
DIB_BITS_READONLY);
bool KDIB::LoadFileCconst TCHAR * pFileName) if ( pFileName—NULL ) return false: HANDLE handle - CreateFileCpFileName, GENERIC_READ. FILE_SHARE_READ. NULL. OPENJXISTING. FILE_ATTRIBUTE_NORMAL. NULL):
Метод 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 вызывается с флагом DIBjSMIjMEDFREE, поэтому выделенный блок будет освобожден в деструкторе. Функция AttachDIB использует всю информацию, упоминавшуюся при описании формата DIB в предыдущем разделе, для инициализации десятка переменных класса. Сначала мы выбираем нужную версию заголовка блока описания растра из четырех возможных, а затем декодируем формат изображения.
556
Глава 10. Основные сведения о растрах
В конце кода функции мы проверяем, соответствует ли изображение, находящееся в режиме сжатия BI_BITFIELD, трем группам битовых масок, поддерживаемых в Windows 95/98. Функция работает как с упакованными, так и неупакованными DIB-растрами. Для неупакованных DIB-растров функции AttachDIB передаются два указателя. Несмотря на всю простоту, этот класс позволит нам загрузить любой BMPфайл и начать эксперименты с растровыми изображениями.
)тображение DIB в контексте устройства В Win32 GDI предусмотрено две функции для вывода аппаратно-независимого растра в контексте устройства: int StretchDIBits(HDC hdc, int xDest. int yDest, int nDestWidth. int nDestHeight, in XSrc, int YSrc. int nSrcWidth, int nSrcHeight. CONST VOID * IpBits. 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-растра. Указатель IpBits ссылается на массив пикселов DIB, а указатель IpBitsInfo — на одну из версий структуры BITMAPINFO. В совокупности они определяют DIB-растр, упакованный или неупакованный. Параметр iUsage обычно равен DIB_RGB_COLORS (для цветовой таблицы RGB) или DIB_PAL_COLORS (для цветовой таблицы, содержащей индексы логической палитры). Последний параметр содержит признак растровой операции, который заслуживает особого разговора. Пока мы будем использовать простейшую растровую операцию SRCCOPY. Функция StretchDIBits выделяет участок изображения DIB, выполняет отсечение, масштабирует выделенный участок по размерам приемного прямоугольника, преобразует к цветовому формату приемной поверхности и записывает полученные данные в приемную поверхность (при использовании растровой операции SRCCOPY). С концептуальной точки зрения процесс состоит из шести этапов: выбор источника, преобразование, отсечение, масштабирование, преобразование цветового формата и растровая операция.
Исходный прямоугольник Первый этап (выбор источника) достаточно прост — источник определяется соответствующей четверкой параметров. Если вы хотите отобразить весь растр,
557
Отображение DIB в контексте устройства
передайте значения [О, О, ширина изображения, высота изображения]. Помните, что для растров с прямым порядком строк развертки поле biHeight структуры BITMAPINFOHEADER отрицательно; используйте абсолютную величину (модуль). В GDI исходный прямоугольник определяется четверкой [XSrc,YSrc,XSrc+nSrcWidth, YSrc+nSrcHeight] с исключением правой и нижней сторон. Если приложение передает при вызове StretchDIBits значения [ширина изображения, высота изображения, -ширина изображения, -высота изображения], используется прямоугольник [ширина изображения, высота изображения, 0, 0]. Чем же этот прямоугольник отличается от прямоугольника [О, О, ширина изображения, высота изображения]? GDI интерпретирует его как отображение системы координат и выводит весь растр, но с зеркальным отражением по вертикали и горизонтали. Используя параметры исходного прямоугольника, можно выделить фрагмент изображения. Если какая-либо часть заданного прямоугольника выходит за границы растра, она считается отсеченной. Некоторые комбинации параметров исходного прямоугольника приведены в табл. 10.3. Таблица 10.3. Параметры XSrc, YSrc, nSrcWidth и nSrcHeight функции StretchDIBits Значения
Исходный прямоугольник
О, 0, ширина, высота
Все изображение, исходная ориентация
ширина, 0, -ширина, высота
Все изображение, зеркальное отражение по горизонтали
О, высота, ширина, -высота
Все изображение, зеркальное отражение по вертикали
ширина, высота, -ширина, -высота
Все изображение, зеркальное отражение по горизонтали и вертикали
О, 0, ширина, 1
Первая строка развертки
О, О, 1, высота
Первый столбец пикселов
Обратите внимание: параметры исходного изображения интерпретируются как логические координаты, а не физические. Вертикальная координата 0 означает первую логическую строку развертки изображения, а не первую физическую строку. В растрах с прямым порядком строк развертки первая логическая строка развертки соответствует первой физической строке, но при обратном порядке строк первая логическая строка соответствует последней физической строке развертки в массиве пикселов.
Приемный прямоугольник и режимы масштабирования Приемник можно определить аналогичным образом — отображением [XDst, YDst, Xdst+nDestWidth, YDst+nDestHeight] из логических координат в физические с исключением правой и нижней сторон. Если прямоугольник не нормализован,
558
Глава 10. Основные сведения о растрах
выполняется зеркальное отражение. Таким образом, окончательная ориентация изображения зависит как от исходного, так и от приемного прямоугольников. Выбранный фрагмент исходного растра масштабируется по размерам приемного прямоугольника. Возможны три принципиально различающихся случая: сохранение исходного масштаба, увеличение или уменьшение. При сохранении масштаба все просто — один пиксел исходного изображения соответствует ровно одному пикселу приемной поверхности, не больше и не меньше. При увеличении один исходный пиксел может соответствовать нескольким приемным пикселам, причем масштабный коэффициент может быть дробным. При уменьшении несколько исходных пикселов преобразуются в один пиксел приемника (масштабный коэффициент тоже может быть дробным). Существует много способов масштабирования, причем ни одному из них нельзя отдать однозначного предпочтения. Способ масштабирования определяется одним из атрибутов контекста устройства — режимом масштабирования (stretch mode). Управление режимом масштабирования в GDI осуществляется двумя функциями: int SetStretchBltMode(HDC hDC. int iStretchMode):
int GetStretchBltModeCHDC hDC);
Функция SetStretchBltMode присваивает значение атрибуту режима масштабирования в контексте устройства, а функция GetStretchBltMode читает его. Допустимые значения перечислены в табл. 10.4. Таблица 10.4. Режимы масштабирования
Режим масштабирования (прежнее название)
Описание
STRETCH_ANDSCANS (BLACKONWHITE)
Пикселы комбинируются поразрядной логической операцией И. В режиме RGB сохраняются черные пикселы
STRETCH_ORSCANS (WHITEONBLACK)
Пикселы комбинируются поразрядной логической операцией ИЛИ. В режиме RGB сохраняются черные пикселы
STRETCH.DELETESCAN (COLORONCOLOR)
Сохраняется один пиксел, остальные удаляются
STRETCHJALFTONE (HALFTONE)
Вычислить средний цвет по нескольким пикселам. Поддерживается только в системах семейства NT. После установки этого режима следует выровнять базовую точку кисти функцией SetBrushOrgEx
Отображение DIB в контексте устройства
559
логическая операция И отдает предпочтение черному цвету (0) перед белым (1). Если вы хотите сохранить белые линии на черном фоне, используй- те опера! цию STRETCH_ORSCANS, поскольку она отдает предпочтение белому цвету. В цветных изображениях эти два режима приводят к искажению цветов, поэтому в ; этом случае используют следующие два режима. При выборе режима STRETCH_ DELETESCAN лишние данные попросту игнорируются. Этот способ работает быстро, но не всегда дает хорошие результаты, поскольку при уменьшении изображения лишние пикселы просто отбрасываются, что приводит к потере информации. В режиме STRETCH_HALFTONE вычисляется средний цвет группы пикселов, что улучшает восприятие уменьшенных объектов человеческим глазом. С другой стороны, этот режим работает гораздо медленнее. Другая проблема заключается в том, что режим STRETCH_HALFTONE реализован только в системах семейства NT.
Преобразование цветового формата Цветное изображение может выводиться на черно-белом принтере, а черно-белое изображение может отображаться на цветном экране. В таких случаях GDI приходится преобразовывать пикселы из формата исходного изображения в формат приемного контекста устройства. Если форматы источника и приемника совпадают, преобразование касается только формата хранения данных. Изображения DIB всегда являются цветными. Даже двуцветный растр должен содержать цветовую таблицу с двумя элементами. Цветовая таблица преобразует индексы пикселов в цветовые значения. Если палитра не используется, пикселы могут содержать непосредственные значения RGB. При выводе на устройство с поддержкой палитры в отображении значений RGB на индексы палитры участвуют две палитры: логическая и системная. Операции с палитрой рассматриваются в главе 13. Чтобы вывести DIB с палитрой на RGB-устройстве, индексы, хранящиеся в растре, приходится преобразовывать в значения RGB по цветовой таблице растра. Задача вывода цветного изображения на монохромном устройстве не имеет однозначного решения. В режиме STRETCH_HALFTONE функция StretchDIBits осуществляет полутоновое преобразование цветных изображений в черно-белый формат; в других режимах StretchDIBits подбирает ближайшие цвета. Для сравнения стоит заметить, что это поведение отличается от отображения аппаратнозависимого растра в черно-белом контексте устройства.
Растровая операция Режим масштабирования учитывается только при уменьшении, то есть при выводе большого исходного изображения в маленьком приемном прямоугольнике. Увеличение реализуется простым повторением пикселов. Если вы не хотите, чтобы в увеличенном растре возникали «зазубрины» на контурах, реализуйте собственный алгоритм масштабирования. Режимы STRETCH_ANDSCANS и STRETCHJDRSCANS предназначены для черно-белых изображений. Если вы хотите, чтобы тонкие черные линии на белом фоне не исчезали и не прерывались в результате масштабирования, воспользуйтесь режимом STRETCH_ANDSCANS, поскольку
Итак, после преобразования цветового формата мы имеем преобразованный массив пикселов, готовый к записи на приемную поверхность. По аналогии с бинарными растровыми операциями, определяющими способ объединения цвета пера/кисти с цветом приемника, в GDI предусмотрены растровые операции, определяющие окончательное значение приемного пиксела. Растровые операции подробно рассматриваются в следующей главе. А пока мы ограничимся простейшей растровой операцией SRCCOPY, которая сводится к Простому копированию пиксела исходного растра на приемную поверхность.
560
Глава 10. Основные сведения о растрах
Пример использования функции StretchDIBits Ниже приведен небольшой пример,' демонстрирующий применение функции StretchDIBits. Для начала необходимо добавить в класс KDIB функцию для отображения DIB. int DrawDIB(HDC 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 GDI_ERROR: } После добавления этой функции мы можем воспользоваться классом KDIB для загрузки и отображения DIB. Код следующего фрагмента загружает растровый рисунок со львом и отображает его в четырех разных ориентациях. KDIB lion: if ( 11on.LoadFile(_T("lion.bmp")) ) {
int w = DIB.GetWidthO: int h = DIB.GetHeightO: DIB.DrawDIBthDC. DIB.OrawDIBChDC. OIB.DrawDIBChDC. DIB.DrawDIB(hDC.
5. 10+w. 5, 10+w.
5, 5. 10+h. 10+h,
w, w, w. w,
h. h, h. h,
0. 0, 0. 0,
0. w. h. SRCCOPY): 0. -w. h. SRCCOPY); 0, w. -h, SRCCOPY): 0, -w. -h. SRCCOPY):
Рис. 10.2. Зеркальное отражение рисунка с использованием функции StretchDIBits
Этот пример приведен только для демонстрационных целей. Изображения в программе должны загружаться один раз, а не каждый раз, когда их потребуется
Отображение DIB в контексте устройства
561
вывести. Программа Bitmap, описываемая в этой главе, позволяет выбрать изобоажения DIB в диалоговом окне и отобразить их в дочерних окнах MDI. С каждым дочерним окном связан экземпляр класса KDIB, который один раз загружает изображение и многократно отображает его. Одно из этих дочерних окон-показано на рис. 10.2.
SetDIBitsToDevice На фоне других функций GDI API функция SetDIBitsToDevice выглядит одиноко Вероятно компании Microsoft следовало бы исключить эту функцию из Win32 API. Если приложение импортирует ее, то, скорее всего, это делается косвенно через класс CDC MFC. функция выводит DIB (полностью или частично) с сохранением исходной ориентации и масштаба, независимо от текущего мирового преобразования или режима отображения окна на область просмотра. Из всех параметров в логической системе координат задается лишь позиция приемника (xDest,yDest). Параметры XSrc, YSrc, dwWidth и dwHeignt определяют часть DIB. Не пьггаитесь проделывать такие же фокусы со знаком параметров, как при вызове Stretchouts высота и ширина передаются в виде беззнаковых целых чисел. Передавать разм™ риемЕоп, прямоугольника не нужно; в системе координат устройства они всегда соответствуют размерам источника. Но самое интересное в функции SetDIBitsToDevice - это способ передачиDIBрастра (или его части). В параметре Ipbrni, как и прежде, передается^™ель на заголовок блока описания растра. Параметр IpvBits указывает на буфер^одержаший несколько строк развертки или весь растр. Функция спр^фована таким образом, чтобы в IpvBits можно было передавать указатель на часп,,ане^а весь DIB-растр Расположение данных в буфере определяется двумя дополни т LZ параметрами. Параметр uStartScan содержит последовательный номер строки развертки в изображении, на первую строку которого <**™f^^ а папаметр cScanLines содержит количество строк развертки в буфере. Функция ISS^^ie^ всегда «Lpyer исходные пикселы в приемникпоэтомууказывать растровую операцию не нужно. Последний параметр, fuColorUse, иденти фицирует способ интерпретации цветовой таблицы „pvimnn повеохФункция SetDIBitsToDevice копирует cScanLines из буфера на приемнук.поверх ность начиная с (xDest, yDest+uStartScan), с учетом отсечения ° угольника и отсечения, действующего в приемном контексте что координаты приемника должны задаваться в системе В следующем фрагменте показано, как при помощи функции вывести все изображение, начиная с точки приемника (х,у). SetDIBitsToDevice(hDC. х, у. mjiWidth. absCmjiHeight). 0. 0. 0. abs(mjiHeight), m_pBits, (const BITMAPINFO *) m_pDIB. DIB_RGB_COLORS): Единственным преимуществом SetDIBitsToDevice отся снижение затрат памяти. Если приложение Wml6, Pa6oT«e тивном компьютере с 4 Мбайт памяти, должно вывести ^е^ сЬоомате 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_OIB.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
} delete [] buffer:
\ Совместимые контексты устройств
563
Для сжатых изображений этот код не работает, поскольку сжатие существснно затрудняет переход между строками развертки. Каждая строка развертки копируется в буфер, инвертируется и отображается на экране вызовом SetDIBitsToDevice. Строки обрабатываются последовательно; в параметрах изменяется только значение uStartScan.
Совместимые контексты устройств Контексты устройств, рассматривавшиеся до настоящего момента, всегда соответствовали реальному физическому устройству, которое обслуживалось специальным драйвером. Контекст устройства предоставляет в распоряжение программ, использующих GDI, абстрактное графическое устройство. В своей внутренней реализации GDI поддерживает для каждого контекста устройства таблицу функций драйвера графического устройства, вызываемых через интерфейс DDL Ta• Кое описание напоминает виртуальные функции C++ или методы СОМ; действительно, в работе этих механизмов имеется определенное сходство. Абстрактный подход позволяет GDI полностью имитировать графическое устройство в памяти — в том же смысле, в каком виртуальный диск имитирует жесткий диск. Для работы с графическими устройствами, имитируемыми в памяти, применяются совместимые контексты устройств (memory device context). По историческим причинам совместимые контексты устройств не являются полностью независимыми от физических графических устройств. В действительности совместимый контекст устройства всегда связывается с физическим графическим устройством. Выражаясь точнее, в GDI поддерживается всего одна функция для создания контекста устройства, совместимого с существующим контекстом: • HOC CreateCompatibleDC(HOC h D C ) ; Эталонный контекст устройства, передаваемый этой функции, должен поддерживать растровые операции, иначе совместимый контекст не принесет особой пользы. Для создания совместимых контекстов обычно используется экранный контекст, поскольку современные видеоадаптеры обеспечивают полную и правильную реализацию растровых операций. Напротив, контекст устройства принтера вряд ли является хорошим кандидатом для создания совместимого Контекста устройства. Принтеры обладают различным уровнем поддержки цветов и растровых операций. Например, в драйвере принтера PostScript поддержка растровых операций обычно ограничена. GDI даже позволяет передавать манипулятор NULL; в этом случае создается контекст устройства, совместимый с текущим экраном. Работа совместимого контекста устройства основана на использовании растра. Все графические команды реализуются как вывод на растре, а не на физическом устройстве. Между этим растром и совместимым контекстом устройства ? не существует жесткой связи. Базовый растр является атрибутом контекста; его можно выбирать и исключать, как манипулятор объекта пера или кисти. Для Получения этого атрибута можно вызвать функцию GetCurrentObject с типом OBJ_BITMAP. При создании совместимого контекста устройства 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 CreateBitmapCint nWidth. int nHeight. UINT cPlanes. UINT cBitsPerPel. CONST VOID * IpvBits); HBITMAP CreateBitmapIndirecttCONST BITMAP * Ipbm); HBITMAP CreateCompatibleBitmap(HOC 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 * Ipbmi. UINT fuUsage): HBITMAP LoadBitmap(HINSTANCE hlnstance. LPCTSTR IpBitmapName):
Между DDB и DIB существует несколько принципиальных различий. По своей исходной архитектуре DDB зависит от устройства. Это означает, что любое гра-
Аппаратно-зависимые растры
565
i фическое устройство может выбрать для представления DDB свой собственный внутренний растровый формат. Реальный формат DDB может изменяться при работе приложения на разных компьютерах и даже на одном компьютере в разных видеорежимах. Аппаратно-зависимый растр, как и DIB, содержит массив пикселов, но при передаче или чтении данных DDB строки развертки всегда следуют в прямом порядке (сверху вниз), поэтому отдельно обрабатывать отрицательную высоту для перевернутых растров не нужно. В отличие от DIB-растров, всегда использующих строки развертки с одной цветовой плоскостью, DDBрастры могут использовать несколько цветовых плоскостей, чтобы обеспечить совместимость с конкретным графическим устройством для получения оптимального быстродействия. Массивы пикселов, передаваемые функциям создания DDB, должны выравниваться по 16-разрядной границе слов. DDB не содержит цветовой таблицы, поэтому реальный цвет каждого пиксела изображения зависит от устройства, на котором оно выводится.
CreateBitmap DDB определяется шириной, высотой, количеством плоскостей, количеством бит на пиксел и массивом цветов (или индексов) пикселов. Эти пять характеристик передаются при вызове функции CreateBitmap. Функция создает растр nWidth* nHeight, с числом цветовых плоскостей, равным cPlanes, и кодировкой cBitsPerPel бит/пиксел. Параметр IpvBits содержит указатель на исходный массив пикселов; предполагается, что размер этого массива равен (nWidth*cBitsPerPel+15)/16*2* cPlanes*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 бит/пиксел, инициализированный шахматным узором 4 х 4, и неинициализированный 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);
GetObject и DDB
CreateBitmapIndirect Функция CreateBitmapIndirect позволяет создать DDB через указатель на структуру BITMAP, которая определяется следующим образом: typedef struct tagBITMAP { LONG bmType: // Тип растра, должен быть равен О LONG bmWidth; LONG bmHeight: LONG bmWidthBytes; WORD bmPlanes: WORD bmBitsPixel; LPVOID bmBits; } BITMAP;
При сравнении полей структуры со списком параметров CreateBitmap обнаруживаются три основных различия. Хотя в структуре появилось новое поле bmType, оно должно быть равно 0. В поле bmWidthBytes хранится размер строки развертки массива пикселов в байтах. Как и в случае с функцией CreateBitmap, оно должно быть четным числом. Поле bmBits содержит указатель на массив пикселов, но этот указатель не определяется как константный. В документации Microsoft ошибочно утверждается, что размер строки развертки должен быть кратен 32 битам. На самом деле это не обязательно. На практике GDI всегда стремится объединять вызовы разных функций в один системный вызов; функция CreateBitmapIndirect также реализуется вызовом CreateBitmap. Но если значение поля bmWidthBytes выходит за 16-разрядные границы, то GDI перед вызовом CreateBitmap выделяет временный блок памяти из системной кучи и копирует исходный массив пикселов.
567
При вызове 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, результат будет тем же. . »
CreateCompatibleBitmap и CreateDiscardableBitmap
Цветной растр, созданный функцией CreateBitmap или CreateBitmapIndirect, может оказаться несовместимым с контекстом устройства, в котором вы собираетесь его отобразить. Вполне нормальный DDB-растр, несовместимый с контекстом устройства, будет отвергнут при попытке использования его в данном контексте. Чтобы создаваемый растр был заведомо совместим с устройством, воспользуйтесь функцией CreateCompatibleBitmap. Функция CreateCompatibleBitmap выглядит гораздо проще — ей передается толь ко манипулятор эталонного контекста устройства, а также требуемая ширина i высота растра. CreateCompatibleBitmap не нужно знать количество цветовых плос костей и количество бит на пиксел, поскольку эти характеристики вычисляются на основании данных эталонного контекста. Если контекст устройства соответ ствует физическому графическому устройству, GDI задействует его характери стики для создания растра. Например, при использовании экранного контекст, устройства режиме с 256 цветами CreateCompatibleDC создает DDB с кодировке] 8 бит/пиксел, а в 32-разрядном режиме True Color создается DDB с кодировке] 32 бит/пиксел. Следовательно, если приложение запрашивает DDB с размерам]
568
Глава 10. Основные сведения о растрах
1024 x 1024, то необходимый объем памяти будет равен 1 Мбайт для режима с 256 цветами и 4 Мбайт для 32-разрядного режима True Color. Для совместимых контекстов устройств GDI создает растр с таким же форматом пикселов, как и у текущего растра, выбранного в контексте устройства. Как было сказано в предыдущем разделе, при создании совместимого контекста устройства в нем выбирается монохромный растр, состоящий из одного пиксела, поэтому функция CreateCompatibleBitmap для этого контекста устройства создает монохромный растр. Следующая функция отвечает на некоторые часто возникающие вопросы — почему функция CreateCompatibleBitmap отказывается создавать DDB и каков максимальный размер DDB-растра? Функция LargestDDB получает манипулятор контекста устройства и использует алгоритм бинарного поиска для определения размеров наибольшего DDB-растра, совместимого с этим контекстом. HBITMAP LargestDDBtHDC hDC) HBITMAP hBmp; int mins = 1: int maxs = 1024
12
while ( true ) // Бинарный поиск наибольшего DDB int mid = (mins + maxs)/2; hBmp = CreateCompatibleBitmapthDC, mid. mid): if ( hBmp ) HBITMAP h = CreateCompatibleBitmap(hDC. mid+1. mid+1); if ( h==NULL ) return hBmp; DeleteObject(h): Del eteObjectChBmp); 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-растра
Windows NT/2000
Windows 95/98
1
17 408x17 408; 36,125 Мбайт
11 474x11 474; 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 Мбайт
Цветовая глубина DC
2039x2039; 15,86 Мбайт
Во времена Wml6 памяти вечно не хватало, поэтому в GDI была включена функция CreateDiscardableBitmap для создания освобождаемых растров. Идея состояла в том, чтобы интерфейс GDI мог освобождать ресурсы растра в случае их нехватки. Каждый раз, когда приложение хотело воспользоваться освобождаемым растром, оно должно было проверить его и воссоздать заново, если растр стал недействительным. Хотя функция CreateDiscardableBitmap входит и в Win32 GDI, она не создает освобождаемый растр. Вместо этого она просто вызывает CreateCompatibleBitmap. В 32-разрядных операционных системах затраты памяти на хранение аппаратно-зависимых растров по-прежнему остаются серьезной проблемой, особенно в экранных режимах True Color при высоком разрешении. Впрочем, программы Win32 могут использовать DIB-секции и тем самым переместить затраты памяти из системных ресурсов GDI на уровень приложения. . '
CreateDI Bitmap Описанные выше функции не позволяют легко создать инициализированный цветной DDB-растр. Хотя при вызове CreateBitmap и CreateBitmapIndirect можно передать указатель на массив пикселов, сложность цветного изображения при этом не учитывается. С другой стороны, аппаратно-не'зависимый растр обладает хорошими средствами для описания стандартных цветовых форматов. По этой причине в GDI была предусмотрена функция CreateDI Bitmap, которая создает инициализированный DDB-растр на базе DIB, то есть в каком-то смысле преобразует DIB в DDB. Функция CreateDIBitmap работает в два этапа: сначала она создает DDB, а затем преобразует DIB в DDB. В параметре hdc передается манипулятор эталонного контекста устройства, с которым должен быть совместим созданный DDBрастр. При передаче NULL создается монохромный DIB-растр. Параметр Ipbmih содержит указатель на структуру заголовка блока описания растра, но в ней используются только поля ширины и высоты. Если высота отрицательна, исполь-
570
Глава 10. Основные сведения о растрах
зуется абсолютное значение. Другие поля (такие, как количество бит на пиксел и режим сжатия) не используются. Инициализация созданного DDB-растра необязательна и зависит от параметра fdwlnit. Если параметр равен CBM_INIT, следующие три параметра полностью описывают неупакованный DIB-растр. Параметр Ipblnit указывает на массив пикселов, параметр Ipbmi — на заголовок блока описания растра, а параметр fuUsage сообщает, содержит ли цветовая таблица индексы палитры или цвета RGB. Следующая функция класса KDIB преобразует DIB в DDB: HBITMAP ConvertToDDB(HDC hDC)
{
return CreateDIBitmapthDC. & m_pBMI->bmiHeader. CBMJNIT, m_pB1ts. m_pBMI, DIB_RGB_COLORS);
}
Если в процессе преобразования DIB в DDB задействована палитра, то используется текущая палитра, выбранная в контексте устройства.
Load Bitmap В Windows-программировании растры обычно присоединяются к модулю в виде ресурса и затем загружаются в виде DDB функцией LoadBitmap. Функция LoadBitmap получает два параметра: hlnstance — манипулятор модуля, содержащего растровый ресурс, и IpBitmapName — имя растрового ресурса. Если параметр hlnstance равен NULL, во втором параметре передаются константы OBM_BTNCORNERS, OBM_CHECK и т. д., определяющие десятки стандартных системных растров. Эти растры либо берутся непосредственно из модуля USER32.dll, либо синтезируются этим модулем. Если для идентификации растра в ресурсном файле используется целочисленный идентификатор, преобразование целого числа в указатель на символьную строку выполняется с помощью макроса MAKEINTRESOURCE. Растровые ресурсы хранятся в модулях Win32 в формате упакованного DIBрастра. Функция LoadBitmap находит растровый ресурс, фиксирует его в памяти для получения манипулятора упакованного DIB-растра и затем создает DDBрастр, совместимый с текущим экранным режимом. Для монохромных растров (то есть DIB-растров, у которых цветовая таблица содержит только черный и белый цвет) GDI использует монохромный формат DDB вместо цветного формата, увеличивающего затраты памяти. При работе в 256-цветном экранном режиме с палитрой загрузка изображений True Color и High Color приводит к ухудшению качества изображения, поскольку приложение не может управлять процессом преобразования цветов. Код следующего фрагмента загружает растровое изображение панели инструментов из библиотеки BROWSEUI.dll: HINSTANCE hMod = LoadLibraryC'browseui.dll"); HBITMAP hBmp - LoadBitmap(hMod. MAKEINTRESOURCE(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 SetDIB1ts(HDC hdc. HBITMAP hbmp. UINT uStartScan. UINT cScanLines. CONST VOID * IpvBits, CONST BITMAPINFO * Ipbmi. UINT fuColorUse): int GetDIBitstHDC hdc. HBITMAP hbmp. UINT uStartScan, UINT cScanLines. CONST VOID * IpvBits. CONST BITMAPINFO * Ipbmi. UINT fuColorUse): Списки параметров этих двух функций совпадают, хотя в документации MSDN имена слегка различаются. Первый параметр определяет эталонный контекст устройства, палитра которого должна использоваться при преобразовании формата пикселов. Второй параметр содержит манипулятор существующего объекта аппаратно-зависимого растра. Пять оставшихся параметров определяют фрагмент неупакованного DIB-растра и интерпретацию его цветовой таблицы. Фрагмент может представлять собой как полный DIB-растр, так и группу смежных строк развертки. Подобное решение в основном предназначено для экономии памяти и для постепенного вывода фрагментов^ растра, загружаемого по медленному соединению. Параметр IpvBits содержит указатель на строки развертки, параметр uStartScan определяет номер первой строки развертки в буфере, а параметр cScanLi nes — количество строк развертки в буфере. Функция SetDIBits преобразует пикселы заданного фрагмента DIB в формат DDB и копирует результат в DDB. Функция GetDIBits преобразует пикселы из формата DDB в формат DIB и копирует результат в буфер фрагмента DIB. Функция SetDIBits в действительности реализуется функцией SetDIBitsToDevice с указанием в качестве приемника совместимого контекста устройства, в котором выбран DDB-растр. В процессе преобразования в совместимом контексте устройства выбирается палитра контекста, определяемого параметром hdc. Существует несколько способов преобразования DIB в DDB. DIB-растр, хранящийся в виде ресурса, можно загрузить в DDB функцией LoadBitmap. К сожалению, при этом вы не управляете процессом преобразования. Функция CreateDIBitmap создает на базе DIB абсолютно новый DDB-растр, совместимый с заданным контекстом устройства. Таким образом, эта функция позволяет в
572
Глава 10. Основные сведения о растрах
Аппаратно-зависимые растры
dibinfo.bmiHeader.biWidth = ddbinfo.bmWidth: dibinfo.bmiHeader.biHeight = ddbinfo.bmHeight; dibinfo.bmiHeader.biPlanes = 1; dibinfo.bmiHeader.biBitCount = nBitCount; dibinfo.bmiHeader.biCompression = nCompression;
определенной степени управлять преобразованием цветов и в то же время с ней легко работать. По сравнению с LoadBitmap и CreateDIBitmap функция SetDIBits обладает более широкими возможностями. Поскольку в DDB копируется не весь DIB-растр, а его фрагмент, вызывающая сторона может использовать буфер меньшего размера и выполнять преобразование постепенно; кроме того, она может объединить несколько маленьких DIB-растров в один большой DDB-растр и управлять форматом DDB. Как считается, самый распространенный способ преобразования DDB в DIB предлагает функция GetDIBits. Процесс преобразования DDB в DIB непрост, поскольку при этом приходится обеспечивать поддержку разных форматов DIB. Функция GetDIBits поддерживает все допустимые для DIB комбинации цветовых кодировок, форматов (RGB/палитра), наличия и отсутствия сжатия RLE в массиве пикселов, а также битовых полей. В параметре Ipbrai передается указатель на информационный заголовок DIB-растра, определяющий его формат. При сжатии RLE приложение не может легко определить размер сжатого изображения. Функцию GetDIBits приходится вызывать дважды. При первом вызове в указателе на массив пикселов передается NULL, на что GDI возвращает необходимый размер буфера. При втором вызове выделенный буфер указанного размера заполняется данными изображения. В листинге 10.3 приведена функция BitmapToDIB, которая является удобной оболочкой для вызова функции GetDIBits. Функция получает манипулятор объекта палитры GDI, используемого для построения цветовой таблицы, манипулятор объекта DDB, количество байт на пиксел и флаг сжатия DIB. Функция вычисляет размер DIB, выделяет буфер нужного размера, записывает в него данные DDB и возвращает указатель на буфер.
HOC
hDC = GetDC(NULL); / / Э к р а н н ы й контекст устройства
HGDIOBJ hpalOld: if ( hPal ) hpalOld = SelectPalette(hDC. hPal. FALSE); else
hpalOld = NULL;
// Запросить у GDI размер изображения GetDIBits(hDC. hBmp. 0. ddbinfo.bmHeight. NULL. (BITMAPINFO *) & dibinfo. DIB_RGB_COLORS); int nlnfoSize = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * GetDIBColorCount(di bi nfо.bmi Header); int nTotalSize = nlnfoSize + GetDIBPixelSize(dibinfo.bmiHeader); BYTE * pDIB = new BYTE[nTotalSize]: if ( pDIB ) { memcpy(pDIB. & dibinfo. nlnfoSize); if ( ddbinfo.bmHeight != GetDIBits(hDC. hBmp. 0. ddbinfo.bmHeight. pDIB + nlnfoSize. (BITMAPINFO *} pDIB. DIB_RGB_COLORS) ) { delete [] pDIB; pDIB = NULL; »
Листинг 10.3. Функция BitmapToDIB: преобразование DDB в DIB BITMAPINFO * BTtmapToDIB(HPALETTE hPal. // Палитра для // преобразования цвета HBITMAP hBmp. // Преобразуемый DDB-растр int nBltCount. int nCompression) // Нужный формат typedef struct { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[256+3]: } DIBINFO:
if ( hpalOld ) SelectObjecUhDC. hpalOld); ReleaseDC(NULL. hDC): return (BITMAPINFO *) pDIB:
BITMAP ddbinfo; DIBINFO dibinfo;
}
// Получение данных DDB if ( GetObject(hBmp, sizeof(BITMAP), & ddbinfo)==0 ) return NULL; // Заполнение структуры BITMAPINFOHEADER // по данным размера и запрашиваемому формату memset(&dibinfo. 0. sizeof(dibinfo)): dibinfо.bmiHeader.biSize
573
sizeof(BITMAPINFOHEADER):
Для упрощения вызова функция BitmapToDIB не требует, чтобы вызывающая сторона передавала структуру BITMAPINFO. Она создает в стеке структуру DIBlNhU с цветовой таблицей из 259 элементов; этого вполне достаточно для DIB-растра, использующего битовые поля и 256 цветов полной палитры. Ширина и высота DIB вычисляются по размерам DDB. После того как первый вызов GetDlims вернет фактический размер изображения, функция выделяет память для буфера, копирует заголовок описания растра, а затем вызывает GetDIBits для загрузки всего DIB-растра.
574
Глава 10. Основные сведения о растрах
Функция GetDIBits обладает и другой неочевидной особенностью. Если присвоить значение только полю biSize и оставить остальные поля равными О, GDI заполнит их данными о количестве бит/пиксел и режиме сжатия, используемом контекстом устройства. Таким образом, приложение может точно определить формат пикселов, используемый контекстом устройства. Этот прием особенно полезен в том случае, если приложение должно напрямую работать с пикселами в видеорежиме с кодировкой 16 бит/пиксел. У 16-разрядной кодировки существует два стандартных подтипа: 5-5-5 и 5-6-5. В некоторых ситуациях приложение должно знать точный формат пикселов; задача решается функцией Pixel Format, приведенной в листинге 10.4. Листинг 10.4. Функция PixelFormat: определение формата пикселов контекста устройства int Pixel Format(HOC hdc) { typedef struct {
BITMAPINFOHEADER bmiHeader. RGBQUAO bmiColors[256+3]: } DIBINFO:
DIBINFO dibinfo: HBITMAP hBmp = CreateCompatibleBitmap(hdc, 1. 1): if ( hBmp—NULL ) return -1; memset(&dibinfo, 0. sizeof(dibinfo)); dibinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER): // Первый вызов для получения значения MBitCount для hdc GetDIBitsthdc. hBmp. 0. 1. NULL. (BITMAPINFO*) & dibinfo. DIB_RGB_COLORS): // Второй вызов для получения цветовой таблицы или битовых полей GetDIBits(hdc. hBmp. 0. 1. NULL. (BITMAPINFO*) & dibinfo. DIB_RGB_COLORS): DeleteObject(hBmp); // Попытаться интерпретировать битовые поля if ( dibinfo.bmiHeader.biBitCount==BI_BITFIELDS ) { DWORD DWORD DWORD DWORD
* pBitFields = (DWORD *) dibinfo.bmiColors: red = pBitFields[0]; green = pBitFields[l]: blue = pBitFields[2]:
if ( (blue==Ox001F) return DIB_16RGB555: else if ( (blue==Ox001F) return OIBJ.6RGB565:
else if ( (blue==OxOOFF)
(green==Ox03EO) && (red==Ox7COO) ) (green==Ox007E) && (red==OxF800) ) (green==OxFFOO) && (red-=OxFFOOOO)
Аппаратно-зависимые растры
575
return DIB_32RGB888: else return -1: switch ( dibinfo.bmiHeader.biBitCount case case case case case case case
1: return DIBJBPP; 2: return DIB_2BPP: 4:return DIB_4BPP; 8: return DIB_8BPP: 24: return DIB_24RGB888: 16: return DIB_16RGB555: 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 GetBitmapBitstHBITMAP 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 в текстовом окне (предполагается, что у нас имеется функция вывода шестнадцатерич-
Глава 10. Основные сведения о растрах
576
ного дампа HexDump). В системах семейства Windows NT почти все видеоадаптеры используют кадровые буферы с одноплоскостным форматом DIB, поэтому у них массив пикселов DDB достаточно близок к массиву DIB. С другой стороны, в стандартных режимах VGA или в системах семейства Windows 95 формат DDB усложняется. void DumpBitmap(HWND hWnd. HBITMAP hBMP) {
int size = GetBitmapBits(hBmp, 0, NULL); BYTE * pBuffer = new BYTE[size]: if ( pBuffer )
{
GetB1tmapBits(hBmp. size. pBuffer): HexDumpthWnd. 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 ВПВШНОС hdcDst. int nXDst. Int nYDst. int nWidth. int nHeight. HOC hdcSrc, int nXSrc. int nYSrc. DWORD dwRop); BOOL BitBltCHDC hdcDst, int nXDst. int nYDst. int.nWDst. int nHDst, HOC hdcSrc. int nXSrc. int nYSrc, nWSrc. int nHSrc. DWORD dwRop):
Функция BitBlt передает прямоугольный блок пикселов с исходного устройства в прямоугольник приемного устройства. Исходный прямоугольник определяется параметрами nXSrc, nYSrc, nWidth и nHeight в логической системе координат исходного контекста устройства. Приемный прямоугольник определяется параметрами nXDst, nYDst, nWidth и nHeight в логической системе координат приемного контекста устройства. Оба контекста устройства должны поддерживать растровые операции RCJJITBLT. Например, если исходный контекст устройства является метафайловым контекстом, попытка вызова BitBlt завершится неудачей, поскольку метафайловый контекст не имеет кадрового буфера, содержимое которого можно прочитать. GDI также не справляется со случаями, когда в исходном контексте устройства действует преобразование поворота или сдвига, способное превратить исходный прямоугольник в параллелограмм или прямоугольник, стороны которого не параллельны осям. Если исходный и приемный прямоугольники имеют разные размеры в системах координат устройства, исходное изображение масштабируется по размерам приемного многоугольника. В приемном контексте устройства могут действовать любые преобразования, хотя в системах семейства Windows 95 мировые преобразования не поддерживаются. Как и в случае с функцией StretchDIBits, изменение знака в параметрах исходного и приемного прямоугольника позволяет выполнить зеркальное отражение растра по вертикальной и/или горизонтальной оси. Последний параметр dwRop определяет тернарную растровую операцию, то есть способ объединения исходного пиксела, приемного пиксела и кисти для формирования нового значения исходного пиксела. Пока мы ограничимся тернарной операцией SRCCOPY, при которой пикселы приемника попросту заменяются пикселами источника. Функция StretchBIt делает практически то же, что и функция BitBlt. Единственное различие заключается в независимом определении размеров исходного и приемного прямоугольников, поэтому функция StretchBIt обычно используется в ситуациях, когда исходный и приемный прямоугольники имеют разные размеры в логических координатах. Понадобится ли реальное масштабирование или нет — зависит от настройки логических систем координат.
Случайная перестановка фрагментов экрана Давайте рассмотрим использование функций BitBlt/StretcnBlt на конкретном примере. Мы напишем программу, которая копирует случайно выбранный прямоугольник экрана в другой случайно выбранный прямоугольник. Чтобы изображение выглядело поярче, мы задействуем случайно выбранную растровую операцию с однородной кистью случайно выбранного цвета. Приемный прямоугольник определяется с отрицательной шириной и высотой, поэтому исходный прямоугольник поворачивается на 180°. В результате масштабирования приемный прямоугольник увеличивается вдвое по сравнению с источником. После нескольких итераций экран начинает выглядеть довольно оригинально. Мы не собираемся писать полноценную экранную заставку (screen saver), поэтому код
578
Глава 10. Основные сведения о растрах
KDDBO
просто выполняется в цикле 200 раз, а затем программа завершается запросом на перерисовку экрана. int WINAPI WinMain(HINSTANCE hlnst. HINSTANCE. PSTR. int)
mJiBitmap = NULL: m_hMemDC = NULL: m_h01dBmp - NULL:
HDC hDC = GetDC(NULL): int width = GetSystemMetrics(SM_CXSCREEN): int height - GetSystemMetrics(SM_CYSCREEN);
virtual -KDDBO
{
for (int i=0: i<2000: i++)
BOOL AttachtHBITMAP hBmp): BOOL LoadBitmap(HINSTANCE hlnst. int id)
BOOL rslt = StretchBlt(hDC. randO % width, randO % height. -64, -64. hDC. randO % width, randO % height, 32. 32. (randO % 256) « 16):
return Attach ( : :LoadBitmap(hInst. MAKEINTRESOURCE(id)) ): }
SelectObjecUhDC. GetStockObject(WHITE_BRUSH)): DeleteObject(hBrush): Sleep(l):
RedrawWindow(NULL. NULL. NULL. RDWJNVALIDATE | RDW_ALLCHILDREN): return 0:
ReleaseODBO:
}
HBRUSH hBrush = CreateSolidBrush(RGB(rand( Ж56. rand(«256.rand(Д256)): SelectObject(hDC. hBrush):
ReleaseDCCNULL, hDC):
BOOL Draw(HDC hDC. int xO. int yO. int w. int h. DWORD гор. int opt=draw_normal): // Запросить размер, подготовить совместимый контекст устройства // и выбрать в нем растр bool KDDB-Prepare (int & width, int & height)
{
Различные способы отображения DDB-растров
BITMAP bmp: if ( ! GetObject(m_hBitmap, sizeof(bmp). & bmp) ) return false:
Функции BitBlt и StretchBlt обычно применяются при выводе аппаратно-зависимых растров. При правильном использовании эти две функции способны создавать разнообразные графические эффекты. В листинге 10.5 приведен простой класс для работы с DDB. Функция KDDB: :Draw позволяет рисовать обычные растры, растры, выровненные по центру и масштабированные по размерам приемника, растры с сохранением пропорций, а также мозаичные растры.
width = bmp.bmWidth; height = bmp.bmHeight: if ( m hMemDC==NULL )
public: HDC mJiMemDC: bool Preparetint & width, int & height): typedef enum { drawjiormal. draw_center. draw_tile,draw__stretch. draw_stretchprop }: HBITMAP GetBitmap(void) const { return m_hBitmap:
// Убедиться в том. что создание // растра прошло успешно
HDC hDC - GetDC(NULL): mJiMemDC = CreateCompatibleDC(hDC): ReleaseDC(NULL. hDC): mJiOldBmp = (HBITMAP) SelectObject(m_hMemDC. mJiBitmap):
Листинг 10.5. class KDDB { protected: HBITMAP mJiBitmap: HBITMAP mJiOldBmp: void ReleaseDDB(void):
579
Использование DDB-растров
return true:
// Освобождение ресурсов void KDDB::ReleaseDDB(void) { if ( mJiMemDC )
{
SelectObject(m_hMemDC. mJiOldBmp): DeleteObject(m_hMemDC): inJiMemDC = NULL;
Продолжение :
580
Глава 10. Основные сведения о растрах
Листинг 10.5. Продолжение DeleteObject(mJiBitmap): mJiBitmap = NULL:
}
return TRUE: } break;
m_h01dBmp = NULL;
.
}
case draw_stretch: return StretchBltthDC. xO, yO, w. h, mJiMemDC. 0.0. bmpwidth, bmpheight. гор);
BOOL KDDB::Attach(HBITMAP hBmp)
{
581
for (int j=0; j
if ( mJiBitmap )
{
Использование DDB-растров
if ( hBmp==NULL ) return FALSE:
case draw_stretchprop: { int ww = w: int hh = h:
if ( mJiOldBmp ) // Исключить mJiBitmap
{ SelectObject(m_hMemDC, mJiOldBmp); mJiOldBmp = NULL:
if ( w * bmpheight < h * bmpwidth ) // Выбор оси hh = bmpheight * w / bmpwidth; // для масштабирования else ww = bmpwidth * h / bmpheight;
if ( mJiBitmap ) // Удалить текущий растр DeleteObject(m_hBitmap) :
// Пропорциональные масштабирование и центровка return StretchBlt(hDC. xO + (w-ww)/2, yO + (h-hh)/2, ww. hh. mJiMemDC. 0. 0. bmpwidth. bmpheight, гор);
mJiBitmap = hBmp: // Заменить новым растром if ( mJiMemDC ) // Выбрать в совместимом контексте устройства, { // если он есть m_h01dBmp = (HBITMAP) SelectObject(m_hMemDC. mJiBitmap): return mJiOldBmp != NULL:
default: return FALSE;
else return TRUE:
BOOL KDDB::Draw(HDC hDC. int xO. { int bmpwidth, bmpheight:
int yO.
int w.
int h,
DWORD гор,
int opt)
if ( ! Preparetbmpwidth. bmpheight) ) return FALSE; switch ( opt ) { case drawjiormal: return BitBltthDC. xO, yO. bmpwidth. bmpheight. mJiMemDC. 0. 0. гор); case draw_center: return BitBlt(hDC. xO + (w-bmpwidth)/2. yO + ( h-bmpheight)/2. bmpwidth, bmpheight. m_hMemDC. 0. 0. гор): break; case draw_tile:
Класс KDDB содержит три переменные: манипулятор совместимого контекста устройства и два манипулятора HBITMAP для нового DDB-растра и для старого DDB-растра, исключаемого из совместимого контекста. DDB-растры чаще всего создаются загрузкой из ресурсов. Экземпляры класса KDDB следует размещать вне обработчика сообщения WM_PAINT, чтобы свести к минимуму затраты на преобразование ресурса из формата DIB в DDB и на создание совместимого контекста устройства. В методе KDDB::Draw поддерживаются различные варианты рисования растра, определяемые последним параметром. В приведенной версии реализованы нормальный вывод, центровка, мозаичная раскладка, простое и пропорциональное масштабирование. В пользовательском интерфейсе растры все чаще используются для оформления заставочных окон (splash screens) и фона. Для небольших текстур часто применяется мозаичное повторение; растры, изображающие самостоятельные объекты, часто выводятся с центровкой и пропорциональным масштабированием.
Сохранение окна/экрана После того как DDB-растр будет выбран в совместимом контексте устройства, вы можете выполнять с ним различные операции с помощью функций GDI.
582
Глава 10. Основные сведения о растрах
Простейшей операцией является сохранение содержимого окна, и как частный случай — сохранение всего экрана. Задача решается приведенной ниже функцией CaptureWindow. HBITMAP CaptureWindow(HWND hWnd) { RECT wnd; if ( ! GetWindowRect(hWnd. & wnd) ) return NULL: HOC hDC = GetWindowDC(hWnd); HBITMAP hBmp = CreateCompatibleBitmapdiDC. wnd.right-wnd.left, wnd.bottom - wnd.top); if
(hBmp)
{ HDC hMemDC - CreateCompatibleDC(hDC); HGDIOBJ hOld - SelectObjectthMemDC. hBmp); BitBltChMemDC. 0. 0. wnd.right - wnd.left. wnd.bottom - wnd.top. hDC. 0. 0. SRCCOPY); SelectObject(hMemDC. hOld); DeleteObjectthMemDC):
} ReleaseDC(hWnd, hDC): return hBmp;
}
Функция CaptureWindow возвращает манипулятор DDB, который затем можно преобразовать в DIB, сохранить в дисковом файле или скопировать в буфер обмена.
Преобразование цветов DDB Два контекста устройств, исходный и приемный, могут иметь разный формат кадрового буфера или характеристики палитры. Например, монохромный исходный растр может копироваться на приемную поверхность с 32-разрядной кодировкой цвета, или наоборот — исходное изображение в формате True Color может копироваться на монохромную поверхность. В этом случае функция BitBlt/ StretchBl t преобразует пикселы из цветового формата источника к формату приемника. Если один из контекстов устройств является совместимым контекстом с выбранным DDB-растром, несовпадение возможно лишь в том случае, если один из контекстов является монохромным. Например, для экрана в режиме с 24-разрядным цветом совместимый контекст устройства обычно создается в соответствующем цветовом формате. Функции LoadBitmap и CreateCompatibleBitmap генерируют только 24-разрядные или монохромные растры. GDI позволяет выбрать в контексте устройства только 24-разрядный или монохромный DDB-растр. Если приложение создает растр с 8-разрядным цветом, создание растра пройдет
Использование DDB-растров
583
успешно, но попытка выбрать его в контексте устройства, совместимом с экраном, завершится неудачей. При выводе монохромного растра на цветной поверхности GDI не ограничивается простым отображением черного и белого цвета; вместо этого GDI позволяет раскрасить растр с использованием атрибутов основного и фонового цвета контекста устройства. По умолчанию фоновым цветом контекста устройства является белый цвет, а основным цветом (цветом текста) — черный, но вместо них можно выбрать любые другие цвета функциями SetBkColor и SetTextCol or. В монохромном растре значения пикселов равны 0 и 1. Считается, что пикселы со значением 0 относятся к основному цвету, а пикселы со значением 1 — к фоновому. При выводе пикселов основного цвета (0) GDI использует основной цвет приемного контекста устройства, а при выводе пикселов фонового цвета (1) — фоновый цвет приемного контекста. В следующем фрагменте показано, как создать мозаичную раскладку с использованием разных основных и фоновых цветов. const COLORREF ColorTab1e[] = { RGBCOxFF. 0. 0). RGB(0. OxFF. 0). RGB(0. 0. OxFF). RGB(OxFF. OxFF. 0). RGB(0. OxFF. OxFF). RGB(OxFF. 0. OxFF) for (int y=0: y
BitBlt(hDC. x, y. bmpwidth. bmpheight. hMemDC. 0. 0. SRCCOPY): } При выводе цветного растра в монохромном контексте устройства каждому цветному пикселу необходимо поставить в соответствие либо 0, либо 1. Мы зна ем, что при выводе DIB в монехромном контексте устройства GDI пытается по добрать для каждого пиксела ближайший цвет, однако при выводе DDB интер фейс GDI действует совершенно иначе. Цвет каждого пиксела сравнивается i фоновым цветом исходного контекста устройства. Значения пикселов, совпа дающих с фоновым цветом, преобразуются в 1 (белый), а остальные пиксель преобразуются в 0 (черный). Обратите внимание: в процессе преобразование учитывается только фоновый цвет, а основной цвет не используется. Этот на первый взгляд «наивный» способ преобразования цветных растров : монохромные на самом деле оказывается очень полезным. Он обеспечивает про стые средства для деления пикселов растра на фоновые и не относящиеся фону; сгенерированный при этом растр может использоваться в качестве масю Маска может пригодиться в тернарных растровых операциях для отображени растров с прозрачными участками (спрайтов). Подробное описание вывода прс зрачных растров вы найдете в главе 11. Ниже приведена новая функция класса KDDB, которая генерирует монохромны растр по заданному цвету фона. Функция CreateMask использует вызов Create Bitmap для создания монохромного растра, устанавливает заданный цвет в качс стве фонового в исходном совместимом контексте устройства, после чего прео(: разует цветной растр в монохромный функцией BitBlt.
584
Глава 10. Основные сведения о растрах
HBITMAP KDDB::CreateMask(COLORREF crBackGround. HOC hMaskDC) { int width, height: if ( ! Prepare(width. height) ) return NULL: HBITMAP hMask = CreateBitmaptwidth, height. 1. 1. NULL); HBITMAP hOld = (HBITMAP) SelectObject(hMaskDC. hMask); SetBkColor(m_hMemDC. crBackGround); BitBlt(hMaskDC. 0. 0. width, height. mJiMemDC. 0. 0. SRCCOPY); return hOld: } На рис. 10.3 изображены 9 масок, созданных функцией KDDB: :CreateMask для каждого из цветов, задействованных в цветном изображении. На первом месте показан цветной растр, а затем следуют монохромные маски. При отображении масок используется основной и фоновый цвет по умолчанию; 1 соответствует белому цвету, а 0 — черному.
585
Использование DDB-растров
новые пикселы растра должны быть окрашены в фоновый цвет меню. Кроме того растры необходимо масштабировать по высоте команд меню. Растр, манипулятор которого передается функциям SetMenuItemBitmap и SetMenuItemlnfo, нельзя удалять до тех пор, пока меню не перестает использоваться. В листинге 10.6 приведен класс для работы с растрами-метками команд меню. Листинг 10.6. Использование растров в качестве меток для команд меню
class KCheckMark protected: typedef enum HBITMAP int HBITMAP i nt
MAXSUBIMAGES = 50 }:
mJiBmp; m_nSubImage!d[MAXSUBIMAGES]; mJiSublmage [MAXSUBIMAGES]; mjnUsed:
public: KCheckMarkO { mJiBmp = NULL:
m nUsed = 0:
-KCheckMarkO; void AddBitmap(int id. HBITMAP hBmp): void LoadToolbar(HMODULE hModule. int resid. bool transparent=false); HBITMAP GetSublmageOnt id): BOOL SetCheckMarkstHMENU hMenu. UINT uPos. UINT uFlags. int unchecked, int checked);
}: void KCheckMark::AddBitmap(int id. HBITMAP hBmp) Рис. 10.3. Разложение цветного DDB-растра на монохромные маски
Использование растров в меню В программировании для Windows с каждой командой меню можно связать два маленьких растра. Эти растры выводятся рядом с командой; первый — когда команда активизирована (checked), а второй — когда команда пассивна (unchecked). По умолчанию Windows не выводит растры для пассивных команд, а активные команды помечаются стандартным растровым рисунком в виде «галочки». Впрочем, эти растры вовсе не обязаны соответствовать активному или пассивному состоянию команды. Скажем, маленький значок в виде принтера рядом с командой Print определенно делает меню более наглядным. Для изменения этих растров-меток используются функции SetMenuItemBitmaps и SetMenuItemlnfo. Хотя сами по себе эти функции просты, с подготовкой и обработкой растров дело обстоит сложнее. Чтобы растр сливался с фоном меню, фо-
if ( mjiUsed < MAXSUBIMAGES ) m_nSubImageId[m_nUsed] = id; m hSublmage [m_nUsed++] = hBmp:
void KCheckMark::LoadToolbar(HMOOULE hModule. int resid. bool transparent) mJBmp - (HBITMAP) : :LoadImage(hModule. MAKEINTRESOURCE(resid). IMAGE_BITMAP. 0. 0. transparent ? LR_LOADTRANSPARENT : 0): AddBitmapUint) hModule + resid. mJiBmp): KCheckMark: :-KCheckMarkO
Продолжение :
586
Глава 10. Основные сведения о растрах
Листинг 10.6. Продолжение {
for (int 1=0: i<m_nllsed: 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: HOC hMemDCS = CreateCompatibleDC(NULL); HOC hMemDCD = CreateCompatibleDC(NULL);
587
Использование DDB-растров
Класс KCheckMark загружает один растровый рисунок, состоящий из нескольких растров меньшего размера, расположенных рядом (наподобие растров, используемых при работе с панелями инструментов). Растр загружается функцией Loadlmage, которая обеспечивает замену фоновых пикселов стандартным цветом окна (COLOR_WINDOW) при помощи флага LR_LOAOTRANSPARENT. Цвет окна по умолчанию обычно совпадает с фоновым цветом меню, и это помогает нам решить проблему слияния растра с фоном меню. Большая часть полезной работы в этом классе выполняется функцией GetSublmage, которая «вырезает» из растра небольшой фрагмент. Предполагается, что фрагменты расположены в одну строку, поэтому их размеры вычисляются по высоте общего растра. Функция получает размеры растров, используемых в качестве меток для команд меню, и масштабирует по ним фрагменты. Идентификатор и манипулятор растра-фрагмента сохраняются в таблице, чтобы их можно было задействовать в будущем. Выбор новых меток вместо назначенных ранее или используемых по умолчанию выполняет функция SetMenuImageltems. Поскольку операционная система Windows не создает копий растров, таблица манипуляторов используется в деструкторе класса для удаления растровых объектов. Экземпляры класса KCheckMark должны существовать на уровне окна или приложения, чтобы их деструкторы вызывались лишь тогда, когда растр перестает использоваться. На рис. 10.4 изображен результат применения растров для оформления некоторых стандартных команд меню. Исходный растр загружен из модуля browserui.dll, идентификатор ресурса 275.
SelectObjecUhMemDCS. mJiBmp); int w = GetSystemMetncs(SM_CXMENUCHECK); int h - GetSystemMetrics(SM_CYMENUCHECK);
* Forward
HBITMAP hRslt - CreateCompatibleBitmapdiMemDCS. w. h): if ( hRslt ) { HGDIOBJ hOld - SelectObjectdiMemDCD. hRslt): StretchBlt(hMemDCD. 0. 0. w, h. hMemDCS, id*bmp.bmHeight, 0. bmp.bmHeight, bmp.bmHeight. SRCCOPY): SelectObject(hMemDCD. hOld): AddBitmap(id. hRslt)} DeleteObject(hMemOCS); DeleteObject(hMemDCD): return hRslt: BOOL KCheckMark::SetCheckMarks(HMENU hMenu. UINT uPos. UINT uFlags int unchecked, int checked) return SetMenuItemBitmaps(hMenu. uPos. uFlags. GetSubImage(unchecked). GetSublmage(checked)): Рис. 10.4. Оформление стандартных команд меню растровыми метками
588
Глава 10. Основные сведения о растрах
Растры также позволяют заменить обычный текст в командах в меню Для этой цели используются функции AppendMenu, InsertMenuItem и SetMenuItemlnfo В следующей программе показано, как создать подменю с растровыми командами. void KBitmapMenu::AddToMenu(HMENU hMenu. int nCount, HMODULE hModule const int nID[], int nFirstCommand) { mJiMenu = hMenu; mjiBitmap = nCount; mjiChecked = 0: m_nFirstCommand = nFirstCommand: for (int i=0: i
CheckMenuItem(m_hMenu, mjiChecked + nFirstCommand MFJYCOMMAND | MF_CHECKED)}
На рис. 10.5 изображено растровое меню с вариантами текстур и окно заполненное выбранной текстурой с помощью функции KDDB: :Draw.
589
Использование DDB-растров
В 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 IParam); virtual LRESULT WndProc(HWND hWnd, UINT uMsg. WPARAM wParam. LPARAM IParam):
static LRESULT CALLBACK BackGroundWindowProc(HWND hWnd, UINT uMsg. WPARAM wParam, LPARAM IParam); public: KBackgroundO m OldProc = NULL; Рис. 10.5. Меню с растровыми командами
virtual -KBackgroundO
Продолжение :
590
Глава 10. Основные сведения о растрах
Листинг 10.7. Продолжение
BOOL Attach(HWND hWnd); BOOL Detatch(HWNO hWnd);
// Реализация KBackground const TCHAR Prop_KBackground[] - _T("KBackground Instance"); LRESULT KBackground::EraseBackground(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM 1 Param) { return DefWindowProc(hWnd, uMsg. wParam. IParam); LRESULT KBackground::WndProc(HWND hWnd. UINT uMsg. WPARAM wParam LPARAM IParam) { if ( uMsg — WM_ERASEBKGND )
return EraseBackground(hWnd, uMsg. wParam. IParam); else return CallWindowProc(m_01dProc. hWnd. uMsg. wParam. IParam); LRESULT KBackground::BackGroundWindowProc(HWND hWnd, UINT uMsg.
{
WPARAM wParam. LPARAM IParam)
KBackground * pThis - (KBackground *) GetProp(hWnd, PropJCBackground); if ( pThis ) return pThis->WndProc(hWnd, uMsg, wParam. IParam); else return DefWindowProc(hWnd. uMsg. wParam. IParam):
BOOL KBackground-Attach (HWND hWnd) {
SetProp(hWnd. PropJBackground. this); mJDldProc - (WNDPROC) SetWindowLong(hWnd. GWL_WNDPROC. (LONG) BackGroundWindowProc): return m 01dProc!=NULL:
BOOL KBackground::Detatch(HWND hWnd) { RemoveProp(hWnd. Prop_KBackground): if ( mJDldProc ) return SetWindowLong(hWnd. GWL_WNDPROC. (LONG) m_01dProc) -= (LONG) BackGroundWindowProcelse return FALSE:
Использование DDB-растров
591
В классе KBackground определяются два виртуальных метода (не считая виртуального деструктора). Метод EraseBackground рисует фон окна; реализация по умолчанию просто вызывает DefWi ndowProc. Метод WndProc обрабатывает все сообщения, хотя в данном случае нас интересует только сообщение WM_ERASEBKGND. Субклассирование существующего окна выполняется вызовом метода Attach. С окном ассоциируется свойство, значение которого представляет собой указатель на экземпляр класса KBackground, а функция окна переопределяется статической функцией BackGroundWi ndowProc. Получив сообщение, эта функция запрашивает значение свойства, чтобы получить указатель this для экземпляра KBackground, а затем передает вызов его методу WndProc. Обратите внимание — мы не можем сохранить указатель this в поле GWL_USERDATA, как в классе KWindow, поскольку субклассируемое окно может быть создано другой стороной, использующей поле GWLJJSERDATA. Не годятся и глобальные переменные, поскольку мы хотим использовать наш класс для одновременной поддержки нескольких окон, но при этом обойтись без создания глобальных диспетчерских таблиц, как в MFC. Класс KBackground решает общую задачу субклассирования и перехвата сообщения WM_ERASEBKGND. Однако существуют различные варианты прорисовки фона — линиями, образующими решетчатый узор, заливкой замкнутых областей, функциями вывода DIB или DDB. Эти варианты реализуются в специализированных классах, производных от родового класса KBackground. Реализация, ориентированная на вывод DDB, приведена в листинге 10.8. Листинг 10.8. Класс для прорисовки фона выводом DDB
class KDDBBackground : public KBackground { KDDB i nt
m_DDB; mjiStyl e:
virtual LRESULT EraseBackground(HWND hWnd. UINT uMsg. WPARAM wParam, LPARAM IParAO; public: KDDBBackgroundO
{
mjnStyle = KDDB::draw_tile:
void SetStyleCint style) { mjnStyle = style: } void SetBitmap(HMODULE hModule. int nRes) { m_DDB.LoadBitmap(hModule. nRes):
LRESULT KDDBBackground::EraseBackground(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam)
Продолжение
592
Глава 10. Основные сведения о растрах
Использование DDB-растроа
593
Листинг 10.8. Продолжение if ( m_DDB.GetBitmap() ) RECT rect: HOC hDC = СHOC) wParam; GetClientRect(hWnd. & rect): HRGN hRgn = CreateRectRgnIndirect(&rect): SaveDC(hDC): SelectClipRgnChDC, hRgn): DeleteObject(hRgn); . m_DDB.Draw(hDC, rect.left. rect.top. rect.right - rect.left. rect.bottom - rect.top. SRCCOPY. m_nStyle)RestoreDC(hDC. -1): return 1: // Обработано }
else return DefWindowProcthWnd. uMsg, wParam. IParam): Для загрузки и вывода DDB класс KDDBBackground использует класс KDDB. Он переопределяет метод EraseBackground и реализует в нем вывод фона. Использовать этот класс несложно. Все, что от вас потребуется, - создать экземпляр класса KDDBBackground, задать растр и стиль вывода, а затем субклассировать окно вызовом метода Attach. На рис. 10.6 показаны результаты субклассирования для диалогового окна, группирующей^рамки и статической рамки (frame). Диалоговое окно заполняется деревянной текстурой, в группирующей рамке кирпичная текстура выравнивается по центру, а в статической рамке та же текстура подвергается пропорциональному масштабированию. А самое замечательное - то, что для каждого окна задача решается всего тремя строками кода при обработке сообщения WMJNITDIALOG.
// KDDBBackground whole: // KDDBBackground groupbox: // KDDBBackground frame: whole.SetBitmap(m_hInstance. IDB_PAPER01); whole.SetStyle(KDDB::draw_tile): whole.Attach(hWnd): groupbox.SetBitmap(mJiInstance, IDB_BRICK02): groupbox.SetStyle(KDDB::draw_center); groupbox.Attach(GetDlgItem(hWnd. IDC_GROUPBOX)): frame.SetBitmap(m_hInstance. IDB_BRICK02); frame.SetStyleCKDDB::draw_stretchprop): frame.Attach(GetDlgItem(hWnd, IDC_FRAME)):
Рис. 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 GetDIBColorTabletHDC hDC. UINT uStartlndex, UINT cEntries. RGBQUAD * pColors): UINT SetDIBColorTabletHDC hDC. UINT uStartlndex, UINT cEntries. CONST RGBQUAD * pColors);
typedef struct tabDIBSection { BITMAP BITMAPINFOHEADER DWORD HANDLE DWORD } DISSECTION:
dsBtn; dsBmih: dsBitfields[3]; dshSection: dsOffset;
CreateDIBSection Функция CreateDIBSection создает объект DIB-секции. Из параметров этой функции самыми важными являются первые три. В первом параметре передается указатель на эталонный контекст устройства. Параметр pbmi указывает на структуру BITMAPINFO, содержащую манипулятор блока описания растра, битовые маски и цветовую таблицу. Параметр iUsage сообщает, содержит ли цветовая таблица значения в формате 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-секцию с типом сжатия BI_RLE4, 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: KDIBSectionО
597
Использование DDB-растров
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[]) DISSECTION dibsec: if ( GetObject(m_hBitmap. sizeof(DIBSECTION). & dibsec) ) KDIB::DecodeDIBFormat(desp); _tcscat(desp. _T(" ")): DecodeDDB(GetBitmap(). desp + Jxslen(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[nSize]: 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. Если вызов был успешным, функция копирует структуру BITMAPINFO и заполняет новую цветовую таблицу; затем вызывается функция D I B : : AttachDIB, инициализирующая переменные класса KDIB. Обратите внимание — мы ограничиваемся освобождением новой структуры BITMAPINFO; деструктор класса KDDB вызывает DeleteObject с манипулятором DIB-секции, что приводит к освобождению массива пикселов, выделенного GDI.
Функции GetObjectType и GetObject для DIB-секции DDB-растры и DIB-секции относятся к общей категории объектов GDI, но между ними, конечно, существуют принципиальные различия. Располагая только манипулятором объекта GDI, трудно сказать, к чему он относится — к DDBрастру или DIB-секции. Для DIB-секции функция GetObjectType возвращает то же ' значение OBJ_BITMAP (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(DISBSECTION), а размер буфера, на который указывает IpvObjects, достаточен для хранения структуры DIBSection. Структура DIBSection содержит информацию о DIB-секции, которую GDI предоставляет приложениям. В первом поле хранится структура BITMAP, которая описывает DIB-секцию со стороны DDB. Во втором поле хранится структура BITMAPINFOHEADER, описывающая размеры и цветовой формат DIB. Помните, что вместо нее также может использоваться структура BITMAPV4HEADER или BITMAPV5HEADER. Вероятно, на уровне GDI следовало бы определить структуры DIBSECTIONV4 и DIBSECTIONV5. Поле dsBitFields содержит массив из трех битовых масок, используемых в 16- и 32-разрядных режимах (в режимах BI_RGB и BI_BITFIELDS). По ним
599
Использование DDB-растров
можно определить текущий формат пикселов в режиме с кодировкой 16 бит/пиксел. Если вы создаете 16-разрядную DIB-секцию в формате BI_RGB, проверьте поле dsBitFields структуры DIBSection, и вы узнаете, какой формат пикселов используется — 5-5-5 или 5-6-5. Два последних поля предназначены для создания DIBсекции на базе объекта файла, отображаемого на память. Их значения совпадают с параметрами, передаваемыми при вызове CreateDIBSection.
GetDIBColorTable и SetDIBColorTable DIB-секция не является полноценным DIB-растром, поскольку приложение не имеет прямого доступа к цветовой таблице, как при работе с цветовой таблицей DIB. Цветовая таблица DIB-секции находится под управлением GDI, и приложение работает с ней только через функции GetDIBColorTable/SetDIBColorTable. Напрашивается вопрос: зачем приложению обращаться к цветовой таблице DIB-секции, если оно уже предоставило ее при создании DIB-секции функцией CreateDIBSection? Существует минимум две веские причины. Во-первых, если при вызове функции CreateDIBSection параметр iUsage был равен DIB_PAL_COLORS, приложение работает только с индексами логической палитры, а не с цветовой таблицей RGB. Если приложение захочет сохранить DIB-секцию в BMP-файле, ему понадобится нормальная цветовая таблица RGB. Во-вторых, многие графические алгоритмы (например, регулировка оттенка, яркости и насыщенности растра или преобразование его к оттенкам серого) реализуются операциями с цветовой таблицей изображений. Для этого приложение выполняет необходимые манипуляции с цветовой таблицей RGB и возвращает ее новое состояние. Функция GetDIBColorTable возвращает цветовую таблицу RGB для DIB-секции. В первом параметре функции передается совместимый контекст устройства, в котором должна быть выбрана DIB-секция. Параметры uStartlndex и cEntries сообщают начальную позицию и количество копируемых элементов. Параметр pCol ors указывает на буфер для записи цветовой таблицы в виде массива структур RGBQUAD. Возвращаемое значение функции определяет количество скопированных элементов; 0 является признаком ошибки. Функция SetDIBColor заполняет цветовую таблицу DIB-секции данными из таблицы, предоставленной приложением. Она вызывается с теми же параметрами и возвращает количество скопированных элементов. Ниже приведены соответствующие функции класса KDIBSection. Обратите внимание: мы используем тот же совместимый контекст устройства, который использовался при выводе растра, а цветовая таблица DIB-секции хранится в переменной KDIB::m_pRGBQUAD. // Копирование цветовой таблицы DIB-секции в цветовую таблицу DIB UINT KDIBSection: :GetColorTable(vo1d) { int width, height:
if ( (GetDepth()>8) |
! Prepare(width. height) ) // Создать совместимый // контекст устройства
return 0: return GetDIBColorTable(m_hMemDC, 0. mjndrUsed. m_pRGBQUAD):
600
Глава 10. Основные сведения о растрах
// Копирование цветовой таблицы DIB в цветовую таблицу OIB-секции UINT KDIBSection::SetColorTable(void) { int width, height; if ( (GetDepth()>8) || ! Prepare(width. height) )// Создать совместимый // контекст устройства return 0; return SetDIBColorTable(m_hMemOC. 0. mjiClrUsed. m_pRGBQUAD);
Применение DIB-секций: аппаратнонезависимый вывод DIB-секции обладают рядом преимуществ по сравнению с DIB и DDB. О Аппаратно-зависимый вывод средствами GDI. GDI не оказывает особой помощи в построении DIB. Для DDB поддерживается всего один цветовой формат, совместимый с текущим режимом экрана. Если графическое приложение хочет реализовать 24-разрядный вывод в экранном режиме с кодировкой 8 бит/пиксел средствами GDI, это можно сделать только с использованием DIB-секции. О Объединение вывода средствами GDI с прямым доступом к массиву пикселов. Только DIB-секции поддерживают одновременный вывод средствами GDI с прямым доступом к массиву пикселов в приложениях. Без DIB-секций вам придется создавать DIB и DDB и передавать пикселы между ними функциями GetDIBits и SetOIBits. О Гибкая схема управления памятью. 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, а потом полученное изображение можно будет сохранить в BMP-файле. Функции SaveWindow (сохранение содержимого окна) и РгатеЗО (рисование объемной рамки) приведены в листинге 10.10. Листинг 10.10. Сохранение окна и построение рамки в DIB-секции
BOOL SaveWindowtHWND hWnd. bool bClient. int nFrame. COLORREF crFrame) RECT wnd; if ( bClient ) if ( ! GetClientRectChWnd. & wnd) ) return FALSE; else if ( ! GetWindowRectthwnd. & wnd) ) return FALSE; KBitmapInfo bmi ; KDIBSection dibsec; bmi. SetFormat (wnd. right - wnd. left + nFrame * 2. wnd bottom - wnd. top + nFrame *2. 24. BI_RGB): if ( dibsec. CreateOIBSection(NULL. bmi .GetBMK). DIB_RGB_COLORS. NULL. NULL) ) { int width, height; dibsec. Prepare(width. height): // Создать совместимый // контекст устройства. // выбрать в нем dibsec if ( nFrame ) { Frame(dibsec.m_hMemDC. nFrame. crFrame. 0. 0. width, height); TCHAR Title[128]; GetwindowTextthWnd. Title. sizeof(Title)/sizeof(Title[0])) : SetBkMode ( di bsec . mJiMemDC . TRANSPARENT ) : SetTextColor(dibsec.m_hMemDC. RGB(OxFF. OxFF. OxFF)); TextOut(dibsec.m_hMemDC. nFrame. (nFrame-20)/2. Title. _tcslen(Title)): HOC hDC: if ( bClient ) hDC = GetDC(hWnd);
602
Глава 10. Основные сведения о растрах
Листинг 10.10. Продолжение else hDC = GetWindowDC(hWnd): // Скопировать содержимое экрана в DIB-секцию BitBltCdibsec.mJiMemDC, nFrame. nFrame. width - nFrame * 2. height - nFrame * 2, hDC. 0, 0. SRCCOPY): ReleaseDC(hWnd, hDC):
Использование DDB-растров
603
скопировать пикселы из экранного контекста устройства в центр DIB-секции. Затем программа сохраняет DIB-секцию в BMP-файле вызовом метода KDIB:: SaveFile. Объемная рамка строится из прямоугольников высотой в один пиксел, цвет которых постепенно темнеет, а начиная с середины рамки — светлеет, что создает иллюзию полукруглой рамки. Пример изображен на рис. 10.7.
return dibsec.SaveFileCNULL); } return FALSE:
void Frame3D(HOC 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 1=0: {
i
i++)
HBRUSH hBrush = CreateSolidBrush(RGB(red. green, blue)): FrameRectthDC. & rect. hBrush): // Один пиксел DeleteObject(hBrush): if ( i
// Первая половина
red = red * 19/20: // Темнее green = green * 19/20: blue = blue * 19/20:
else
// Вторая половина
// Светлее red = red * 19/18; if ( red>255 ) red 255: green = green * 19/18; if ( green>255 ) green 255; blue = blue * 19/18; if ( blue>255) blue = 255; InflateRect (Srect. -1. -1): // Меньше Функция SaveWindow получает четыре параметра. Параметр hWnd содержит манипулятор окна, параметр bClient определяет сохраняемую часть (все окно или клиентская область), параметр nFrame содержит толщину добавляемой рамки, а параметр crFrame определяет цвет рамки. Функция готовит структуру BITMAPINFO для 24-разрядной DIB-секции, используя несложный класс KBitmapInfo, предназначенный для инициализации BITMAPINFO. Экземпляр KDIBSection создается в стеке. После создания DIB-секции функция SaveWindow создает совместимый контекст и выбирает в нем DIB-секцию. Теперь можно вызвать функцию Frame3D для рисования рамки, вывести надпись с текстом из заголовка окна и, наконец,
Рис. 10.7. Сохраненная клиентская область в рамке
Приведенный пример показывает, как использовать GDI для вывода в DIBсекции. Прямой доступ к массиву пикселов организуется так же, как это делается в DIB. Дополнительная информация приведена в следующей главе.
Применение ОХВ-секций: вывод в высоком разрешении Выделение памяти для хранения DIB-секции в адресном пространстве пользовательского режима позволяет работать с изображениями значительно больших размеров, чем при использовании DDB. Возможность создания DIB-секции на базе манипулятора файла, отображаемого на память, упрощает контроль над размещением данных на диске и в памяти. Эти две особенности часто используются в графических редакторах и программных процессорах растровых изображений (Raster Image Processor, RIP). Процессор растровых изображений получает документ, написанный на языке описания страниц, и преобразует его в растровое изображение высокого разрешения, который после полутоновой обработки передается на принтер высокого разрешения. Например, программу Ghostscript можно рассматривать как программный процессор RIP — Ghostscript получает документ в формате PostScript и воспроизводит его в растровом виде в разных вариантах разрешения. В этом разделе мы напишем несложный программный процессор RIP, работа которого основана на использовании DIB-секции. Конечно, документы PostScript слишком сложны для подобных примеров, но мы прибегнем к помощи GDI
604
Глава 10. Основные сведения о растрах
и воспользуемся другим выразительным, полезным и общедоступным средством — расширенными метафайлами Windows (EMF). Наша задача — создать файл, отображаемый в память, на базе которого будет создана DIB-секция высокого разрешения, и затем воспроизвести EMF-файл в этой DIB-секции. Завершив воспроизведение, мы удаляем DIB-секцию, закрываем файл и получаем растровое представление EMF-файла в высоком разрешении. Основные трудности связаны с тем, что функция CreateDIBSection требует, чтобы смещение массива пикселов было кратно DWORD. BMP-файлы не удовлетворяют этому требованию, поскольку их массивы пикселов никогда не начинаются на границе DUORD. Мы пойдем обходным путем и воспользуемся 24-разрядным графическим форматом Targa, который содержит очень простой заголовок регулируемой длины и поддерживает несжатые массивы пикселов RGB в 24-разрядном формате. В листинге 10.11 приведен класс КТагда24, предназначенный для работы с DIB-секциями в 24-разрядном графическом формате Targa. Листинг 10.11. Работа с DIB-секциями с использованием файлов, отображаемых на память class KTarga24 : public KDIBSection { #pragma pack(push.1) typedef struct { BYTE IDLength: BYTE CoMapType: BYTE ImgType: WORD Index: WORD Length; BYTE CoSize: WORD X Org; WORD Y Org; WORD Width; WORD Height: BYTE Pixel Size; BYTE AttBitS; char ID[14]: } ImageHeader; Ipragma pack(pop) HANDLE HANDLE
// // // // // // // // // // // // //
00 Длина строки-идентификатора 01 О = таблица отсутствует 02 2 = TGA_RGB 03 индекс первого элемента цветовой таблицы 05 количество элементов в цветовой таблице 07 размер элемента цветовой таблицы 08 О OA О ОС ширина OE высота 10 размер пиксела 11 О 12 заполнитель, обеспечивающий выравнивание ImageHeader по границе DWORD
mJiFile: m_hFileMapping;
public: KTarga24() mJiFile = INVALID_HANDLE_VALUE; mJiFileMapping = INVALID_HANDLE_VALUE; virtual -KTarga24()
Использование DDB-растров
ReleaseDDBO: ReleaseDIBO: if ( m_hFileMapping!=INVALID_HANDLE_VALUE CloseHandle(m_hFi1eMappi ng): if ( mJiFile !- INVALID_HANDLE_VALUE ) CloseHandle(mJiFile): 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 = sizeof(tgaheader.ID): tgaheader.ImgType = 2; tgaheader.Width = width: tgaheader.Height - height; tgaheader.Pixel Size = 24: strcpy(tgaheader.ID. "BitmapShop"); mJiFile = CreateFiletpFileName. 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: mJiFileMapping = CreateFileMapping(m_hFile, NULL. PAGE_READWRITE. 0. sizeof(tgaheader) + imagesize. NULL); if ( m_hFileMapping==INVALID_HANDLE_VALUE ) return FALSE: DWORD dwWritten = NULL: WriteFilednJiFile, & 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_COLORS. m_hFi1eMappi ng. si zeof(tgaheader));
605
606
Глава 10. Основные сведения о растрах
Класс КТагда24 определен как производный от класса KDIBSection. Он содержит две новые переменные для хранения манипуляторов файла и файлового отображения. Главным методом класса является метод Create, при вызове которого передается ширина, высота и имя файла. Функция создает объекты файла и файлового отображения, заполняет 32-байтовый заголовок графического формата Targa и создает DIB-секцию с использованием объекта файлового отображения. Поскольку длина заголовка равна 32 байтам, смещение массива пикселов равно 32 — величине, кратной DWORD. Обратите внимание: файл должен быть создан с общим доступом для чтения и записи, и файловое отображение также должно иметь права доступа для чтения и записи; в противном случае попытки создания DIB-секции или вывода в файл завершатся неудачей. Объекты файла и файлового отображения закрываются в деструкторе после удаления объекта DIB-секции (в ReleaseDDB). Расширенные метафайлы (EMF) описаны в главе 16. Пока достаточно запомнить, что EMF-файл представляет собой записанную последовательность команд GDI, которая легко обрабатывается и воспроизводится. Приведенная ниже функция использует класс КТагда24 для воспроизведения EMF-файла. BOOL RenderEMF(HENHMETAFILE hemf. int width, int height, const TCHAR * tgaFileName)
{
KTarga24 targa: int w = (width+3)/4*4; // Убедиться, что значение кратно 4 if ( targa.Create(w, height. tgaFileName) ) { targa.Preparetw. height):
Итоги В этой главе рассматривались три типа растров, поддерживаемых в GDI, — аппаратно-независимые растры (DIB), аппаратно-зависимые растры (DDB) и DIBсекции. Основное внимание уделялось вопросам создания, преобразования, отображения и простейшего применения этих типов растров, а также различиям между ними. Растры — настолько серьезная тема, что ее описание разделено на три главы. В главе 11 рассматриваются нетривиальные и интересные аспекты применения растров: растровые операции, прозрачность, прямой доступ к пикселам и альфаналожение. Глава 12 посвящена обработке изображения посредством прямого доступа к пикселам. Кроме того, в главе 17 рассматривается декодирование и печать изображений в формате JPEG.
Примеры программ В этой главе рассматриваются два примера программ (табл. 10.6). Важно то, что работа этих двух программ основана на нескольких полезных классах, которые в усовершенствованном виде будут использоваться в других главах, посвященных растровым изображениям. Таблица 10.6. Программы главы 10 Каталог проекта Samples\Chapt_10\Bitmaps
BitBltUarga.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
Итоги
Samples\Chapt_10\Scrambler
Описание
Демонстрация загрузки и сохранения BMP-файлов, сохранения экрана, различных способов отображения растров, применения растровых меток и команд меню, растровых фонов, DIB-секции, аппаратно-независимого выврда с использованием DIB-секции и вывода растров в шестнадцатсричном формате Применение функции StretchBlt для случайной перестановки фрагментов экрана^
609
Тернарные растровые операции
ка. Растровые операции, объединяющие эти три цвета, обычно называются тернарными растровыми операциями. В GDI поддерживаются только поразрядные логические операции, которые выполняются с каждым битом пиксела независимо от других пикселов и ограничиваются булевыми операциями AND (&), OR (|), NOT (-) и XOR (Л). При таких ограничениях существует 256 (2Л(2А3), или 28) возможных тернарных растровых операций.
Коды растровых операций
Глава 11 Нетривиальное использование растров Растры играют чрезвычайно важную роль в программировании для Windows, поэтому эту тему невозможно охватить в одной главе. В предыдущей главе описаны три типа растров, поддерживаемых в GDI: аппаратно-независимые растры (DIB), аппаратно-зависимые растры (DDB) и DIB-секции. Мы рассмотрели основные принципы работы с растрами, в том числе различные способы их отображения, применение растровых изображений в пользовательском интерфейсе и даже программную реализацию вывода с высоким разрешением. Однако предыдущая глава далеко не исчерпывает темы растровых изображений. В этой главе мы будем изучать растровые операции, прозрачность и альфаналожение. Глава 12 посвящена работе с растровыми изображениями на уровне прямого доступа к пикселам, а палитры рассматриваются в главе 13.
Гернарные растровые операции При рисовании линий или заливке областей GDI использует бинарные растровые операции, которые определяют способ объединения пиксела пера или кисти с пикселом приемника и получения нового пиксела приемника. В GDI поддерживается шестнадцать бинарных растровых операций, для работы с ними используется пара функций SetROP2 и GetROP2. Вполне логично предположить, что при работе с растрами существуют похожие растровые операции, позволяющие создавать всевозможные специальные эффекты. При этом возникает новый фактор — пикселы изображения-источника. Таким образом, при работе с растрами приходится учитывать три фактора: цвет пиксела пера или кисти, цвет пиксела приемника и цвет пиксела источни-
Для представления 256 разных растровых операций достаточно одного байта. Учитывая, что каждый бит интерпретируется независимо от остальных, механизм кодировки выглядит весьма простым. Предположим, Р — бит пера или кисти, S — бит источника, a D — бит приемника. Если результат растровой операции всегда равен Р, операции присваивается код OxFO. Если результат операции всегда совпадает с S, код равен ОхСС, а если он всегда совпадает с D, то код равен ОхАА. Ниже приведены определения этих кодов растровых операций на языке C/C++: const BYTE rop_P = OxFO; const BYTE rop_S = OxCC; const BYTE rop_D = OxAA;
/ / 1 1 1 1 0 0 0 0 / / 1 1 0 0 1 1 0 0 / / 1 0 1 0 1 0 1 0
Все остальные растровые операции определяются на основании этих трех констант посредством булевых операций. Например, если в результате растровой операции 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. Например, растровая операция ОхЕ2 записывается формулой DSPDxax. Преобразуя ее в инфиксную запись, мы получаем D A (S&(P A D))). В более наглядном виде эта формула выглядела бы так: (S&P) | (-S&D), или SPaSnDao. Почему же первой формуле отдается предпочтение перед второй? По двум причинам. На большинстве процессоров реализована инструкция XOR, не уступающая по скорости операции AND. В первой формуле используются три операции, а во второй — четыре. Для вычисления первой формулы нужен только один дополнительный
610
Глава 11. Нетривиальное использование растров
611
Тернарные растровые операции
регистр, а для второй — два регистра. Ниже приведена реализация этих формул на псевдокоде (для кадрового буфера с 32-разрядной кодировкой цвета): // Реализация ОхЕ2 DSPDxax по схеме DA(S&(P"D)) mov eax. Р // Р хог eax. D // P A D A and eax. S // S&(P D) X хог eax. D // D (S&(P"D)) mov D. eax // Записать результат // Реализация OxE2 mov eax and eax mov ebx not ebx and ebx D or eax, ebx mov D. eax
0: SPDDDDDD 1:SPDSPDSP 2: SDPSDPSD 3: DDDDDDDD 4: DDDDDDDD
// -S&D // (S&P) | (-S&D) // Записать результат
Растровые операции в GDI обычно кодируются 32-разрядными двойными словами DWORD вместо простых байтов от 0 до 255. В старшем слове хранится один из 256 однобайтовых кодов растровых операций, о которых говорилось выше; младшее слово содержит кодировку формулы, определяющей растровую операцию. В исходной архитектуре для определения растровой операции используется только младшее слово; старшее слово содержит дополнительную информацию для аппаратных блиттеров. В старых реализациях растровые операции кодировались вручную на оптимизированном ассемблере, поэтому предпочтение отдавалось общим алгоритмам вместо 256 разных случаев для разных растровых операций и большой таблицы переходов. В современных реализациях используется автоматический генератор растровых операций, который в отличие от своих предшественников не жалуется на необходимость создания 256 разных функций. Механизм кодировки младшего слова тернарной растровой операции — настоящее произведение искусства, появившееся в те давние времена, когда программистам приходилось тщательно обдумывать каждую строку машинного кода и экономить каждый бит памяти. В формулах растровых операций используется 7 разных символов, причем длина формулы может достигать 12 символов. Простейшему механизму кодировки для этого понадобилось бы Iog2(712) или 33,7 бита информации, но разработчикам приходилось ограничиваться одним 16-разрядным словом. Формула растровой операции делится на две части: операторы и операнды (по аналогии со стеками операторов и операндов на некоторых стековых машинах). Операторы кодируются старшими 11 битами, а оставшиеся 5 бит остаются для операндов. Из 11 бит 10 используются для кодировки пяти логических операций, по два бита на операцию: 0 — NOT, 1 — XOR, 2 — OR, 3 — AND. Последний флаговый бит является признаком последней операции NOT. Закодировать строку операндов всего 5 битами очень нелегко, но умные программисты отыскали в строках операндов повторяющиеся цепочки. Всего было выделено 8 таких цепочек, для кодировки которых достаточно трех битов. Последние два бита определяют величину сдвига цепочек. Схема кодировки младшего слова тернарных операций GDI изображена на рис. 11.1.
Цепочка
5: S+SP-DSS
Оператор
SPaSnDao по схеме (S&P)|(~S&D) S S&P S
6: S+SP-PDS 7: S+SD-PDS
Op 5
I Op 3
Op 4
14
13
12
Op 1
Цепочка
Not
Смещение
j
J^
15
Op 2
11
10
9
8
7
6
5
4
3
2
1
0
Рис. 11.1. Структура младшего слова растровой операции
Конечно, сказанное стоит пояснить на конкретном примере. Возьмем растровую операцию ОхЕ2; полный код растровой операции равен ОхООЕ20746, поэтому младшее слово равно 0x0746. Таким образом, Ор5 = NOT, Op4 = NOT, ОрЗ = XOR, Op2 = 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 и NOMIRRORBITMAP. Флаг CAPTUREBLT (0x40000000) используемся при работе с окнами, имеющими собственную поверхность вывода, которая может объединяться с содержимым других окон посредством альфа-наложения. При использовании флага CAPTUREBLT пикселы всех окон, расположенных поверх текущего окна, включаются в итоговое изображение. По умолчанию изображение состоит только из содержимого текущего окна. Флаг NOMIRRORBITMAP (0x80000000) предотвращает зеркальное отражение растров по вертикали и горизонтали оси из-за разной направленности осей в исходном и приемном прямоугольниках.
Диаграмма тернарных растровых операций Алгебраические формулы растровых операций точны и удобны, но визуальное представление поможет вам лучше разобраться в многочисленных разновидностях этих операций. Создав шаблоны для трех переменных, мы сможем сгенерировать итоговый растр по алгебраическим формулам и наглядно увидеть результаты применения всех операций. На рис. 11.2 приведена простая диаграмма тернарных растровых операций, полученная применением растровых операций к трем растрам, изображенным слева.
613
Тернарные растровые операции
Растр узора ( 8 x 8 пикселов) сгенерирован применением шаблона OxFO по направлениям X и Y. Источник сгенерирован по шаблону ОхСС, а приемник по шаблону ОхАА. В данном примере белому цвету соответствует логическое значение 1, а черному - логический ноль. 256 маленьких растров представляют все возможные результаты растровых операций Они сгенерированы созданием узорной кисти по узорному растру и последующим объединением источника с приемником при выбранной узорной КИСТИ
Ниже приведен код, использованный при построении диаграммы растровых операций Обратите внимание: растры 8 x 8 требуются для того, чтобы узорная кисть работала и в системах семейства Windows 95. Растр генерируется в совместимом контексте устройства, а затем масштабируется в экранном контексте устройства, поскольку узорные кисти не масштабируются. const WORD Bit_Pattern [] - { OxFO. OxFO. OxFO. OxFO. OxOF. OxOF. OxOF, OxOF }: const WORD Bitjource [] - { OxCC. OxCC. 0x33. 0x33, OxCC. OxCC. 0x33, 0x33 }: const WORD Bit_Destination[] = { OxAA, 0x55, OxAA, 0x55, OxAA. 0x55, OxAA. 0x55 }: void Rop3Chart(HDC hDC) HBITMAP Pbmp = CreateBitmap(8. HBITMAP Sbmp = CreateBitmap(8, HBITMAP Dbmp = CreateBitmap(8. HBITMAP Rbmp = CreateBntmap(8. HBRUSH HDC HDC HDC
Pat Sr Dst Rst
= • = -
8. 3. 3. 3,
I. 1. 1. 1.
I, 1. 1. 1.
Bit_Pattern); Bit_Source): Bit_Destination): NULL):
CreatePatternBrush(Pbmp); CreateCompatibleDC(hDC): CreateCompat1b1eDC(hDC): CreateCompatitjleDC(hDC):
// Узорная кисть // Совм. DC для источника // Совм. DC для приемника // Совм. DC для результата
SelectObject(Src. Sbmp); SelectObjecttOst. Dbmp); SelectObject(Rst. Pbmp): StretchBltthDC. 20. 20, StretchBlUhDC. 20. 220. StretchBltdnDC. 20. 420.
), Rst, 0. 0. ], 8. SRCCOPY); ). Src, 0. 0. 3. 8, SRCCOPY): 3. Dst. 0. 0. 3. 8, SRCCOPY);
SetBkMode(hDC, TRANSPARENT): TextOutthDC. 20, 105, "Pattern". 7): TextOuUhDC. 20. 305. "Source". 6): TextOutChDC. 20. 505. "Destination". 11): Se1ectObject(Rst. Rbmp); SelectObject(Rst, Pat);
for (int i=0: i<16: i++) Рис. 11.2. Диаграмма тернарных растровых операций
"0*X". i); TextOut(hDC, 140+ i*38. 10. mess. 2):
614
Глава 11. Нетривиальное использование растров
wsprintfCmess. "ПО", 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 + Crop*16)*38. 30 + (rop/16)*38. 32. 32. Rst. 0. 0, 8. 8. SRCCOPY):
Код ROP
Формула
Имя ROP2
OxF00021
Р
R2_COPYPEN
OxOFOOOl
-Р
R2_NOTCOPYPEN
SRCCOPY
ОхСС0020
S
NOTSRCCOPY
0x330008
-S
ОхАА0029
D
0x550009
-D
ОхСОООСА
Р & S
OxFOOOSA
Р|S
Ох0500А9
-(Р | D)
R2_NOTMERGEPEN
ОхОА0329
-P&D
R2_MASKNOTPEN
0x5000325
P&-D
R2_MASKPENNOT
Ох5А0049
Р"D
R2JORPEN
Ox5FOOE9
-(P&D)
R2_NOTMASKPEN
ОхАОООСЭ
Р & D
R2_MASKPEN
Имя ROP3 PATCOPY
Приемник
DeleteObject(Src); DeleteObject(Dst): DeleteObjectCRst): DeleteObject(Pat): DeleteObject(Pbmp); DeleteObject(Sbmp); DeleteObject(Dbmp); DeleteObject(Rbmp);
Приемник и источник
MERGECOPY
Узор и приемник
PATINVERT
Часто используемые растровые операции Набор из 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
Формула
BLACKNESS
0x000042
0
R2JLACK
WHITENESS
OxFF0062
1
R2_WHITE
Имя ROP2
615
Тернарные растровые операции
Приемник и источник
ОхА50065
- (Р
А
OxAF0229
-Р
D
R2_MERGENOTPEN
OxF50225
Р | ~D
R2_MERGEPENNOT
OxFA0089
Р|0
R2JCRGEPEN
NOTSRCERASE
ОхПООАб
-( S | D)
SRCERASE
0x440328
S & -D
D)
0x660046
S*D
*Ох8800С6
S & D
MERGEPAINT
ОхВВ0226
-S | D
SRCPAINT
ОхЕЕ0086
S |D
PATPAINT
ОхРВОАОЭ
P | -S | D
ОхВ8074А
P A (S&(P A D)))
ОхЕ20746
D A (S&(P A D)))
SRCINVERT SRCAND
Узор, источник и приемник
R2_NOP
R2_NOTXORPEN
Растровые операции в таблице упорядочены по степени зависимости от трех переменных Р, S и D. В этом отношении эта таблица отличается от большинства таблиц растровых операций, содержимое которых обычно упорядочивается по числовым значениям кодов. Понимание зависимостей поможет вам в программировании. Например, если растровая операция зависит от приемника (D), вряд ли ее стоит использовать при печати. Растровые операции предназначены для растровых устройств, у которых каждый пиксел адресуем, доступен для чтения и для записи. Некоторые графические устройства (например, принтеры PostScript) не являются полноценными растровыми устройствами; это означает, что они не
616
Глава 11. Нетривиальное использование растров
поддерживают полного набора растровых операций (особенно тех, которые зависят от пикселов приемника). Если растровая операция не зависит от растраисточника, его не обязательно передавать при вызовах функций BitBlt, StretchBIt и StretchDIBits. В GDI предусмотрена специальная функция для выполнения растровых операций, не использующих источника: BOOL PatBlt(HDC hDC. int nXLeft. int nYLeft. int nWidth, int nHeight. DWORD dwROP):
.Функция PatBlt комбинирует текущую кисть с прямоугольным регионом приемника. Обратите внимание на отсутствие параметров, определяющих растр-источник. Набор допустимых растровых операций не ограничивается именами ROP, выбранными Microsoft. Вы можете использовать любые растровые операции, в которых не задействован источник. Узнать, использует ли растровая операция ту или иную переменную, несложно. Для этого достаточно проверить, генерирует ли ROP одинаковые результаты при значениях этой переменной, равных 1 и 0. Проверочные функции для трех переменных приведены ниже. boo! inline RopNeedsNoDestination(int Rop) { return ((Rop & OxAA) » 1) == (Rop & 0x55);
} boo! inline RopNeedsNoSource(int Rop)
{ return ((Rop & OxCC) » 2) == (Rop & 0x33)-
}
bool inline RopNeedsNoPatterndnt Rop) {
return ((Rop & OxFO) » 4) == (Rop & OxOF);
BLACKNESS, WHITENESS Эти две растровые операции обычно используются для инициализации поверхностей и их возврата в исходное состояние. Операция BLACKNESS присваивает всем пикселам О, WHITENESS устанавливает все биты пикселов в 1. На устройстве с палитрой результат не всегда соответствует черному и белому цвету, но обычно первым цветом в палитре является черный, а последним — белый. Операции BLACKNESS и WHITENESS не зависят ни от одной из трех переменных. Как нетрудно догадаться, самая эффективная реализация этой операции организует заполнение памяти постоянными величинами. BLACKNESS реализуется вызовом memsetCpBits, 0, nlmageSize), a WHITENESS — вызовом memsetCpBits, OxFF, nlmageSize). При создании DDB или DIB-секции массив пикселов часто находится в неопределенном состоянии (исключение составляют инициализируемые DDBрастры и DIB-секции, отображаемые на память). Создаваемые поверхности рекомендуется инициализировать и переводить в определенное состояние. Черный и белый цвета отличаются от других тем, что они представляются как 0 и 1. В частности, для цвета С справедливы следующие утверждения: Black AND С - Black Black OR С = С
Тернарные растровые операции
617
Black XOR С = С White AND С = С White OR С = White White XOR С = NOT С При частичном заполнении растров черным или белым цветом с применением масок эти свойства играют важную роль при создании интересных эффектов.
Только узор: PATCOPY, R3_NOTCOPYPEN Операция PATCOPY используется для заполнения прямоугольных областей текущей кистью (по аналогии с функцией FillRect). Противоположная операция (OxOFOOOl) не имеет официального названия, поэтому в табл. 11.1 имя для нее выбрано по аналогии с бинарной операцией R2_NOTCOPYPEN. Операция R3_NOTCOPYPEN заполняет прямоугольную область цветом, противоположным цвету текущей кисти.
Только источник: SRCCOPY, NOTSRCCOPY Мы неоднократно встречались с операцией SRCCOPY, которая просто копирует пикселы источника в приемник. Эта операция обычно требуется для отображения растра в исходном цвете. Операция NOTSRCCOPY копирует в приемник цвет, противоположный цвету источника. В режимах True Color и High Color эта операция может использоваться для создания негативов. В Windows NT 4.0 операция NOTSRCCCPY иногда реализуется неправильно (согласно документации (KB Q174534)). Ошибка должна быть исправлена в Service Pack 4.
Только приемник: R3J40P, DSTINVERT Операция DSTINVERT меняет цвет пиксела приемника на противоположный. Операция R3_NOP заменяет цвет пиксела приемника тем же цветом — напрасная трата процессорного времени. Вероятно, у GDI хватает сообразительности, чтобы игнорировать операцию R3_NOP 'ч обойтись без напрасного перемещения пикселов.
Без приемника: MERGECOPY
Существует десять тернарных растровых операций, зависящих от источника и узора и не зависящих от приемника. Лишь одной из этих операций было присвоено имя — MERGECOPY. Имя операции MERGECOPY (OxCOOOCA) выбрано неудачно. В бинарных растровых операциях строка «MERGE» обозначает логическую операцию OR, но здесь это правило почему-то не соблюдается. Операция MERGECOPY заменяет пиксел приемника результатом конъюнкции пиксела источника с пикселом узора. Если растр-источник является монохромным, MERGECOPY окрашивает белые пикселы в цвет кисти и оставляет черные пикселы без изменений. Если растр-источник не является монохромным, иногда бывает удобнее использовать в качестве маски кисть. Например, чтобы в цветном изображении отображался только красный канал, создайте однородную красную кисть (RGB(OxFF,0,0)) и воспользуйтесь растровой операцией MERGECOPY для вывода изображения. Красный канал копируется без изменений, а остальные две составляющие обнуляются. В результате создается изображение, состоящее из оттенков красного цвета.
618
Глава 11. Нетривиальное использование растров
Тернарные растровые операции
bmi8bpp.bmiColor[i].rgbRed = i & GetRValue(Mask): bmi8bpp.bmiColor[i].rgbGreen = i & GetGValue(Mask): bmi8bpp.bmiCo1or[i].rgbBlue - i & GetBValue(Mask);
Приведенная ниже функция выводит один канал изображения, определяемый заданной маской. Например, если параметр mask равен RGB(OxFF,0,0), отображается только красный канал. void DisplayChannelCHDC hDC. int x, int y. int width, int height. HOC hDCSource. COLORREF mask) { HBRUSH hRed = CreateSolidBrush(mask) ; HBRUSH hOld = (HBRUSH) SelectObjecUhDC. hRed): BitB1t(hDC. x. y. width, height. hDCSource. 0. 0. MERGECOPY); SelectObjectthDC. hOld): DeleteObject(hRed):
HBITMAP hRslt = CreateDIBSection(NULL, (BITMAPINFO *) & bmiSbpp. NULL. DIB_RGB_COLORS. NULL. NULL): if ( hRs1t==NULL ) return NULL; SelectObjectthMemDC. hRslt);
Разделение цветовых составляющих — стандартный прием, поддерживаемый во многих графических редакторах. Говорят, специалисты по компьютерной графике предпочитают просматривать изображение по каналам, устранять все дефекты и потом получать итоговое изображение, объединяя все каналы. Именно так создаются полноценные изображения в оттенках серого цвета. Если в приведенном выше примере приемная поверхность имеет 8-разрядный формат пикселов, а цветовая таблица настроена в соответствии с одноканальной цветовой шкалой, то позднее вам останется лишь изменить цветовую таблицу для получения изображения в оттенках серого. В листинге 11.1 приведена функция Channel Split, разделяющая произвольный цветной DIB-растр на три изображения в оттенках серого (по одному для каждого канала RGB).
HBRUSH hBrush = CreateSolidBrush(Mask); // Однородный красный. // зеленый или синий цвет HGDIOBJ hOld - SelectObjectdiMemDC. hBrush);
Листинг 11.1. Деление растра на каналы RGB // Создание изображения в оттенках серого (DIB-секции) // по одному каналу RGB в DIB HBITMAP ChannelSplittconst BITMAPINFO * pBMI. const void COLORREF Mask. HDC hMemDC)
SelectObject(hMemDC. hOld): DeleteObject(hBrush);
int width = pBMI->bmiHeader.biWidth; int height - pBMI->bmi Header. bi Height: BMI8BPP bmi8bpp; memsetUbmiSbpp, 0, sizeof(bmi8bpp)): bmi 8bpp.bmi Header.bi Si ze bmiSbpp.bmiHeader.biWidth bmi 8bpp.bmi Header.bi Hei ght bmi8bpp.bmiHeader.biPlanes bmi 8bpp.bmi Header.bi Bi tCount bmiSbpp.bmiHeader.biCompression
StretchDIBits(hMemDC. 0. 0. width, height. 0. 0. width, height. pBits, pBMI, DIB_RGB_COLORS. MERGECOPY); for (i-0: i<256; i++) // Перейти к настоящей цветовой таблице // оттенков серого цвета bmiSbpp.bmiColor[i].rgbRed bmiSbpp.bmiColor[i].rgbGreen bmi 8bpp.bmi Color[i].rgbBlue ;
SetDIBColorTable(hMemDC. 0. 256. bmiSbpp.bmiCol or);
return hRslt;
pBits.
typedef struct { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColor[256]: } BMI8BPP;
= Sizeof(BITMAPINFOHEADER); - width: - height: - 1: - 8; - BI_RGB:
for (int i-0; i<256: 1++) // Цветовая таблица одного из каналов RGB
619
}
Функция Channel Split создает DIB-секцию с 8-разрядной кодировкой пикселов и цветовой таблицей, содержащей оттенки цвета одного из каналов RGB. Например, если параметр mask равен RGB(255,0,0), цветовая таблица будет содержать элементы RGB(O.O.O), RGB(1,0,0) и т. д. до RGB(255,0,0). DIB-секция выбирается в совместимом контексте устройства вместе с однородной кистью, созданной на базе маски того же канала. Функция StretchDIBits использует растровую операцию MERGECOPY для выделения красного канала и сопоставляет каждому пикселу элемент цветовой таблицы DIB-секции. Если пиксел исходного DIB-растра равен RGB(r,g,b), в результате операции MERGECOPY генерируется цвет RGB(r,0,0), В цветовой таблице DIB-секции ему соответствует индекс г, который и сохраняется в массиве пикселов DIB-секции. Наконец, происходит модификация цветовой таблицы, чтобы DIB-секция выводилась как изображение в оттенках серого цвета. Функция ChannelSplit работает с любыми DIB-растрами — с любой цветовой глубиной, с битовыми масками, сжатыми и несжатыми, почти не требуя допол нительного кодирования с вашей стороны. Впрочем, есть и недостатки — хотя i большинстве случаев сгенерированное изображение в оттенках серого выгляди:
620
Глава 11. Нетривиальное использование растров
вполне нормально, результат не идеален. Для получения более точного результата GDI пришлось бы просматривать всю цветовую таблицу в поисках идеального совпадения или оптимального приближения, а это относительно медленный процесс. Судя по результату (что, впрочем, проявляется лишь в изображениях с плавными переходами цветов), GDI использует механизм аппроксимации — вероятно, по соображениям быстродействия. Вместо того чтобы просматривать всю цветовую таблицу в поисках каждого пиксела, можно построить сетку RGB из N х N х N точек. При необходимости каждая точка сетки сопоставляется с элементом цветовой таблицы. Каждый пиксел RGB, сгенерированный в результате растровой операции, аппроксимируется ближайшей точкой сетки, индекс которой и считается цветовым индексом пиксела. Реализация полноценного алгоритма деления изображения на цветовые каналы требует операций непосредственно с массивом пикселов. Мы вернемся к этой теме позднее.
Тернарные растровые операции
HICON hlcon = (HICON) LoadlmagethMod. MAKEINTRESOURCE(resid). IMAGEJCON. 48. 48. LR_DEFAULTCOLOR); if ( hlcon) { DrawIconthDC. х. у. hlcon): ICONINFO iconinfo: Getlconlnfo(hlcon. & iconinfo): Destroylcon(hlcon); BITMAP bmp: GetObjectCiconinfo.hbmMask. sizeof(bmp), & b m p ) : , HGDIOBJ hOld = SelectObject(hMemDC. iconinfo.hbmMask): BitBlt(hDC. x+56. y. bmp.bmWidth. bmp.bmHeight. hMemDC.O.O.SRCCOPY): Sel ectObject(hMemDC, iconi nfо.hbmColor): BitBltChDC. x+112, y. bmp.bmWidth, bmp.bmHeight. hMemDC.O.O.SRCCOPY):
Без источника: PATINVERT Из 10 тернарных растровых операций, зависящих от узора и приемника, но не от источника, имя присвоено только операции PATINVERT. Указанные растровые операции обладают теми же возможностями, что и бинарные растровые операции, выбираемые функцией SetROP2. Операции ROP, в которых не задействован источник, могут использоваться с функциями PatBlt, что позволяет обойтись без более сложных вызовов. Одной из областей их применения является модификация текущего изображения в контексте, соответствующего физическому устройству или блоку памяти, связанному с DDB или DIB-секцией. Например, если у вас имеется черно-белое изображение, вы можете воспользоваться операцией R3_MASKPEN(OxAOOOC9), чтобы раскрасить его цветной кистью или разделить на каналы RGB. При использовании кисти с шахматным узором операция R3_MASKPEN позволяет создать эффект частичного затенения, при котором половина пикселов сохраняет прежний цвет, а другая половина закрашивается черным цветом.
Тернарные операции без узора Следующая группа растровых операций использует источник и приемник, но не использует узор. Из 10 возможных растровых операций этой категории имена присвоены 6. Вероятно, Microsoft считает эти операции более важными. Наличие имени у тернарной растровой операции обычно означает, что она требуется самой операционной системе. Операции SRCAND и SRCINVERT используются Windows при отображении значков и курсоров мыши. Ресурс значка/курсора обычно содержит группу изображений для разных вариантов размера и цветовой глубины. Каждый значок/курсор обычно состоит из двух растров: черно-белой маски и цветной маски. Черно-белый значок или курсор может состоять из одного растра двойной высоты; в этом случае выводимый растр описывается второй половиной растра. Маска выводится растровой операцией SRCAND и стирает ту область, в которой будет находиться цветной растр. После этого цветной растр выводится операцией SRCINVERT. Следующий фрагмент поможет вам лучше разобраться в применении растровых операций при выводе значка:
621
SelectObject(hMemDC. iconinfo.hbmMask): BitBltChDC. x+168, y. bmp.bmWidth, bmp.bmHeight. hMemDC.0.0,SRCAND); Sel ectObject(hMemDC, iconinfo.hbmColor): BitBltChDC. x+168. y. bmp.bmWidth, bmp.bmHeight. hMemDC.0.0,SRCINVERT); Sel ectObject(hMemDC, hOld); DeleteObject(iconinfo.hbmMask): Del eteObject(i coni nfо.hbmCol or):
}
Программа загружает значок размером 48 х 48 из модуля Loadlmage и выводит его стандартной функцией Win32 Drawl con; при этом значок масштабируется по своим стандартным размерам (обычно 32 х 32). Для удовлетворения нашего любопытства вызывается функция Getlconlnfo, возвращающая манипуляторы двух DDB-растров — маски и цветной растра. Затем эти два растра выводятся по отдельности, чтобы мы могли присмотреться к ним поближе. Далее вывод значка имитируется выводом обоих растров в одном и том же месте. При выводе маски используется растровая операция SRCAND, а при выводе цветного растра — операция SRCINVERT. На рис. 11.3 показано строение некоторых значков, используемых в графической среде Windows. Прежде чем продолжить обсуждение, давайте определимся с некоторыми терминами. При выводе значка, определенного в виде прямоугольной области, некоторые пикселы этой области должны изменяться, а некоторые должны оставаться прежними. Изменяемая область называется непрозрачной (opaque); все остальные пикселы образуют прозрачную область. Обычно непрозрачная область определяет форму значка — например, мусорной корзины или папки. Из рисунка видно, что в маске непрозрачная область обозначается черным цветом (0), а прозрачная — белым цветом (1). Таким образом, первый вызов BitBlt, использующий растровую операцию SRCAND, закрашивает непрозрачную область черным цветом и оставляет прозрачную область без изменений. В цветном растре непрозрачная область заполнена цветными пикселами, а в прозрачной области находятся черные пикселы (0). Второй вызов BitBlt с использованием операции SRCINVERT выводит цветные пикселы без изменения прозрачной области.
622
Глава 11. Нетривиальное использование растров
623
Тернарные растровые операции
выводится растровой операцией SRCINVERT; в результате пикселы непрозрачной области заменяются пикселами цветного растра, а пикселы прозрачной области сохраняют прежнее состояние. Таблица 11.2. Прозрачное отображение растров с применением маски II !« a « 1:У ' • »
Рис. 11.3. Применение растровых операций при выводе значков
Если бы при создании маски непрозрачная область описывалась белым цветом (1), а для прозрачной области был зарезервирован черный цвет (0), для очистки непрозрачной области вместо SRCAND следовало бы использовать растровую операцию 0x220326 (DSna). Обратите внимание: оператор NOT в DSna фактически инвертирует маску в процессе применения. Маска и цветной растр могут иметь разные непрозрачные области. Если непрозрачная область маски меньше непрозрачной области цветного растра, при использовании второй растровой операции некоторые пикселы непрозрачной области не закрашиваются черным цветом (1); операция SRC INVERT заменяет пиксел приемника значением D*S вместо S. С другой стороны, при совпадении непрозрачных областей того же результата можно добиться растровой операцией SRCPAINT (DSo). . А что произойдет, если маска выглядит так, как показано на рис. 11.3, а прозрачные пикселы цветного растра окрашены в белый цвет (1) вместо черного (0)? В результате применения SRCAND и SRCINVERT прозрачные пикселы инвертируются вместо того, чтобы оставаться неизменными. Мы должны заменить первую растровую операцию, применяющую маску, на SRCERASE (SDna). Если в маске поменялись цвета, первая растровая операция должна быть заменена на NOTSRCERASE (SDon). Для вывода инвертированного источника можно воспользоваться растровой операцией MERGEPAINT (~S|D, DSno). Итак, мы нашли применения для всех шести тернарных растровых операций, не использующих узора, которым компания Microsoft присвоила имена. Если вы не полностью разобрались в том, как работают эти операции, в табл. 11.2 приведена краткая сводка их применения для вывода маски и цветного растра в разных условиях. В таблице для обозначения пикселов растра используется запись (X,Y), где X — цвет прозрачных пикселов, a Y — цвет непрозрачных пикселов. Таким образом, первая строка таблицы читается следующим образом: если в маске прозрачная область обозначена белыми пикселами, а непрозрачная область обозначена черными пикселами, после вывода маски операцией SRCAND прозрачная область остается без изменений, а непрозрачная область окрашивается в черный цвет. Затем цветной растр, в котором прозрачная область обозначена черным цветом,
Маска
ROP маски
Результат применения маски
Цветной растр
ROP цветного растра
Итоговый результат
(Белый, черный)
SRCAND
(D, черный)
(Черный, С)
SRCINVERT
(Белый, черный)
SRCAND
(D, черный)
(Черный, С)
SRCPAINT
(D,C)
(Белый, черный)
SRCAND
(D, черный)
(Белый, С)
MERGEPAINT
(D,C)
(Черный, белый)
R3_DSna
(D, черный)
(Черный, С)
SRCINVERT
(D,C)
(Белый, черный)
SRCERASE
(Dn, черный)
(Белый, С)
SRCINVERT
(D,C)
(Черный, белый)
NOTSRCERASE
(Dn, черный)
(Белый, С)
SRCINVERT
(D,C)
Другие растровые операции Мы рассмотрели тернарные растровые операции, не зависящие от узора, источника или кисти, а также зависящие от одного или двух факторов. Общее количество растровых операций этих операций равно 2 + 2 x 3 + 1 0 x 3 = 38. Остальные 218 растровых операций зависят от всех трех переменных. Из этих 218 операций в GDI имя было присвоено только операции PATPAINT (P|-S|D). Операция PATPAINT объединяет пиксел кисти; инвертированный пиксел источника и пиксел приемника логическим оператором OR. He зная условий, которым должны подчиняться эти переменные, трудно понять, зачем нужна подобная растровая операция. Пока оставим PATPAINT в покое и займемся другими операциями, для которых можно найти практическое применение. В одном из способов рисования прозрачных растров используются два растра: маска и цветной растр-источник (см. выше пример с выводом значков). Недостаток подобного решения заключается в необходимости создания маски, точно совпадающей с пикселами цветного растра. А если определить маску в виде кисти вместо отдельного растра? Предположим, мы создали шахматный узор, в котором половина пикселов окрашена в черный цвет, а другая половина остается белой. Можно ли вывести на поверхности устройства лишь каждый второй пиксел цветного растра, не изменяя второй половины? Какую растровую операцию следует для этого применить? Нужная операция легко вычисляется при помощи булевой алгебры. Эффект, которого мы хотим добиться, описывается формулой (P&S)| (-P&D): если пиксел
624
Глава 11. Нетривиальное использование растров
кисти равен 1 (белый), результат совпадает с пикселом растра-источника; в противном случае используется пиксел приемника. Заменяя Р, S и D кодами OxFO, ОхСС и ОхАА, мы получаем ОхСА. По таблице тернарных растровых операций мы находим полный код операции ОхСАОУАЭ и официальную формулу D A (P&(S~D)). Чтобы перейти к противоположной интерпретации Р, формула принимает вид (-P&S) | (P&D); ей соответствует код ОхАС. В результате мы получаем ОхАС0744 и Л А официальную формулу 5 (Р&(5 0)). Функция Fadeln, приведенная в листинге 11.2, при помощи растровой операции ОхСАОУАЭ создает эффект «постепенного проявления». Исходное изображение выводится за четыре шага с применением разных узорных кистей. На первом шаге проявляется 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: 1++) { HBITMAP hMask - CreateBitmap(8. 8, 1. 1. Mask[i]): HBRUSH hBrush= CreatePatternBrush(hMask): DeleteObject(hMask); HGDIOBJ hOld = SelectObjectthDC, hBrush);
// DA(P&(SXD)). if P then S else D StretchDIBits(hDC. x. y. w. h. 0, 0. w. h, pBits. pBMI. DIB_RGB_COLORS. ОхСАОУАЭ); SelectObject(hDC. hOld): DeleteObject(hBrush); Выше рассматривался вывод значков с применением растровых операций SRCAND и SRCINVERT за два вызова функции BitBlt. В системе семейства Windows NT можно было бы создать узорную кисть на базе маски и объединить два вызова в один, используя при этом растровую операцию (D&P) A S с кодом Ох6С01Е8. Эта идея реализована в следующем фрагменте.
625
Тернарные растровые операции
void MaskBitmapNT(HDC 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): SetBrushOrgExChDC. org.x, org.y. NULL); BitBlt(hDC. x, y, width, height. hMemDC. 0. 0. Ox6C01E8); // S"(P&D) Select-Object (hDC. hOld); DeleteObject(hBrush); } В системах семейства Windows 95 не поддерживаются узорные кисти больше 8 x 8 пикселов. Кроме того, из-за перемещения маски в узорную кисть эта функция будет нормально работать только в режиме отображения ММ_ТЕХТ, поскольку узорная кисть не масштабируется вместе с режимами отображения и мировыми преобразованиями. Для монохромного растра-источника можно воспользоваться цветной кистью, чтобы раскрасить растр и вывести его с использованием прозрачности. Если мы хотим, чтобы черные пикселы (0) источника выводились цветом кисти, а белые пикселы (1) оставались прозрачными, следует воспользоваться формулой (-S&P) | (S&D). ROP-код этой операции равен ОхВ8, или ОхВ8074А, а официальная формула имеет вид P A (S&(P A D)). При противоположной интерпретации растра-источника формула принимает вид (S&P) | (-S&D), и в итоге мы получаем код ОхЕ20746 с официальной формулой D A (S&(P A D)). Листинг 11.3 демонстрирует применение растровой операции ОхВ8074А для раскраски непрозрачных пикселов^монохромного растра произвольной кистью. Функция ColorBitmap осуществляет вывод с ROP-кодом ОхВ8074А, а функция TestCol or ing иллюстрирует раскраску монохромного растра пятью разными кистями. Листинг 11.3. Раскраска монохромного растра с применением растровых операций void ColorBitmaptHDC hDC. int x. int y, int w, HDC hMemDC. HBRUSH hBrush)
{
int h.
// P*(S&(P A D)). if (S) D else P HGDIOBJ hOldBrush = SelectObject(hDC. hBrush); BitBlUhDC. x. y. w. h. hMemDC, 0. 0. OxB8074A); SelectObjecUhDC. hOldBrush):
void TestColoringtHDC hDC. HINSTANCE hlnstance) { HBITMAP hPttrn: HBITMAP hBitmap = LoadBitmapthlnstance. MAKEINTRESOURCE(IDB_CONFUSE)): BITMAP bmp; n GetObject(hBitmap. sizeof(bmp). &bmp): Продолжение
626
Глава 11. Нетривиальное использование растров
627
Прозрачные растры
Листинг 11.3. Продолжение SetTextColor(hDC. RGB(0. 0. 0)): SetBkColorthDC. RGB(OxFF. OxFF, OxFF))-: HOC hMemDC - CreateCompatibleDC(NULL); HGDIOBJ hOld - SelectObject(hMemDC. hBitmap);
for (int 1=0: i<5: 1++) { HBRUSH hBrush; switch (i) {
case 0: hBrush = CreateSolidBrush(RGB(OxFF. 0. 0)): break: case 1: hBrush = CreateSolidBrush(RGB(0. OxFF. 0)); break; case 2: hPttrn = LoadBitmap(hInstance. MAKEINTRESOURCE(IDB_PATTERN01)); hBrush = CreatePatternBrush(hPttrn); DeleteObject(hPttrn): break; case 3: hBrush = CreateHatchBrush(HS_DIAGCROSS, RGB(0. 0, OxFF)); break; case 4: hPttrn = LoadB1tmap(hInstance, MAKEINTRESOURCE(IDB_WOOD01)): hBrush = CreatePatternBrush(hPttrn); DeleteObject(hPttrn);
ColorBitmap(hDC. 1*30+10-2. 1*5+10-2. bmp.bmWidth. bmp.bmHeight. hMemDC. (HBRUSH)GetStockObject(WHITEJRUSH)): ColorBitmapChDC. 1*30+10+2. 1*5+10+2. bmp.bmWidth. bmp.bmHeight. hMemDC. (HBRUSH)GetStockObject(DKGRAY_BRUSH)); ColorBitmapChDC. 1*30+10, i*5+10, bmp.bmWidth. bmp.bmHeight, hMemDC. hBrush); DeleteObject(hBrush); BitB1t(hDC. 240. 25. bmp.bmWidth, bmp.bmHeight. hMemDC. 0. 0, SRCCOPY); SelectObjectthMemDC. hOld); DeleteObject(hBitmap); DeleteObject(hMemDC): Функция загружает монохромный растр с изображением одного недоумевающего человечка и рисует группу из пяти человечков. В данном примере использовались белая и черная кисти и небольшое смещение выводимых растров, создающее простейший объемный эффект. Поскольку растровая операция ОхВ8074 рисует растры в прозрачном режиме, выводятся только пикселы непрозрачной области. На рис. 11.4 изображено пять цветных растров вместе с исходным монохромным растром (справа).
Рис. 11.4. Прозрачная раскраска монохромного растра
Прозрачные растры Даже при таком количестве растровых операций в компании Microsoft полагают, что прозрачный вывод растров — очень сложная задача, поэтому для ее решения были созданы три специальные функции: BOOL PlgBlttHDC hdcDest. CONST POINT** IpPoint. HOC hDCSrc. int nXSrc. int nYSrc, int nWidth. int nHeight, HBITMAP hbmMask, int xMask. int yMask): BOOL MaskBlt(HDC hdcDest. int nXDest. int nYDest, int nWidth, int nHeight. HOC hDCSrc, int nXSrc. int nYSrc. HBITMAP hbmMask, int xMask. int yMask. DWORD dwRop): BOOL TransparentBlt(HDC hdcDest. int nXOriginDest. int nYOriginDest. int nWidthDest, int nHeightDest. HOC hdcSrc, int nXOriginSrc, int nYOriginSrc. int nWidthSrc. int nHeightSrc. UINT crTransparent); Эти три функции поддерживаются не на всех платформах Win32. Функции PlgBlt и M a s k B l t поддерживаются только в системах семейства Windows NT, а функция TransparentBIt — только в Windows 98, Windows 2000 и последующих системах. Ниже будет показано, как эти функции имитируются на базе других растровых функций и прямого доступа к массиву пикселов. Даже при беглом взгляде на прототипы функций нетрудно заметить сходство между ними. Все функции получают два манипулятора контекстов устройств — для источника и приемника. Следовательно, эти функции работают как с совместимыми контекстами устройств, в которых выбран DDB-растр или DIB-секция, так и с физическими устройствами, поддерживающими растровые операции.
628
Глава 11.
Нетривиальное использование растров
DIB напрямую не поддерживаются; чтобы использовать эти функции, вам придется преобразовать DIB в DDB или в DIB-секцию.
Функция PlgBIt Функция PlgBIt решает две задачи: преобразование прямоугольного растра в параллелограмм и управление прозрачностью при помощи маски. Следовательно, результат вызова этой функции определяется тремя факторами: приемником, источником и узором. Исходный прямоугольник представляет собой подмножество точек поверхности-источника, определяемое параметрами nXSrc, nYSrc, nWidth и nHeight. Все параметры задаются в логической системе координат контекста-источника. Как и при использовании других, более простых функций блиттинга, в контекстеисточнике не могут действовать преобразования поворота и сдвига, однако смещение, масштабирование и зеркальные отражения допускаются. Это ограничение гарантирует, что исходный прямоугольник в системе координат устройства контекста-источника всегда соответствует прямоугольнику, стороны которого параллельны обеим осям. Параллелограмм-приемник определяется манипулятором контекста устройства и массивом из трех точек. Выше уже говорилось о том, что аффинное преобразование однозначно определяется отображением трех точек одного пространства в три точки другого пространства. Три точки, на которые ссылается параметр IpPoint, однозначно определяют параллелограмм на приемной поверхности, четвертая вершина которого вычисляется по формуле D = В + С - А, где А = 1pPoint[0], В = lpPoint[l] и С = 1pPoint[2]. Левый верхний угол исходного прямоугольника отображается в А, правый верхний угол отображается в точку В, левый нижний угол отображается в С, а правый нижний — в D. Отображение исходного прямоугольника в приемный параллелограмм представляет собой общее аффинное преобразование, допускающее смещение, масштабирование, отражение, повороты и сдвиги. С геометрической точки зрения функция PlgBlt по сравнению с StretchBIt обеспечивает дополнительные повороты и сдвиги. Растр маски определяется манипулятором растра и двумя целыми числами. Растр должен быть монохромным, в противном случае вызов функции завершится неудачей. Два целых параметра xMask и yMask определяют местонахождение пиксела маски, соответствующего левому верхнему углу растра-источника. При выходе за границы маски она применяется повторно — так же, как узорная кисть используется при заливке замкнутых областей. Если маска не задана, в приемном параллелограмме отображается все содержимое источника. В противном случае пикселы маски со значением 1 (белый) соответствуют участкам, копируемым из источника в приемник, а пикселы маски со значением 0 (черный) оставляют пикселы приемника без изменений. Если преобразовать маску в узорную кисть, логика ее применения выражалась бы растровой операцией с кодом ОхСА07А9, то есть P&S|-P&D. В системах семейства Windows NT функция PlgBlt может быть заменена функцией StretchBIt с настройкой мирового преобразования, преобразованием маски в узорную кисть и использованием растровой операции ОхСА07А9. В других
Прозрачные растры
629
системах StretchBIt может заменить PlgBlt лишь при отсутствии поворотов и сдвигов и при условии, что размеры маски не превышают 8 x 8 пикселов. В листинге 11.4 приведен пример использования функции PlgBlt для вывода объемного куба. Функция DrawCube рисует три грани куба функцией PlgBlt с применением источника и маски. Функция MaskCube управляет созданием маски в форме прямоугольника с закругленными углами, размер которой совпадает с размерами растра-источника. Листинг 11.4. Рисование трехмерного куба с использованием функции PlgBIt void DrawCube(HDC hDC. int x. int y, int dh, int dx. int dy. HDC hMemDC. int w. int h. HBITMAP hMask)
{
SetStretchBltModethDC. HALFTONE);
// 6 // 0 4 // 1 II г 5 // 3 POINT P[3] = { { x - dx. у - dy }, { x, у }. // 012 { x - dx. у - dy + dh } }; POINT Q[3] = { { x. у }. { x + dx. у - dy }. // 143 { x. у + dh } }; POINT R[3] - { { x - dx, у - dy }, { x, у - dy - dy }. // 061 { x. у } }: PlgBlt(hDC, P. hMemDC. 0. 0, w, h. hMask. 0. 0 ) : PlgBltChDC. Q. hMemDC. 0. 0, w. h. hMask. 0. 0); PlgBlUhDC. 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); PatBltthMemDC, 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) SelectObject(hMemDC. hBmp); DrawCube(hDC. x+dx. y+size. size. dx. dy. hMemDC. w, h. hMask); if ( hMask ) DeleteObject(hMask):
630
Глава 11. Нетривиальное использование растров
631
Прозрачные растры
На рис. 11.5 изображен результат вывода двух кубов. Первый куб рисуется с деревянной текстурой без маски, в результате чего изображение получается однородным. На гранях второго куба выводится растр, сгенерированный программой для построения множества Мандельброта, с маской в виде прямоугольника с закругленными углами.
StretchBlUhdcDest, x, y. w. h. hdcSrc. nXSrc. nYSrc. nWidth. nHeight. SRCINVERT): StretchTile(hdcDest. x. y. w. h. hbmMask. xMask, yMask, nWidth. nHeight, 0x220326): return StretchBlUhdcDest. x. y. w, h, hdcSrc. nXSrc. nYSrc. nWidth. nHeight, SRCINVERT): else "return StretchBlUhdcDest. x, y. w, h, hdcSrc. nXSrc. nYSrc. nWidth. nHeight, SRCCOPY): map.Setup(nXSrc. nYSrc. nWidth, nHeight): HOC hdcMask - NULL: int maskwidth - 0: int maskheight= 0; if ( hbmMask ) BITMAP bmp; GetObjecUhbmMask. sizeof(bmp). & bmp); maskwidth - bmp.bmWidth: raaskheight - bmp.bmHeight; hdcMask = CreateCompatibleDC(NULL); SelectObjecU hdcMask, hbmMask);
Рис. 11.5. Трехмерный куб, созданный с помощью функции PlgBlt
for (int dy=map.miny; dy<=map.maxy; dy++) for (int dx=map.minx; dx<=map.raaxx; dx++)
,
Эффектно, не правда ли? К сожалению, программа работает только в системах семейства NT (даже не в Windows 98!). Чтобы вы лучше усвоили описанные выше возможности GDI, было бы полезно создать реализацию PlgBlt, работающую на всех платформах Win32. Давайте попробуем это сделать. В листинге 11.5 приведена функция G_PlgBlt, имитирующая PlgBlt. Листинг 11.5. Реализация PlgBlt BOOL G_PlgBlt(HDC hdcDest, const POINT * pPoint. HOC hdcSrc. int nXSrc, int nYSrc. int nWidth, int nHeight. HBITMAP hbmMask, int xMask. int yMask) KReverseAffine map(pPoint): if ( map.SimpleO ) // Отсутствие сдвига и поворота int int int int
x у w h
= =
pPoint[0].x: pPoint[0].y; pPoint[l].x-pPoint[0].x; pPoint[2].y-pPoint[0].y:
if ( hbmMask ) // маска: if (M) the S else D. S * (-M & (S*D))
i
float sx. sy: map.Map(dx. dy. sx. sy): if ( (sx>=nXSrc) && (sx<=(nXSrc+nWidth)) ) if ( (sy>=nYSrc) && (sy<=(nYSrc+nHeight)) ) if ( hbmMask ) {
if ( GetPixeKhdcMask. ((int)sx+xMask) * maskwidth. ((int)sy+yMask) * maskheight) ) SetPixeKhdcDest. dx, dy, GetPixeK hdcSrc. (int)sx. (mt)sy)):
}
else SetPixeKhdcDest. dx. dy. GetPixel(hdcSrc, (int)sx, (int)sy)): if ( hdcMask ) Del eteObjecU hdcMask); return TRUE:
632
Глава 11. Нетривиальное использование растров
Сначала рассмотрим случай без поворотов и сдвигов. При отсутствии сдвигов и поворотов PlgBlt реализуется несколькими вызовами StretchBIt с простыми растровыми операциями. Функция G_P1gBlt создает в стеке экземпляр класса KReverseAffine, выполняющего преобразование. Если преобразования сдвига и поворота не выполняются, метод KReverseAffine: rSimple возвращает TRUE. Функция проверяет, был ли передан при вызове действительный манипулятор маски, и если не был — вызывает StretchBIt с операцией SRCCOPY. Если маска передается, мы должны имитировать ее несколькими вызовами функций. Вспомните, что говорилось выше: если пиксел маски окрашен в белый (1) цвет, пиксел приемника заменяется пикселом источника; в противном случае пиксел приемника остается без изменений. В булевой алгебре эта операция выражается формулой M&S|~S&D, или 0 Л (М&(5 Л 0)). При прямой реализации этой формулы нам потребуется промежуточный растр, поскольку D используется дважды. Но если перейти к формуле S A (-M&(S*D)), S будет использоваться дважды, a D — только один раз. Из формулы видно, как реализовать семантику применения маски. Сначала D преобразуется в S A D операцией SRCINVERT, затем новый приемник D объединяется с ~М операцией AND, после чего выполняется еще одна операция SRCINVERT с S. Для второй операции маска играет роль источника с учетом возможного мозаичного повторения. Для выполнения операции -S&D используется безымянная операция с ROP-кодом 0x220326. Обратите внимание: при выводе растра маски используется функция St retch Tile, обеспечивающая мозаичное повторение маски на приемной поверхности. Если используется преобразование сдвига или поворота, нам придется поработать по-настоящему. Программа вызывает метод KReverseAf f i ne:: Setup для настройки обратного аффинного преобразования из параллелограмма приемной поверхности в прямоугольник поверхности-источника. Этот способ очень часто применяется при обработке поворотов и сдвигов растров. При отображении пикселов источника на приемную поверхность при растяжении возникают пробелы, а сжатие приведет к дублированию вычислений. Следуя в обратном направлении, от приемника к источнику, мы гарантируем, что каждый пиксел приемника будет обработан, и притом ровно один раз; растяжение и сжатие при этом обрабатывается автоматически. Функция Setup также вычисляет ограничивающий прямоугольник для приемного параллелограмма. После подготовки совместимого контекста устройства для маски программа начинает в цикле перебирать все точки в ограничивающем прямоугольнике параллелограмма. При этом программа отображает каждую точку прямоугольника в координатное пространство растра-источника и проверяет ее принадлежность исходному прямоугольнику (поскольку ограничивающий прямоугольник больше параллелограмма). При хранении координат отображенной точки приемника и сравнениях с границами источника используются вещественные числа. Слишком ранний переход к целым числам приведет к ошибкам при выводе некоторых граничных пикселов вследствие погрешностей округления. Если точка принадлежит исходному прямоугольнику, значит, она входит и в параллелограмм, поэтому мы переходим к вычислению значения пиксела. При наличии маски программа читает соответствующий пиксел маски. Если этот пиксел окрашен в белый (1) цвет, пиксел источника выводится на приемной поверхности; в противном случае приемник не изменяется. Тем самым мы успеш-
633
Прозрачные растры
но реализовали семантику применения маски. Если маска не задана, пиксел источника просто копируется в приемник.. Обратите внимание на прибавление xMask и yMask к координатам растра-источника — тем самым мы учитываем относительный сдвиг маски. Мозаичный эффект достигается вычислением остатка от деления координаты на соответствующий размер маски. Вспомогательный код и класс KReverseAffine приведены в листинге 11.6. Листинг 11.6. Вспомогательный код и класс для имитации PlgBlt
// // // //
Параметры dx. dy, dw. dh определяют приемный прямоугольник Параметры sw. sh определяют размеры прямоугольника-источника Параметры sx, sy определяют начальную точку в растре-источнике, который дублируется до размеров sw x sh
BOOL StretchT11e(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 = SelectObjectthMemDC. hSrc): int syO = sy % bmp.bmHeight:
// Смещение текущей плитки // по оси у
for (int y=0: y<sh; у+=(bmp.bmHeight - syO)) {
int height = mintbmp.bmHeight - s y O . sh - у); // // // int sxO = sx % bmp.bmWidth; // for (int x=0; x<sw: x+=(bmp.bmWidth
{
Текущая высота плитки Смещение текущей плитки по оси х
- sxO))
int width = min(bmp.bmWidth - s x O . sw - x):
// Текущая
// ширина плитки StretchBIt(hDC. dx+x*dw/sw. dy+y*dh/sh. dw*width/sw. dh*height/sh. hMemDC. sxO. syO. width, height, гор); sxO = 0; // После первой плитки в ряду перейти к полной ширине
} syO = 0;
// После первого ряда перейти к полной высоте плиток
SelectObject(hMemDC. hOld): DeleteObject(hMemDC): Продолжение
634
Глава 11. Нетривиальное использование растров
Листинг 11.6. Продолжение return TRUE; • }
void minmax(int 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: // PO 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) && (xO==x2): }
635
Прозрачные растры
Кватернарные растровые операции: MaskBIt Вероятно, какому-то высшему существу из Microsoft показалось, что тернарные растровые операции недостаточно сложны, поэтому к ним нужно добавить кватернарные растровые операции, зависящие от четырех переменных. Это выглядит довольно странно, учитывая, что в GDI имя было присвоено лишь одной тернарной растровой операции, зависящей от узора, источника и приемника — PATPAINT (P|-S|D). Мы даже не знаем, как использовать PATPAINT, хотя в предыдущем разделе было продемонстрировано применение некоторых тернарных операций, зависящих от всех трех переменных. Код тернарной растровой операции можно рассматривать как комбинацию кодов двух бинарных растровых операций. Старшая половина кода тернарной растровой операции используется в тех случаях, когда Р = 1; младшая половина используется, когда Р - 0. Для примера рассмотрим растровую операцию тождественной замены с кодом ОхАА; как старшая, так и младшая половина равна ОхА. Следовательно, результат растровой операции вообще не зависит от кисти. Код D равен ОхА, поэтому результат всегда представляет собой D, то есть исходное состояние приемника. Теперь рассмотрим операцию PATINVERT с кодом 0x5А. Старшая половина равна 0x5, а младшая — ОхА. Следовательно, когда Р = 1, используется растровая операция -D, а когда Р = 0 — операция D, что дает P*D. Четвертым фактором в кватернарных растровых операциях является монохромный растр маски. Код кватернарной растровой операции состоит из двух кодов тернарных операций: основной и фоновой. Основная операция используется, если пиксел маски равен 1, а фоновая операция используется для пикселов маски, равных 0. В GDI определен макрос MAKEROP4, объединяющий два 24-разрядных тернарных кода в один 32-разрядный кватернарный код. #define MAKEROP4(fore. back) (DWORD) ((((back)«8) & OxFFOOOOOO) | (fore) Макрос берет 8-разрядный индекс растровой операции, сдвигает его 8 бит влево и объединяет с 24-разрядным кодом основной операции. В результате образуется 32-разрядный код кватернарной операции. Структура кватернарного ROPкода изображена на рис. 11.6.
void SetupCint 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); } }:
Мы только что реализовали свой первый алгоритм с поддержкой поворотов и сдвигов, а также создали замену для мощной функции PI gBl t. Если выполнить эту программу, она построит почти такой же объемный куб, как на рис. 11.5. Правда, работает она примерно в 7 раз медленнее, поскольку в ней используются медленные функции GetPixel/SetPixel. Позднее в этой главе будут описаны приемы прямого доступа к пикселам, заметно повышающие быстродействие программы.
Полный код основной операции 24 бита U-Кодировка формулы основной операции 16 бит-И \% , ,,, , Индекс фоновой операции 4
Индекс основной операции
Ор5
Ор4
ОрЗ
Ор2
Ор1 Not
Parse String
Offset
Полный код кватернарной операции 32
Рис. 11.6. Код кватернарной растровой операции
Маска в кватернарных операциях не является равноправным фактором, поскольку она ограничивается всего двумя значениями: 1 (белый) и 0 (черный). Вы не сможете создать цветную маску, цветные пикселы которой будут объединяться с цветными пикселами кисти, источника и приемника. Кватернарные
636
Глава 11. Нетривиальное использование растров
операции создавались прежде всего как простое и эффективное средство прозрачного вывода растров, a MaskBlt — единственная функция GDI, получающая код кватернарной растровой операции. Понять, как работает функция MaskBlt, не так уж сложно. Первые пять параметров определяют прямоугольник на приемной поверхности. Следующие три параметра определяют прямоугольник на поверхности источника, размеры которого совпадают с размерами приемной поверхности. Это те же восемь параметров, которые используются в BitBlt. Впрочем, парной функции StretchMaskBlt (по аналогии с парой BitBlt/StretchBlt) не существует. Если приложение хочет выполнить масштабирование при вызове MaskBlt, оно должно соответствующим образом настроить логические системы координат. Следующие три параметра, hbmMask, xMask и yMask, определяют растр маски. Маска дублируется по мозаичному принципу по аналогии с маской PlgBlt. Последний параметр MaskBlt определяет кватернарную растровую операцию. Если пиксел маски равен 1, новый пиксел приемной поверхности определяется пикселами кисти, источника и приемника, объединенными основной растровой операцией; в противном случае используется фоновая растровая операция. Найти хороший пример, демонстрирующий применение MaskBlt, непросто, поэтому мы снова воспользуемся примером с выводом значков: void MaskBltDrawIcon(HDC hDC. int x. int y. HICON hlcon) ICONINFO iconinfo: Getlconlnfo(hlcon.
iconinfo);
BITMAP bmp; GetObject(iconinfo.hbmMask. sizeof(bmp).
bmp);
HDC hMemDC = CreateCompatibleOC(NULL); HGDIOBJ hOld = SelectObject(hMemDC, iconinfo.hbmColor): MaskBlt(hDC. x. y. bmp.bmWidth, bmp.bmHeight. hMemOC, 0, 0, iconinfo.hbmMask. 0. 0. MAKEROP4(SRCINVERT. SRCCOPY)); SelectObjectChMemDC. hold): DeleteObject(i coni nfо.hbmMask); DeleteObject(iconinfo.hbmColor): DeleteObject(hMemDC);
} В отличие от предыдущей программы мы обошлись всего одним вызовом функции. По сравнению с функцией MaskBitmapNT, использующей узорную кисть и экзотическую растровую операцию ОхбС01Е8 (S*(P A D)), мы передаем маску непосредственно при вызове MaskBlt без применения узорной кисти. В этом примере используется кватернарная растровая операция MAKEROP4(SRCINVERT. SRCCOPY), которая выполняет логическую операцию XOR для пикселов маски, равных 1, и простое копирование для нулевых пикселов маски. Эти операции соответствуют правилам вывода значков. На практике единичным основным пикселам соответствуют нулевые пикселы цветного растра, поэтому операция XOR не изменяет пиксела приемника.
Прозрачные растры
637
Имитация MaskBlt Функция MaskBlt обеспечивает простую концептуальную модель выполнения различных растровых операций с основными и фоновыми пикселами. К сожалению, приложения, использующие MaskBlt, работают только в операционных системах семейства Windows NT. В этом разделе мы разработаем модель функции MaskBlt для других систем. Моделирование MaskBlt — очень хорошее упражнение, позволяющее лучше разобраться в применении растровых операций. Сразу договоримся, что мы не будем реализовывать растровые операции на уровне пикселов, поскольку для этого нам потребуется запрограммировать все 256 тернарных растровых операций, от которых зависит функция MaskBlt. Вместо этого мы попробуем имитировать MaskBlt при помощи нескольких тернарных операций. Конечно, это приведет к некоторому снижению быстродействия, но потери оправдываются познавательной ценностью такого упражнения. Функция G_MaskBlt (листинг 11.7) полностью реализует все возможности BitBlt. Задача разбивается на несколько подзадач — от простых, использующих не более трех растровых операций, до более общих, требующих временного растра и шести растровых операций. Функция сначала извлекает из кода кватернарной ROP индексы основной и фоновой операции. Как было сказано выше, функция MaskBlt должна использовать основную растровую операцию для единичных пикселов маски (белых) и фоновую операцию для нулевых пикселов (черных). Следовательно, наша реализация должна быть ориентирована на выполнение двух ROP: основной и фоновой. Листинг 11.7. Имитация MaskBlt BOOL TriBitBlUHDC 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 горЗ) {
* HDC hMemDC = CreateCompatibleDC(hdcDest): SelectObjecUhMemDC, hbmMask):
if ( (ropl»16)!=OxAA ) // not D BitBlt(hdcDest. nXDest. nYDest, nWidth, nHeight, hdcSrc. nXSrc. nYSrc. ropl): BitBltChdcDest, nXDest. nYDest. nWidth. nHeight. hMemDC, xMask, yMask. rop2); DeleteObject(hMemDC): if ( (rop3»16)!=OxAA ) // not D return BitBlt(hdcDest. nXDest. nYDest, nWidth, nHeight. hdcSrc. nXSrc. nYSrc. горЗ): else return TRUE; inline bool D independent(DWORD гор)
638
Глава 11. Нетривиальное использование растров
BitBlt(hMemDC, 0. 0. nWidth, nHeight. hdcSrc, nXSrc. nYSrc. back « 16): // hMemDC содержит итоговое // фоновое изображение
} inline boo! S_independent(DWORD гор) . { return ((OxCC & rop)»2)== (0x33 & гор);
BitBlt(hdcDest. 0. 0. nWidth. nHeight. hdcSrc. nXSrc. nYSrc. fore « 16): // Основное изображение
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 DWORD fore
(dwRop » 24) & OxFF: (dwRop » 16) & OxFF:
if ( back==fore ) // основная операция совпадает с фоновой,
// маска hbmMask не нужна return BitBltthdcDest. 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. // ( foreAback. fore*back ) SRCAND. // ( fore'back. 0 ) (back'OxAA) « 16): // { fore, back } if ( ^independent(fore) ) // Основная операция не зависит от D return TriBitBH (hdcDest. nXDest. nYDest, nWidth. nHeight. hdcSrc. nXSrc. nYSrc. hbmMask. xMask. yMask. (fore^back) « 16. // ( fore^back. fore^back ) 0x22 «16. // ( 0. fore'back ) (foreAOxAA) « 16): // { fore, back } // И основная, и фоновая операция зависят от D if ( SJndependent(back) && SJndependent(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 = CreateCompatib1eBitmap(hdcDest. nWidth. nHeight): HOC hMemDC = CreateCompatibleDC(hdcDest): Se 1 ectObj ect(hMemDC. hTemp): BitBltthMemDC. 0. 0. nWidth. nHeight. hdcDest. nXDest. nYDest. SRCCOPY): SelectObjectChMemDC. GetCurrentObjectChdcDest. OBJJRUSH)):
639
Листинг 11.7. Продолжение
return ((ОхАА & гор)»1)=- (0x55 & гор):
) {
Прозрачные растры
Продолжение
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 фон» — именно этого мы и добивались. Рассмотрим пример. Допустим, основная растровая операция имеет формулу DAS, а фоновая — Р. Следовательно, результат, которого мы хотим добиться, — «if (M) D^S else P». Согласно приведенному выше алгоритму, первая растровая операция описывается формулой D*SAP. После применения маски с операцией SRCAND мы переходим к формуле «if (M) D*SAP else 0». Наконец, растр-источник используется повторно с операцией DAP, и результат равен «if (M) D A S else P». Почему мы требуем, чтобы фоновая растровая операция не зависела от приемника? Потому, что первая растровая операция изменяет состояние приемника. Для всех последующих растровых операций, использующих исходное состояние приемника, необходимо создать его копию. Аналогичным образом, если основная растровая операция не зависит от приемника, достаточно использовать в качестве второй операции операцию NOTSRCAND (0x22), а в качестве третьей — (основаА0). Еще один простой случай — когда и основная, и фоновая операции не зависят от источника. В этом случае мы можем интерпретировать маску как источник, сконструировать новую растровую операцию и выполнить BitBlt при выборе маски в совместимом контексте устройства. Индекс ROP вычисляется по формуле (основа&ОхСС)|(фон&ОхЗЗ), где ОхСС обозначает источник, а 0x33 — «NOT источник». Как видите, мы можем разбирать ROP на составляющие и собирать их заново. Предположим, основной операцией является ROP PATCOPY (OxFO), а фоновой — PATINVERT (Ох5А); обе операции не зависят от S. Новая рас-
640
Глава 11. Нетривиальное использование растров
тровая операция вычисляется по формуле (OxFO&OxCC)|(Ox5A&Ox33), то есть ОхСО|Ох12 = OxD2, или P4D&-S). Обратите внимание: в роли S в данном случае выступает растр маски. Данная формула означает, что для S = 1 должна использоваться операция Р, а для 5 = 0 — РА0. Если ни основная, ни фоновая операция не относятся к этим простым случаям, возникают проблемы. Мы знаем, что обе растровые операции (основная и фоновая) зависят от приемника, но не можем добиться нужного эффекта одним вызовом BitBH. После первого вызова BitBlt приемник изменяется, поэтому все последующие ссылки на него будут относиться к измененному, а не к исходному состоянию приемника. Существует единственный выход — создать временный растр, скопировать в него приемную поверхность, а затем построить на ней фоновое изображение. После этого на главной приемной поверхности строится основное изображение, которое объединяется с фоновым изображением с использованием маски. Таким образом, функция MaskBlt моделируется несколькими вызовами BitBlt — от одного до шести.
Прозрачные растры
ние: функция TransparentBl t, в отличие от StretchBl t, не поддерживает зеркального отражения. Если растр был предварительно обработан для применения цветового ключа, использовать функцию TransparentBIt очень легко. Давайте вернемся к выводу значков, но на этот раз — с помощью функции TransparentBIt. Вспомните: значок состоит из монохромной маски и цветного растра. Прозрачные участки цветного растра обычно окрашиваются в черный цвет. Если черный цвет не встречается в изображении, он обычно выбирается для обозначения прозрачности. Согласно общепринятому правилу, цветовой ключ, как правило, определяется цветом первого пиксела растра. Следующая функция определяет цвет первого пиксела растра значка и передает его в качестве цветового ключа при вызове TransparentBIt. Таким образом, значок выводится всего одной функцией блиттинга. void TransparentBltDrawIcorKHDC hDC. int x. int y. HICON hlcon)
{
1
ICONINFO iconinfo: Getlconlnfo(hlcon, & iconinfo): BITMAP bmp: GetObjecUiconinfo.hbmMask. sizeof(bmp), & bmp):
Цветовые ключи: TransparentBIt Обе рассмотренные функции, PlgBIt и MaskBH, используют монохромный растрмаску для управления выводом растра-источника. Главный недостаток решений, основанных на применении масок, заключается в том, что мы должны создать два идеально совпадающих растра: источник и маску. Маска должна точно соответствовать источнику как по размерам, так и по мельчайшим деталям изображения. Построение этих растров требует большого количества монотонной работы. Решение этой проблемы стоит поискать в Голливуде, у специалистов по визуальным эффектам. В кино уже давно используется методика комбинированных съемок с применением так называемого «синего экрана». Сначала съемка производится на фоне равномерно освещенного экрана синего цвета. В процессе монтажа синий фон заменяется другим изображением-«подложкой». Например, актера можно снять в студии подвешенным на нескольких незаметных шнурах, а потом наложить полученное изображение на изображение неба; получится, что человек парит в воздухе. Кстати, синий цвет — не единственный из возможных, хотя при съемках он используется чаще всего. Существует и другой прием, работающий по тому же принципу, — все участки изображения, яркость которых превышает заданный порог (или наоборот, оказывается ниже его), заменяются участками другого изображения. Эти две методики основаны на применении так называемых цветовых ключей. В GDI на платформах Windows 98 и Windows 2000 поддержка цветовых ключей была представлена новой функцией TransparentBIt. При вызове функция TransparentBIt получает 11 параметров. Первые пять параметров определяют прямоугольник приемной поверхности устройства, следующая пятерка — прямоугольник на поверхности источника, а последний параметр — цветовой ключ, заданный в виде RGB-значения. Функция TransparentBIt копирует на приемную поверхность пикселы источника, не совпадающие с цветовым ключом; при необходимости изображение увеличивается или уменьшается. Обратите внима-
641
HDC hMemDC = CreateCompatibleDC(NULL); HGDIOBJ hOld = SelectObject(hMemDC, iconinfo.hbmColor); COLORREF crTrans = GetPixel(hMemDC. 0. 0): TransparentBIt(hDC. x. y. bmp.bmWidth. bmp.bmHeight. hMemDC. 0. 0. bmp.bmWidth, bmp.bmHeight. crTrans):
SelectObjecUhMemDC. hold);
DeleteObject(iconinfо.hbmMask):
DeleteObject(i coni nfо.hbmColor);t DeleteObject(hMemDC):
} Функция TransparentBIt весьма эффектна, особенно учитывая ее «голливудское» происхождение. К сожалению, она поддерживается только в Windows 98, Windows 2000 и последующих системах. В Windows NT 4.0 поддержка TransparentBIt отсутствует. Эта функция экспортируется не из GDI32.DLL, а из MSIM32.DLL, поэтому к вашей программе должна быть подключена дополнительная библиотека MSIMG32.DLL По имеющейся информации, реализация TransparentBIt в Windows 98 приводит к утечке ресурсов, поэтому широко использовать эту функцию не рекомендуется. Короче, у нас достаточно причин для создания собственной реализации, не зависящей от платформы.
Имитация TransparentBIt Одним из важнейших этапов в реализации TransparenBlt является построение растра маски по цветовому ключу. Монохромные DDB-растры обладают очень удобными средствами для создания масок. Вспомните: когда цветное изображение преобразуется в монохромный растр, все пикселы, цвет которых совпадает с фоновым цветом приемного контекста, преобразуются в 1 (белый), а остальным
644
Глава 11. Нетривиальное использование растров
Листинг 11.8. Продолжение SetTextCo1or(hDC. oldFore): SetBkColor(hDC. oldBack):
1. Наложение рисунка на приемную поверхность операцией XOR. 2. Объединение маски с приемной поверхностью операцией AND. 3. Повторное наложение рисунка на приемную поверхность операцией XOR.
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. HOC hdcSrc. int nSxO. int nSyO. int nSw, int nSh) StretchBlt(hdcDest, nDxO. nDyO. nDw. nDh. hdcSrc. nSxO. nSyO. nSw. nSh, SRCINVERT);
645
Прозрачность без маски
// A
ApplyMask(hdcDest. nDxO, nDyO, nDw. nDh. SRCAND); // if trans D S else 0 return StretchBlUhdcDest. nDxO. nDyO. nDw. nDh. hdcSrc. nSxO. nSyO. nSw. nSh. SRCINVERT): // if trans D else S
1 Метод KDDBMask:: Create создает монохромный растр по заданному контексту устройства на основании цветового ключа. Метод вычисляет размеры растра, отображая исходный прямоугольник на систему координат устройства (на случай, если в исходном контексте устройства не используется принятый по умолчанию режим отображения ММ_ТЕХТ). Фактическое преобразование цветного растра в монохромный зависит от фонового цвета исходного контекста устройства. Метод KDDB: :TransBlt использует знакомую последовательность растровых операций SRCINVERT/SRCAND/SRCINVERT для достижения эффекта прозрачности.
Прозрачность без маски Практически во всех методиках прозрачного вывода растров используется монохромный растр, играющий роль маски. В ресурсах курсоров и значков имеется встроенная маска; функциям PlgBlt и MaskBlt растр маски передается в числе параметров. Единственным исключением является функция TransparentBlt, работающая с цветовыми ключами. Впрочем, в нашей имитации мы вернулись к работе с масками. Возникает вопрос: как реализовать прозрачность без применения масок? Например, если маску по каким-либо причинам трудно создать или она поглощает много ресурсов? Существуют ли альтернативные решения? В этом разделе рассматриваются некоторые из этих альтернатив.
Прозрачный вывод с использованием геометрических фигур Базовый алгоритм прозрачного вывода под управлением маски состоит из трех шагов.
После этих трех этапов область, соответствующая черному (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)); SelectObjectChDC. GetStockObject(BLACK_PEN)); Ellipse(hDC. XDest. YDest, XDest + nDestWidth. YDest RestoreDCChDC. -1);
nDestHeight);
return StretchDIBits(hDC. XDest. YDest. nDestWidth. nDestHeight. XSrc. YSrc, nSrcWidth. nSrcHeight, pBits, pBMI, iUsage, SRCINVERT);
} Эта функция сначала выводит источник при помощи функции StretchDIBits с растровой операцией SRCINVERT^ а затем рисует эллипс функцией Ellipse. Повторный вызов StretchDIBits с операцией SRCINVERT гарантирует, что изображение будет выводиться только в областях, находящихся внутри эллипса. При использовании функции OvalStretchDIBits для рисования нетривиального растра возникает неприятное мерцание, поскольку инвертирование пикселов — относительно медленный процесс. Чтобы уменьшить мерцание, можно позаимствовать структуру цветного растра у значков, где прозрачные пикселы обычно окрашиваются в черный цвет. В этом случае, после окраски непрозрачной области приемника в черный цвет, можно воспользоваться растровой операцией SRCPAINT для объединения источника с приемником. Предполагается, что мы можем модифицировать растр-источник таким образом, чтобы его прозрачные пикселы были окрашены в черный цвет; это можно сделать при помощи DDB или DIB-секции, выбранной в совместимом контексте устройства. Функция OvalStretchBlt иллюстрирует эту идею. BOOL OvalStretchBHCHDC hDC. int XDest. int YDest. int nDestWidth, int nDestHeight. HDC hDCSrc, int XSrc. int YSrc. int nSrcWidth. int nSrcHeight) { // Окрасить источник за пределами эллипса в ЧЕРНЫЙ цвет
646
Глава 11. Нетривиальное использование растров
SaveOC(hDCSrc); BeginPath(hDCSrc): Rectangle(hDCSrc. XSrc. YSrc, XSrc + nSrcWidth+1. YSrc + nSrcHeight+1): EllipsethDCSrc. XSrc, YSrc. XSrc + nSrcWidth. YSrc + nSrcHeight): EndPath(hDCSrc): SelectObject(hDCSrc. GetStockObject(BLACK_BRUSH)); SelectObject(hDCSrc. GetStockObject(BLACK_PEN)); FillPath(hDCSrc): RestoreDCChDCSrc. -1); // Нарисовать ЧЕРНЫЙ эллипс на приемной поверхности SaveDC(hDC): SelectObjecUhDC, GetStockObject(BLACK_BRUSH)): SelectObj ect(hDC. GetStockObj ect(BLACK_PEN)): Ellipse(hDC, XDest. YDest. XDest + nDestWidth. YDest + nDestHeight); RestoreDC(hDC. -1): // Объединить источник с приемником return StretchBlt(hDC. XDest. YDest, nDestWidth. nDestHeight. hDCSrc. XSrc. YSrc. nSrcWidth. nSrcHeight. SRCPAINT): Сначала мы при помощи функций для работы с траекториями закрашиваем область за пределами эллипса черной кистью. Пикселы внутри эллипса остаются без изменений. Кстати говоря, если растр выводится несколько раз, эту операцию можно выполнить всего один раз и многократно использовать ее результат. Второй шаг — стирание приемной поверхности — остается прежним. На третьем шаге вместо SRCINVERT используется растровая операция SRCPAINT. В результате мерцание должно уменьшиться, поскольку только пикселы эллипса переходят от исходного цвета к черному, а затем заменяются пикселами источника. Чтобы полностью устранить мерцание, следует выполнить весь вывод на внеэкранном растре, а затем скопировать его на экран.
Прозрачный вывод с использованием отсечения Если маска имеет простую геометрическую форму, прозрачный вывод без мерцания легко реализуется с использованием региона отсечения. К сожалению, программисты часто недооценивают возможности регионов отсечения — в основном из-за того, что в 16-разрядном интерфейсе GDI поддержка регионов оставляла желать лучшего. Следующая функция выводит овальный растр, для чего используется одна простая операция блиттинга с применением региона отсечения. BOOL ClipOvalStretchDIBits(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) RECT rect = { XDest. YDest. XDest + nDestWidth. YDest + nDestHeight }: LPtoDP(hDC. (POINT *) & rect. 2): HRGN hRgn = CreateEllipticRgnIndirect(& rect):
Прозрачность без маски
647
.SaveOC(hDC): SelectC1ipRgn(hDC. hRgn): DeleteObject(hRgn): BOOL rslt = StretchDIBitsChDC. XDest. YDest. nDestWidth. nDestHeight. XSrc. YSrc. nSrcWidth. nSrcHeight. pBits, pBMI. iUsage, SRCCOPY): RestoreDC(hDC. -1): return rslt: } Вероятно, стоит напомнить некоторые факты, относящиеся к работе с регионами и отсечением. Регионы отсечения определяются в координатах устройства, а не в логических координатах. Следовательно, перед тем как создавать регион вызовом CreateElipticRgnlndirect, функция ClipOvalStretchDIBits сначала должна отображать приемный прямоугольник в систему координат устройства приемного контекста.
Предварительная подготовка изображений В некоторых ситуациях прозрачный растр должен отображаться только на поверхностях с однородным цветом фона. Например, растровые метки команд меню обычно отображаются на фоне меню, однородный цвет которого определяется текущей конфигурацией системы. Чтобы обеспечить максимальную эффектна•/' ность при работе с такими изображениями, следует перед выводом подготовить изображение в итоговом виде. В Win32 API появилась чрезвычайно удобная функция, которая помогает в решении этой задачи: HANDLE LoadlmagetHINSTANCE hinst. LPCTSTR IpszName. UINT uType. int cxDesired, int cyDesired.
648
Глава 11. Нетривиальное использование растров
COLOR_WINDOW. Флаг LRJ/GACOLOR требует, чтобы в растрах использовались цвета VGA. За подробностями обращайтесь к документации MSDN. Среди этих флагов наибольший интерес вызывает LR_TRANSPARENT. При установке этого флага Loadlmage заменяет-пикселы, цвет которых совпадает с цветом первого пиксела изображения, цветом COLOR_WINDOW. Следовательно, если растр выводится на фоне COLOR_WINDOW, вывод всего растра простейшей операцией SRCCOPY приведет к тому же эффекту, что и вывод растра с применением маски. Однако эта возможность может использоваться только в том случае, если растр задействует палитру и отображается на фоне системного цвета COLOR_WINDOW. Почему в GDI нет функции, которая позволяла бы назначить произвольный цвет в качестве прозрачного? В следующем фрагменте показано, как при помощи функции Loadlmage загрузить серию изображений и создать простейшую анимацию. Мы используем изображение комара из DirectX SDK. Анимационная последовательность состоит из трех изображений с разными положениями ног и крыльев. При последовательном выводе растров с небольшим смещением возникает иллюзия движения. void TestLoadImage(HDC hDC. HINSTANCE hlnstance) { HBITMAP hBitmap[3]; const nID [] = { IDB_MOSQUIT1. IDB_MOSQUIT2. IDB_MOSQUIT3 };
for (int i=0: i<3: i++) hBitmap[i] = (HBITMAP) Loadlmage(hlnstance, MAKEINTRESOURCE(nID[i]). IMAGE_BITMAP. 0. 0. LR_LOADTRANSPARENT | LR_CREATEDIBSECTION ): BITMAP bmp: GetObject(hBitmap[0]. sizeof(bmp). & bmp): HDC hMemDC = CreateCompatibleDC(hDC); SelectObject(hDC. 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[i«3]): i nt newx = i; int newy = abs(200-i£400): if ( lastx!=-l ) { SetRectRgn(hRgn. newx. newy. newx+bmp.bmWidth. newy + bmp.bmHeight): ExtSelectClipRgn(hDC. hRgn, RGN_DIFF): PatBlt(hDC, lastx. lasty. bmp.bmWidth. bmp.bmHeight. PATCOPY): SelectClipRgnthDC. NUtL):
Альфа-наложение
649
BitBlt(hDC. 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]);
} При создании окна цвет фона по умолчанию равен COLOR_WINDOW; функция Loadlmage заменяет им черный цвет (прозрачный). Следовательно, для вывода изображения достаточно одного вызова BitBlt с растровой операцией SRCCOPY. Для создания анимации мы должны вывести одно изображение, стереть его, перейти в новую позицию и повторить вывод и стирание. В нашем примере предыдущее изображение стирается функцией PatBlt. Обратите внимание: поскольку мы работаем с чистым фоном окна, стирание сводится к простой закраске цветом COLOR_WINDOW. При более сложном фоне нам пришлось бы сохранить участок фона и восстановить его. Чтобы уменьшить мерцание, мы при помощи региона отсечения исключаем участок, на котором выводится новое изображение, из обновляемой области, что позволяет избавиться от повторного изменения пикселов экрана и обеспечивает плавность анимации.
Альфа-наложение В нескольких последних разделах мы подробно рассматривали растровые операции. Если хорошенько подумать, во многих ситуациях поразрядные растровые операции не имеют особого смысла. Собственно, что вы получите при объединении двух пикселов поразрядными операциями 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 Dst.green Dst.blue Dst.alpha
=
Src.red Src.green Src.blue Src.alpha
* * * *
alpha alpha alpha alpha
+ + + +
(1-alpha) (1-alpha) (1-alpha) (1-alpha)
* * * *
Dst.red Dst.green Dst.blue 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. HOC hdcSrc. int nXOriginSrc. int nYOriginSrc. int nWidthSrc. int nHeightSrc, BLENDFUNCTION blendFunction): По своему прототипу функция AlphaBlend напоминает StretchBH. Первые пять параметров определяют приемный контекст устройства и прямоугольник приемной поверхности в логических координатах. Следующие пять параметров определяют контекст устройства источника и прямоугольник на поверхности источника в логических координатах. При этом действуют стандартные ограничения для контекста источника, то есть исходный прямоугольник должен находиться в контексте источника, а в последнем не могут действовать преобразования сдвига и поворота, приводящие GDI в замешательство. Обратите внимание: ограничения на контекст источника не позволяют напрямую использовать DIB с функцией AlphaBlend. Последний параметр, blendFunction, содержит структуру BLENDFUNCTION, передаваемую по значению. Эта структура заменяет код растровой операции, используемый при вызове StretchBlt. Структура BLENDFUNCTION управляет процессом объединения двух растров, источника и приемника. Поле BlendOp определяет операцию наложения источника, однако единственным допустимым значением этого поля является AC_SRC_OVER, при котором растр-источник накладывается на приемник на основании альфа-коэффициентов источника. Поддержка альфа-наложения в OpenGL предусматривает и другие варианты (например, источник с постоянным цветом). Следующее поле, BlendFlags, должно быть равно нулю; его использование зарезервировано на будущее. Последнее поле, Al phaFormat, прини-
651
Альфа-наложение
мает два значения: 0 означает постоянный альфа-коэффициент, a AC_SRC_ALPHA — использование альфа-коэффициентов отдельных пикселов. Если поле AlphaFormat равно 0, для всех пикселов растра-источника используется одинаковый альфа-коэффициент, заданный в поле SourceAlphaConstant. Допустимые значения лежат в интервале 0-255, а не 0—1, как можно было бы предположить. В данном случае 0 означает полную прозрачность, а 255 — полную непрозрачность. Альфа-коэффициенты пикселов приемника равны 255SourceConstantAl pha. В этом случае альфа-наложение выполняется по следующим формулам: 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):
Если поле Al phaFormat равно AC_SRC_ALPHA, данные альфа-канала должны входить в пикселы поверхности источника. Другими словами, это должен быть физический контекст устройства в 32-разрядном режиме или совместимый контекст устройства, в котором выбран 32-разрядный DDB-растр или DIB-секция. В любом случае каждый пиксел источника состоит из четырех 8-разрядных каналов: красного, зеленого, синего и альфа-канала. Альфа-канал каждого пиксела используется в сочетании с полем SourceConstantAlpha для объединения источника с приемником. Вычисления производятся по следующим формулам: Tmp.red Tmp.green Tmp.blue Tmp.alpha
= = =
Round((Src.red Round((Src.green Round((Src.blue Round((Src.alpha
beta
= 255 - Tmp.alpha;
Dst,red Dst.green Dst.blue Dst.alpha
= = = -
Tmp.red Tmp.green Tmp.blue Tmp.alpha
+ + + +
* * * *
SourceConstantAlpha)/255); SourceConstantAlpha)/255); SourceConstantAlpha)/255); SourceConstantAlpha)/255);
Round((beta Round((beta RoundUbeta Round((beta
* * * *
Dst.red Dst.green Dst.blue Dst.alpha
)/255); )/255); )/255); )/255);
Внимательно рассматривая эти формулы, можно заметить, что альфа-коэффициент уровня пикселов Src.alpha применяется только к пикселам приемника, но не к пикселам источника. GDI предполагает, что альфа-коэффициент уже был внесен в данные растра-источника предварительным умножением. Вероятно, это сделано для удобства игрового программирования, где сцены могут быть предварительно сгенерированы во внешней программе или создаваться в результате работы других компонентов. Изображение с предварительным внесением альфа-данных напоминает прозрачный растр с черным фоном (цветной растр в значке); оно тоже ускоряет вывод за счет гибкости. Если значение SourceConstantAlpha равно 255, временную переменную Tmp вычислять не нужно. Если быстродействие критично для вашей программы, возможно, вас обеспокоит необходимость деления на 255 для каждого пиксела. На процессорах Intel полноценное деление занимает десятки тактов и потому считается исключи-
652
Глава 11. Нетривиальное использование растров
тельно медленной операцией. Доверьтесь реализации GDI и современным компиляторам — они достаточно умны, чтобы заменить деление на константу умножением со сдвигом.
653
Альфа-наложение
Результат наложения показан на рис. 11.7.
Пример альфа-наложения с постоянным коэффициентом Проще всего реализовать альфа-наложение с постоянным коэффициентом. Для него даже не требуется, чтобы поверхность источника была 32-разрядной, поскольку альфа-коэффициент передается в структуре BLENDFUNCTION. Рассмотрим простой пример с наложением нескольких прямоугольников со сплошной заливкой: Рис. 11.7. Альфа-наложение цветных прямоугольников с постоянным коэффициентом
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(OxFF. 0, 0). RGB(0. OxFF. 0). RGB(0. 0. OxFF) }: HBRUSH hBrush = CreateSolidBrush(Color[i]); FillRectthDC. & 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): В этом примере источником и приемником является один и тот же контекст устройства. Сначала мы рисуем три однородных прямоугольника красного, зеленого и синего цвета, а затем накладываем их на фон окна функцией AlphaBlend. При каждом вызове используется постоянный альфа-коэффициент 0,5. Сначала фон окна окрашен в сплошной белый цвет RGB(OxFF,OxFF,OxFF). После наложения красного прямоугольника пикселы окрашиваются в цвет RGB(OxFF,Ox80,Ox80). После наложения второго, зеленого прямоугольника пикселы на пересечении красного и зеленого прямоугольников принимают цвет RGB(Ox80,OxBF,Ox40). После наложения третьего, синего прямоугольника пересечение всех трех прямоугольников содержит пикселы с цветом RGB(Ox40,Ox60, 0x90). Возможно, это не совсем то, чего вы ожидали, но эта величина рассчитывается по формулам альфа-наложения: RGB(0x40,0x60.0x90) = RGB(OxFF.OxFF.OxFF) * 0.125 + RGB(OxFF.O.O) * 0.125 + RGB(O.OxFF.O) * 0.25 + RGB(O.O.OxFF) * 0.5
Постепенное проявление и исчезновение растров В разделе «Прозрачные растры» была приведена функция последовательной «проявки» растра с применением растровых операций. Альфа-наложение предоставляет новые средства для постепенного вывода растров. Ниже приведен новый вариант функции, использующий альфа-наложение с постоянным коэффициентом. BOOL AlphaFade(HDC hDCDst. int XDst, int Y D s t . 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 ( ! AlphaBlend(hDCDst. XDst. Y D s t . nDstW. nDstH. hDCSrc. YSrc, YSn*. nSrcW. nSrcH. blend):
return TRUE;
Функция А1 phaFade накладывает растр-источник на приемную поверхность в пять этапов, с альфа-коэффициентами 1/5, 1/4, 1/3, 1/2 и 1/1. После первого вывода часть изображения уже присутствует в приемнике, поэтому накапливаемые коэффициенты будут равны 1/5, 2/5, 3/5, 4/5 и, наконец, 5/5.
Прозрачные окна В Windows 98/2000 появился новый расширенный стиль окна. При установке флага WS_EX_LAYERED весь вывод в окно вместо непосредственного вывода на экран кэшируется в растре, размеры которого совпадают с размерами экрана. Далее содержимое этого растра может быть выведено на экран посредством альфаналожения. Приложение даже может задать цветовой ключ для такого окна. Все пикселы, цвет которых совпадает с цветом ключа, будут прозрачными, то есть
654
Глава 11. Нетривиальное использование растров
среди них будут видны пикселы, находящиеся под окном. Когда на экране появляются ранее закрытые части окна со стилем WS_EX_LAYERED, перерисовка со стороны приложения не нужна — GDI просто заново выводит на экран кэшированное содержимое растра. При правильной реализации этот стиль позволяет создавать новые визуальные эффекты и повышает быстродействие за счет затрат на хранение кэшированных растров. Стиль WS_EX_LAYERED указывается либо при вызове CreateWindowEx, либо позднее при помощи SetWindowLong. После создания окна можно задать постоянный альфа-коэффициент для окна и необязательный цветовой ключ при помощи функции SetLayeredWi ndowAttrl butes:
655
Альфа-наложение
pBMI->bmiHeader.biHeight - 1 - y: • •
зг*
BOOL SetLayeredWindowAttributestHWND hWnd. COLORREF crKey. BYTE bAlpha. DWORD dwFlags); Параметр hWnd содержит манипулятор окна с флагом стиля WS_EX_LAYERED. Параметр dwFlags содержит один или оба флага LWA_COLORKEY и LWA_ALPHA. При использовании флага LWA_COLORKEY параметр сгКеу определяет цветовой ключ прозрачности. Для флага LWA_ALPHA параметр ЬА1 pha определяет постоянный альфакоэффициент источника. Стиль WS_EX_LAYERED может использоваться только для окон верхнего уровня. Следующий фрагмент показывает, как создать окно со стилем WS EX_LAYERED в функции окна: switch ( uMsg) {
case WM_CREATE: m_hWnd = hWnd: SetWindowLong(m_hWnd, GWL_EXSTYLE. GetWindowLong(m_hWnd. GWLJXSTYLE) | WS_EX_LAYERED) ; SetLayeredWindowAttributes(m_hWnd. RGB(0. 0, 1). OxCO, LWA_ALPHA | LWA_COLORKEY ); return 0:
В этом фрагменте функция GetkindowLong возвращает текущие флаги расширенных стилей, которые после объединения с WS_EX_LAYERED записываются на прежнее место. При вызове SetLayeredWi ndowAttri butes устанавливается альфа-коэффициент 0,75 (ОхСО/255) и цветовой ключ RGB (0,0,1) - несколько необычный цвет, очень близкий к черному. Выполнение этого фрагмента заметно влияет на внешний вид окна. Практически весь вывод в окне, включая дочерние окна, становится полупрозрачным, хотя и выполняется гораздо медленнее. Впрочем, меню или диалоговые окна остаются непрозрачными. На рис. 11.8 показан пример окно с DIB-растром, наложенное на исходный текст программы в MSVC IDE. Обратите внимание: рисунок был получен сохранением всего экрана. Если сохранить только содержимое окна, вместо экранного изображения вы получите только содержимое кэшированного растра. Как обычно бывает с новыми технологиями, прозрачные окна выводятся значительно медленнее обычных. Также огорчает и то, что меню выводятся непрозрачными, а окна не перерисовываются так, как положено.
BYTE * D » pBitsDst + GetOffsetCpBMIDst, dx, j + dy); BYTE * S = pBitsSrc + GetOffset(pBMISrc. sx, j + sy); Рис. 11.8. Прозрачное окно
Альфа-канал: класс AirBrush Во всех примерах, приведенных выше, используется постоянный альфа-коэффициент, применяемый к каждому, пикселу растра-источника. Возможности постоянных альфа-коэффициентов ограничены. Например, они даже не справляются с задачей прозрачного вывода растров, которая легко решается при помощи маски. Растр маски можно рассматривать как альфа-канал с кодировкой 1 бит/пиксел, отделенный от основного растра. Давайте рассмотрим некоторое типичные применения альфа-каналов. Хотя функция AlphaBlend позволяет выбирать в совместимом контексте устройства как DDB, так и DIB-секции, при использовании альфа-каналов можнс работать только с DIB-секциями. Дело в том, что в экранных режимах, не использующих 32-разрядную кодировку цвета, 32-разрядный DDB-растр не будет совместим с экранным контекстом устройства. В 32-разрядной DIB-секции каждый пиксел хранится в 4 байтах. Первый три байта обычно содержат данные синего, зеленого и красного каналов, а в последнем байте хранится альфа-канал AlphaBlend — единственная функция, которая читает и записывает данные аль фа-канала, поэтому GDI не оказывает особой помощи в подготовке этих данных При использовании альфа-наложения на уровне отдельных пикселов AlphaBlem предполагает, что пикселы источника были предварительно умножены на аль фа-коэффициент. Следовательно, чтобы воспользоваться альфа-каналом, мы долж ны обладать прямым доступам к пикселам DIB-секции.
656
Глава 11. Нетривиальное использование растров
В современных графических редакторах обычно поддерживаются разные типы кистей (не путать с кистями GDI!), предназначенных для рисования больших точек и толстых линий. Кисть в графическом редакторе определяется своей формой, цветом, ориентацией, жесткостью и другими хитроумными атрибутами. Например, довольно часто встречается круглая цветная кисть с жесткостью в 50 % — характеристикой, определяющей скорость изменения пикселов кисти от однородного цвета в центре до абсолютно прозрачного цвета на периметре. При рисовании точек или линий такой кистью их границы плавно сливаются с фоном без образования четких контуров, как при стандартном рисовании линий средствами GDI. Описанный эффект можно легко воспроизвести при помощи альфа-канала. В листинге 11.9 приведен класс KAirBrush, реализованный на базе DIB-секции. Листинг 11.9. Класс KAirBrush class KAirBrush { HBITMAP HOC HBITMAP i nt int
mJiBrush; mJiMemDC: m_h01d: m_nWi dth: m_nHeight;
AlphaBlendChDC. x-m_nWidth/2, y-m_nHeight/2, mjiWidth. mjiHeight. 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 } }: mJiBrush (void m_hMemDC mJiOld
// Однородный цветной круг на белом фоне { PatBlUmJiMemDC. 0. 0. width, height, WHITENESS); HBRUSH hBrush = CreateSolidBrush(color): SelectObject(m_hMemDC. hBrush): SelectObjecUmJiMemDC. GetStockObject(NULL_PEN)); Ellipse(m_hMemDC, 0, 0. width, height);
DeleteObject(m_hBrush);
SelectObjecUmJiMemDC, GetStockObject(WHITEJRUSH)): DeleteObjecUhBrush):
m_h01d = NULL: mJiMemDC = NULL; mJiBrush = NULL:
mJiBrush = NULL; mJMemDC = NULL; m_h01d = NULL:
•-KAirBrush О { ReleaseO; }
}:
void Create(int width, int height. COLORREF 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 };
= CreateDIBSection(NULL. & Bmi. DIB_RGB_COLORS. **) & pBits, NULL, NULL); = CreateCompatibleDC(NULL); = (HBITMAP) SelectObjecUmJiMemDC. mJiBrush):
mjiWidth = width; mjiHeight = height;
void Release(void) { SelectObject(m_hMemDC. mJiOld): DeleteObject(m_hMemDC);
public: KAirBrushO
657
Альфа-наложение
BYTE * pPixel = pBits; // Вычислить альфа-канал и умножить значения пикселов for (int y=0; y
{
•
// Расстояние от центра, нормализованное в интервале [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[l] pPixel[2] pPixel[3]
= = = =
pPixel[0] * alpha / 255; pPixel[l] * alpha / 255: pPixel[2] * alpha / 255: 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_CR£AT£: m_brush.Create(32. 32. RGB(0. OxFF. 0)); return 0;
case WMJ.BUTTONDOWN:
wParam = MKJ.BUTTON; // Перейти к следующей секции case case WM_MOUSEMOVE: if (wParam & MK_LBUTTON ) { m_brush.Apply(m_hDCBitmap. LOWORD(lParam). HIWORD(lParam)): Refresh(LOWORDdParamO). HIWORD(lParam));
} return 0:
Аналогичная методика применяется при выводе геометрических фигур с размытыми краями или при наложении изображений. Чтобы размыть границы выводимых линий, многоугольников, кругов и т. д., присвойте внутренним пикселам альфа-коэффициент 255, а внешним пикселам — альфа-коэффициент 0. Альфа-коэффициенты пограничных пикселов должны отражать степень их размытия. Например, при рисовании линии под углом 45° некоторые пограничные
659
Альфа-наложение
пикселы будут принадлежать линии лишь наполовину, поэтому их альфа-коэффициенты должны быть равны 127. Иногда размытие требует проведения довольно сложных вычислений. Один простой, хотя и недешевый способ заключается в создании монохромного растра, увеличенного в п раз по сравнению с оригиналом. В этом растре рисуется увеличенный вариант геометрической фигуры. Полученное изображение делится на блоки размером п х п, и сумма пикселов каждого блока преобразуется в данные альфа-канала.
Рис. 11.9. Точки и линии, нарисованные при помощи класса KAirBrush
Имитация альфа-наложения ,;, Альфа-наложение, как и другие приятные возможности GDI, поддерживается * не на всех платформах Win32, что ограничивает возможности его применения. Если вы хотите использовать альфа-наложение в реальной программе, вам придется имитировать его своими силами. Один из вариантов реализации рассматривается ниже. t На этот раз мы не пытаемся имитировать функцию Al phaBI end, а ограничимся реализацией альфа-наложения между двумя 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.biBHCount / 8 ) * x;
// Альфа-наложение между двумя 32-разрядными DIB-растрами BOOL AlphaB1end3232(BITMAPINFO * pBMIDst. BYTE * pBitsDst. int dx. int dy, int w. int h.
Продолжение
660
661
Глава 11. Нетривиальное использование растров
D[2] = ( S[2] * alpha + beta * D[2] + 127 ) / 255; D[3] = ( S[3] * alpha + beta * D[3] + 127 ) / 255; D += 4; S +- 4:
Листинг 11.10. Продолжение BITMAPINFO * pBMISrc. BYTE BLENDFUNCTION blend)
pBitsSrc. int sx, int sy.
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:
' Приемником и источником для функции Al phaBl end3232 являются одинаковые по размеру прямоугольники в 32-разрядном DIB-растре. Таким образом, мы можем вычислить адрес пиксела как в источнике, так и в приемнике, а масштабирование этой функцией не поддерживается. Альфа-наложение делится на три случая- только постоянный альфа-коэффициент, только альфа-канал и одновременное применение постоянного альфа-коэффициента с альфа-каналом. Функция перебирает все пикселы приемного прямоугольника и объединяет их с пикселами источника. Для таких важных операций, как альфа-наложение, следует создать несколько вариантов функции для разных комбинаций поверхности источника и приемника Например, функция Al phaBl end!632 будет работать с 16-разрядным приемником и 32-разрядным источником, а функция Al phaBl end824 будет получать 8-разрядный приемник, использующий палитру и требующий поиска в цветовой таблице, и 24-разрядный источник, не поддерживающий альфа-канала.
for ( i n t j=0: j
BYTE * D = pBitsDst + GetOffsetCpBMIDst, dx, j + dy); BYTE * S - pBitsSrc + GetOffseUpBMISrc. sx. j + sy):
int i : switch ( format ) {
case 0: // Только постоянный альфа-коэффициент for (i=0; i<w: i++) { D[0] = ( S[0] * alpha + beta * D[0] + 127 0[1] = ( S[l] * alpha + beta * D[l] + 127 D[2] = ( S[2] * alpha + beta * D[2] + 127 D[3] - ( S[3] * alpha + beta * 0[3] + 127 D += 4; S += 4; } break;
case 1: // Только альфа-канал for (i=0; i<w: i++) { beta = 255 - S[3]: D[0] - SCO] + ( beta D[0] D[l] = SCI] + ( beta * D[l] D[2] • S[2] + ( beta * D[2] D[3] = S[3] + ( beta * DCS] D +- 4: S += 4; } break;
+ 127 127 127 127
) ) ) )
) ) ) )
/ / / /
255: 255: 255: 255:
/ 255: / 255; / 255: / 255:
case 2: // Постоянный коэффициент вместе с альфа-каналом for (i=0: i<w; i++) { beta = 255 - ( S[3] * alpha + 127 ) / 255; D[0] = ( S[0] D[l] = С S[l]
return TRUE:
alpha alpha
beta beta
0[0] D[l]
127 ) / 255: 127 ) / 255:
Итоги Основной темой этой главы является формирование новых пикселов по нескольким операндам. Мы подробно рассмотрели тернарные и кватернарные растровые операции, различные варианты прозрачного вывода растров, альфа-наложение и отображение растров на параллелограмм. Также были описаны общие принципы работы растровых операций, процесс разбиения и анализа ROP-кодов и построение растровых операций для решения конкретных практических задач. В этой главе нашлось место и для новых экзотических средств GDI для вывода растров (MaskBlt, PlgBlt, TransparentBlt и Al phaBl end) с примерами использования и имитации этих функций на тех платформах Win32, где они не поддерживаПосле описания растровых форматов GDI (глава 10) и функций GDI (глава 11) нам остается лишь узнать, как организовать прямой доступ к массиву пикселов, как реализовать возможности, предоставляемые GDI, а в некоторых случаях - и улучшить реализацию. В следующей главе рассматривается прямой доступ к пикселам и его применение при обработке графических изображении.
Примеры программ К главе 11 прилагается всего одна программа AdvBitmap, демонстрирующая весь изложенный материал (табл. 11.3).
662
Глава 11. Нетривиальное использование растров
Таблица 11.3. Программа главы И Каталог проекта
Описание
Samples\Chapt_ll\Adv_Bitmap
Выэод диаграммы тернарных растровых операций, демонстрация вывода значков с применением растровых операций, спрайтовая анимация, альфа-наложение, применение масок при выводе растров, постепенное проявление растров, P l g B l t и т. д. Соответствующие команды находятся в меню Test, а также появляются после открытия BMP-файлов
Глава 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
Прямой доступ к пикселам
case DIB_2BPP: return ( pPixel[x/4] » Shift2bpp[xl4] ) & 0x03;
Прямой доступ к пикселам Прежде всего нам понадобится несколько общих функций для работы с отдельными пикселами DIB или DIB-секции: При наличии полной структуры BITMAPINFO и указателя на массив пикселов работа с DIB-секцией почти не отличается от работы с DIB. В сущности, нам нужны аналоги функций GetPixel и SetPixel GDI. При работе с аппаратно-независимым растром обращение к отдельным пикселам сжатого изображения — задача не из простых. Если прочитать пиксел из растра, сжатого по алгоритму RLE, еще реально (хотя и очень долго), то записать что-либо в сжатый растр практически невозможно. Ведь если новый пиксел отличается от соседних, возможно, вам придется расширять строку развертки. Поэтому мы предполагаем, что все сжатые растры (как растры со сжатием RLE, так и сжатые изображения в формате JPEG или PNG) заранее распакованы. Также следует учитывать, что GetPixel и SetPixel работают с данными COLORREF, которые могут представлять значения RGB или индексы палитры. Наша базовая функция должна использовать тот же формат пикселов, что и растр, с которым она работает (то есть цветовые индексы для растров с палитрой или 16-, 24- или 32-разрядные значения RGB для растров, не использующих палитру). Поверх этих базовых функций строятся функции, работающие с COLORREF. В листинге 12.1 приведены два новых метода класса KDIB: GetPixel Index и SetPixel Index. Функция GetPixellndex возвращает цветовые данные для пиксела в заданной позиции. Для растра с кодировкой 1 бит/пиксел эти данные будут состоять из 1 бита, для растра с кодировкой 2 бита/пиксел — из 2 битов и т. д. Функция SetPixel Index решает противоположную задачу — она заменяет пиксел в заданной позиции новыми цветовыми данными. Листинг 12.1. Общие функции для обращения к пикселам DIB
const BYTE ShiftlbppC] = { 7. 6, 5. 4. 3. 2. 1. О }: const BYTE Masklbpp [] = { Ox7F. OxBF. OxDF, OxEF, OxF7. OxFB. OxFD. OxFE }: const BYTE Shift2bpp[] = { 6, 4, 2. 0 }: const BYTE Mask2bpp [] = { -OxCO. -0x30, -OxOC. -0x03 }: •const BYTE Shift4bpp[] = { 4, 0 }: const BYTE Mask4bpp [] = { -OxFO, -OxOF }: 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 = m_pOrigin + у * mjiDelta: switch ( mjiImageFormat ) { case DIB_1BPP:
return ( pPixel[x/8] » Shiftlbpp[xX8] ) & 0x01:
665
case DIB_4BPP: return ( pPixel[x/2] » Shift4bpp[xM] ) & OxOF: case DIBJBPP: return pPixel[x]: case DIB_16RGB555: case DIB_16RGB565: return ((WORD *)pPixel)[x]: case DIB_24RGB888: pPixel += x * 3: return (pPixeUO]) | (pPixel[l] « 8) | (pPixel[2] « 16): case DIBJ2RGB888: 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 + у * lyiDelta; switch ( mjiImageFormat )
{
case DIBJBPP: pPixel[x/8] = (BYTE) ( ( pPixel[x/8] & Masklbpp[x«8] ) | ( (index & 1) « Shiftlbpp[x£8] ) ): break; case DIBJBPP: pPixel[x/4] = (BYTE) ( ( pPixel[x/4] & Mask2bpp[x*4] ) ( (index & 3) « Shift2bpp[x3;4] ) ): break: case DIBJBPP: pPixel[x/2] = (BYTE) ( ( pPixel[x/2] & Mask4bpp№] ) ( (index & 15) « Shift4bpp[x^2] ) ): break; case DIBJBPP: pPixel[x] = (BYTE) index; break;
Продолжение
666
Глава 12. Графические алгоритмы и растры Windows
Листинг 12.1. Продолжение
case DIB_16RGB555: case DIB_16RGB565: ((WORD *)pPixel)[x] break:
(WORD) Index;
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 будет выделено ровно три страницы памяти. Смещение последнего пиксела в растре равно 0x2 FFD. При попытке прочитать его как DWORD процессор выйдет на 1 байт за пределы 12-килобайтного блока, что, скорее всего, приведет к возникновению GPF. Метод SetPixel Index имеет практически такую же структуру, как GetPixel Index. Для 1-, 2- и 4-разрядных растров присваивание пикселу сводится к удалению
Аффинные преобразования растров
667
его первоначальных битов при помощи маски и внесению новых данных. Для 24-разрядных растров указатель преобразуется к типу указателя на RGBTRIPLE и данные копируются как структура RGBTRIPLE, чтобы компилятор сгенерировал код для копирования ровно трех байтов. Выполняете ли вы операции с растрами, требующие произвольного доступа к пикселам, — например, повороты, зеркальные отражения или копирование данных между растрами одинакового формата? Функции GetPixel Index и SetPixel Index — это именно то, что вам нужно. Эти функции также очень хорошо работают для растров, не использующих палитры. Если вы захотите окрасить в красный цвет пиксел растра с кодировкой 8 бит/пиксел, вам придется предварительно свериться с цветовой таблицей. Мы вернемся к этой теме позднее.
Аффинные преобразования растров Применение методов GetPixel Index и SetPixel Index, созданных в предыдущем разделе, лучше продемонстрировать на конкретном примере. Давайте попробуем реализовать общий алгоритм аффинных преобразований растров. Вообще говоря, основные принципы решения этой задачи уже встречались нам ранее при имитации функции PlgBlt. В листинге 12.2 приведены две функции: KDIB::PlgBlt и KDIB: rTransformBitmp. Функция 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(pPoi nt): 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->GetPixel!ndex( (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. хЗ, y3:
Map(xm, Map(xm, MapCxm. Map(xm,
0, mjiWidth. 0, mjiWidth.
0. 0. mjiHeight. m_nHeight.
xO, y O ) ; // 0 xl. y l ) : //
1
x2. y2): / / 2 x3, y3);
3
int xmin. xmax; int ymin, ymax;
Аффинные преобразования растров
669
Функция KDIB: :PlgBlt предполагает, что заранее был создан приемный растр нужного размера, формат пикселов которого соответствует формату растра-источника. В результате преобразования растра обычно генерируется новый растр другого размера. При поворотах и сдвигах возникают уголки, которые должны заполняться цветом фона, поскольку они лежат за пределами преобразованного изображения. Функция KDIB: :TransformBitmap отвечает за «подготовку сцены» к вызову KDIB: :PlgBlt. При вызове ей передается матрица преобразования и цвет фона На основании текущего формата DIB-растра функция вычисляет точный размер преобразованного растра и создает DIB-секцию соответствующего размера с текущим форматом растра. Перед передачей функции KDIB: :PlgBlt для непосредственного преобразования созданная DIB-секция закрашивается цветом фона. , е На рис. 12.1 изображена картинка с цветами, повернутая на 15 функцией KDIB::PlgBlt.
minmax(xO. xl. x2. x3. xmin, xmax): minmax(yO, yl. y2. уЗ, ymin, ymax): int destwidth = xmax - xmin: int destheight - ymax - ymin; KBitmapInfo dest; dest.SetFormat(destwidth, destheight. m_pBMI->bnriHeader.biBitCount. m_pBMI->bmiHeader.biCompression);
BYTE * pBits;
HBITMAP hBitmap = CreateDIBSection(NULL, dest.GetBMK). DIB_RGB_COLORS. (void **) & pBits. NULL. NULL); if ( hBitmap==NULL ) return NULL; HOC 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): DeleteObject(hMemDC); KDIB destDIB; destDIB.AttachDIB(dest.GetBMK). pBits. 0 ) ; POINT P[3] - { { xO-xmin, yO-ymin }. { xl-xmin, yl-ymin }. { x2-xmin. y2-ymin } }; destDIB.PlgBltCP. this, 0, 0, mjiWidth, mjiHeight): return hBitmap;
Рис. 12.1. Поворот растров функцией KDIB::PlgBLt
На компьютере с относительно слабым процессором Pentium 200 МГц функция KDIB::PlgBlt рассчитывает поворот изображения 1024 х 768, 24 бит/пиксел за 1,062 секунды; получается 0,7 мегапиксела в секунду. Если для сравнения заменить вызовы GetPixellndex/SetPixelIndex вызовами функций GDI GetPixel/ SetPixel, время обработки увеличивается до 16,9 секунды (0,044 мегапиксела в секунду). Эксперимент наглядно доказывает, что прямой доступ к пикселам работает гораздо быстрее функций GDI GetPixel /SetPixel. Учитывая, что функция KDIB::PlgBlt использует вещественные вычисления, выигрыш по быстродействию оказывается даже больше, чем в 17 раз.
670
Глава 12. Графические алгоритмы и растры Windows
Быстрые специализированные преобразования растров
Быстрые специализированные преобразования растров Когда быстродействие выходит на первый план, общие алгоритмы приходится оптимизировать для конкретных ситуаций. Специализация особенно важна для графических алгоритмов — таких, как преобразование изображений. Если вам захочется писать специализированные функции для разных форматов DIB, можно закодировать операции с пикселами конкретного формата DIB «на месте»; это позволит избавиться от издержек на вызов функции во внутреннем цикле, проверку формата растра, двойное вычисление адресов пикселов и т. д. Оптимизация также возможна в области применения вещественных вычислений для отображения координат приемного растра в координаты источника. Стандартная реализация преобразований вещественных чисел в целые работает очень медленно, поскольку в ней задействован вызов функции. В листинге 12.3 приведена функция преобразования растров, не использующая вещественных вычислений и работающая только с 24-разрядными растрами. Функция PlgBlt24 начинается так же, как и PlgBlt — с подготовки обратного преобразования из приемника в источник. Функция KReverseAffine:: Setup возвращает ограничивающий прямоугольник приемной поверхности, который затем сравнивается с размерами приемного растра, чтобы убедиться в правильности полученных значений. Параметры ограничивающего прямоугольника источника преобразуются к формату с фиксированной точкой умножением на константу FACTOR, равную 65 536. Затем аналогичные операции выполняются с матрицей преобразования. В данном случае используется формат с фиксированной точкой, состоящий из 16-разрядной целой и 16-разрядной дробной частей. Такое представление позволяет работать с большими растрами и обеспечивает достаточную точность.
map.Setup(nXSrc, nYSrc, nWidth. nHeight): // Обеспечить принадлежность границам растра приемника
if ( map.minx < 0 ) if ( map.maxx > mjnWidth )
map.minx = 0: map.maxx = m_nWidth:
if ( map.miny < 0 ) map.miny = 0: if ( map.maxy > mjnHeight ) map.maxy = mjiHeight; // Прямоугольник источника в формате с фиксированной точкой
sminx sminy smaxx smaxy
= =
nXSrc * nYSrc * ( nXSrc ( nYSrc
FACTOR: FACTOR; + nWidth ) * FACTOR; + nHeight ) * FACTOR;
// int int int int int int
Матрица преобразования в формате с фиксированной точкой mil = (int) (map.m_xm.eMll * FACTOR); m!2 = (int) (map.m_xm.eM12 * FACTOR); m21 = (int) (map.m_xm.eM21 * FACTOR): m22 = (int) (map.m_xm.eM22 * FACTOR): mdx = (int) (map.m_xm.eDx * FACTOR): mdy = (int) (map.m_xm.eOy * 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 - SOggin + (sy/FACTOR) * SDelta:
Листинг 12.3. Оптимизированная функция преобразования 24-разрядных DIB-растров BOOL KDIB::P1gBlt24(const POINT * pPoint. KDIB * pSrc. int nXSrc. int nYSrc. int nWidth. int nHeight) { // Множитель для перехода от FLOAT к формату с фиксированной точкой const int FACTOR = 65536: // Сгенерировать обратное преобразование от приемника к источнику KReverseAffine map(pPoint):
int int int int
671
// Скопировать три байта * ((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, наша улучшенная функция PlgBlt24 поворачивает за 172 миллисекунды. Таким образом, в секунду обрабатывается 4,36 мегапиксела — по сравнению с общим решением на базе GetPixellndex/SetPixel Index достигается выигрыш в 520%. А если сравнить с решением на базе GetPixel/SetPixel, функция PlgBlt24 работает в 100 раз быстрее! Более того, возможности оптимизации P1gBlt24 еще не исчерпаны. Например, вместо проверки каждого пиксела на принадлежность ограничивающему прямоугольнику растра-источника можно заранее вычислять точки пересечения приемной строки развертки с ограничивающим прямоугольником источника, что позволит обойтись без проверок на уровне отдельных пикселов.
Преобразования цветов Существует множество графических алгоритмов, в которых каждый пиксел растра должен изменяться по тем или иным правилам. Цветовые преобразования применяются к каждому пикселу независимо от остальных, вне какого-либо глобального контекста. Примерами таких алгоритмов является преобразование цветных растров в оттенки серого, гамма-коррекция, преобразования цветовых пространств, регулировка оттенка, яркости или насыщенности и т. д. Все алгоритмы преобразования цветов построены по одному шаблону. Если у растра имеется цветовая таблица, преобразованиям подвергаются все элементы цветовой таблицы. В противном случае мы перебираем все пикселы растра и применяем преобразование к каждому пикселу в отдельности. В простейшем варианте используется общий алгоритм, которому среди параметров передается указатель на функцию. Каждое преобразование цвета описывается простой статической функций. Чтобы обработать растр с применением заданного преобразования, достаточно вызвать общий алгоритм и передать специализированную функцию преобразования цвета в качестве параметра. В другом, похожем решении определяется абстрактный класс цветового преобразования с виртуальной функций, выполняющей непосредственную работу. В растровых алгоритмах быстродействие обычно является критическим фактором, поэтому вызов простой или виртуальной функции для каждого пиксела большого растра оказывается неприемлемым. Конечно, мы не хотим заново повторять один и тот же код или строить программу на базе макросов. Остается единственная альтернатива — функции-шаблоны. Конечно, лучше было бы определить эти алгоритмы для класса KDIB, однако в ситуации, когда компилятор C++ не позволяет определять функции-шаблоны в качестве членов класса, придется создать статическую функцию-шаблон, которой передается указатель на экземпляр KDIB. Ситуация усложняется тем, что
673
Преобразования цветов
компилятор C++ не поддерживает и дружественных функций-шаблонов, поэтому некоторые закрытые члены класса придется преобразовать в открытые. В листинге 12.4 приведен алгоритм преобразования цветов DIB-растра, построенный на базе шаблона. Листинг 12.4. Шаблон для преобразования цветов растра template bool ColorTransform(KDIB * dib. Dummy map) // Цветовая таблица OS/2 DIB: 1-. 4-. 8 бит/пиксел, сжатие RLE if ( dib->m_pRGBTRIPLE ) for (int 1=0: im_nClrUsed; i++) map(dib->m_pRGBTRIPLE[i].rgbtRed. dib->m_pRGBTRIPLE[i].rgbtGreen. dib->m_pRGBTRIPLE[i].rgbtBlue): return true;
// Цветовая таблица Windows DIB: 1-. 4if ( dib->m_pRGBQUAD )
8 бит/пиксел, сжатие RLE
for (int 1=0: 1m_nC]rUsed: i+ map(di b->m_pRGBQUAD[i].rgbRed. di b->m_pRGBQUAD[i].rgbGreen, di b->m_pRGBQUAD[i].rgbBlue); return true:
for (int y=0; yiri_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) & Ox7COO ) » 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) & OxFSOO ) » 8; BYTE green = ( (* (WORD *) pBuffer) & Ox07EO ) » 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:
Преобразования цветов
675
В этой части кода обрабатываются все форматы DIB с палитрой, включая сжатые и несжатые форматы RLE. Оставшийся код обрабатывает 16-разрядные растры High Color, 24- и 32-разрядные растры True Color с альфа-каналами. Логический порядок массива пикселов в данном случае роли не играет, поскольку программа просто перебирает пикселы в порядке их расположения в памяти. Для 16-разрядных растров поддерживаются два распространенных формата. Программа должна извлечь каналы RGB, преобразовать каждый из них в 8-разрядную величину, вызвать функцию преобразования цвета, а затем снова упаковать полученный результат в 16разрядное слово. С 24-разрядными растрами все совсем просто. В 32-разрядном растре альфа-канал остается без изменений. Все остальные экзотические форматы DIB (например, внедренные изображения JPEG или PNG, а также растры с нестандартными битовыми полями) не поддерживаются текущей реализацией функции Col orTransf orm.
Преобразование растров в оттенки серого Преобразование цветов из пространства RGB в оттенки серого обычно осуществляется по формуле: Серый = 0,299 х Красный + 0,587 х Зеленый + 0,114 х Синий В компьютерной реализации нам хотелось бы обойтись без вычислений с плавающей точкой. Ниже приведен метод (построенный на базе шаблона Col orTransform) преобразования растра 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 Klmage : public KDIB { public: boo! ToGreyScale(void);
return true:
}
Функция Col orTransf orm получает два параметра: указатель на экземпляр KDIB и указатель на функцию. Конечно, передача указателя на функцию без предварительного задания прототипа выглядит несколько странно, но очевидно, этот способ поддерживается и используется в STL. Первая часть функции обрабатывает цветовые таблицы BMP-файлов в формате OS/2: каждая структура RGBTRIPLE обрабатывается вызовом функции преобразования цветов (параметр, тар). Функция преобразования цветов получает по ссылке три параметра (красный, зеленый и синий канал) и возвращает преобразованный цвет в этих же переменных. Фрагмент для работы с цветовой таблицей растров Windows выглядит аналогично, если не считать того, что на этот раз используется структура RGBQUAD.
bool Klmage::ToGreyScale(void)
{
return ColorTransformtthis, MaptoGray): } Для инкапсуляции алгоритмов обработки растров, разработанных в этой главе, мы создаем класс Klmage, производный от KDIB. Класс Klmage не содержит дополнительных переменных. Из всех методов этого класса выше приведен только метод ToGreyScale. Позднее мы добавим в этот класс другие методы. Метод Klmage::ToGreyScale преобразует текущий цветной DIB-растр в оттенки серого. Для этого он просто вызывает функцию-шаблон Col orTransf orm и передает ей функцию преобразования цвета MaptoGray. Функция MaptoGray, исполь-
676
Глава 12. Графические алгоритмы и растры Windows
зуя целочисленные операции, вычисляет яркость серого цвета и присваивает ее всем трем каналам RGB. В отладочной версии MaptoGray компилируется как отдельная функция, указатель на которую передается Color-Transform. В окончательной версии для достижения оптимального быстродействия все вызовы MaptoGray заменены подставляемым кодом.
Гамма-коррекция Выводимые изображения подвержены фотометрическим искажениям, обусловленным нелинейной реакцией экрана монитора на интенсивность сигнала. Фотометрическая реакция устройства вывода называется гамма-реакцией (gamma response). В разных операционных системах для экрана монитора используются разные гамма-характеристики. Например, изображение, подготовленное на компьютере Macintosh, на PC выглядит слишком темным. С другой стороны, изображение, переданное с сервера PC на Macintosh, может показаться излишне светлым. Для компенсации этих различий приходится корректировать гамма-характеристики устройства. Гамма-коррекция обычно выполняется независимо по всем трем каналам RGB. Три массива по 256 байт вычисляются заранее и передаются программному гамма-преобразователю или видеоадаптеру. Каждый массив относится к одному из каналов. Гамма-коррекция легко реализуется с помощью функции-шаблона ColorTransform.
677
Преобразования цветов
В приведенном фрагменте реализуется функция пользовательского уровня KDIB: :GammaCorrect. Эта функция получает три независимых гамма-коэффициента значения которых обычно лежат в интервале от 0,2 до 5,0. Функция вычисляет три гамма-таблицы (по одной для каждого RGB-канала) по определению гамма-коррекции, после чего вызывает функцию ColorTransform и передает ей в качестве преобразователя функцию MapGamma. Функция MapGamma просто берет из таблицы элемент с заданным индексом. Гамма-коррекция с коэффициентом, равным 1, представляет собой тождественное преобразование цвета. При гамма-коэффициенте меньше 1 изображение «темнеет», а если коэффициент превышает 1 - «светлеет». Если применить гамма-коррекцию 2,2 к изображению, подготовленному на Macintosh, оно будет выглядеть точно так же, как во время создания. На рис. 12.2 показано изображение тигра до и после гамма-коррекции.
BYTE redGammaRamp[256]; BYTE greenGammaRamp[256]: BYTE blueGammaRamp[256];
•
inline void MapGanwaCBYTE & 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 Klmage::GammaCorrect(double redgamma. double greengamma { for (int i=0: i<256; i++) { redGammaRamp[i] = gamma( redgamma. i); greenGammaRamp[i] = gammatgreengamma, i); blueGammaRamp[i] = gamma( bluegamma. i);
return ColorTransform(this. MapGamma);
double bluegamma)
Рис. 12.2. Гамма-коррекция
Механизм поиска в таблице, реализованный функцией MapGamma, может использоваться и для регулировки цвета по другим критериям. На самом деле имеется лишь одно обязательное условие - каналы RGB должны быть независимыми друг от друга. Например, redGammaRamp можно определить таким образом, чтойы интенсивность красного канала уменьшалась на 10 %, а остальные каналы оставались без изменений. , Win32 GDI поддерживает настройку характеристик гамма-коррекции графического устройства, если оборудование и драйвер устройства поддерживают загрузку гамма-таблиц. Задача решается функцией SetDeviceGammaRamp, входящей
678
Глава 12. Графические алгоритмы и растры Windows
в ICM 2.0. В DirectDraw операции с гамма-таблицами также выполняются через интерфейс IDirectDrawGammaControl. Все современное оборудование PC должно поддерживать загрузку гамма-таблиц.
Преобразование пикселов в растрах // Вернуть true, если данные изменились virtual boo! MapRGBCBYTE & red. BYTE & green. BYTE & blue) - 0: // Вернуть true, если данные изменились virtual bool MapIndex(BYTE & index) MapRGB(m_pColor[index*mjiSize+2]. m_pCo1or[i ndex*m_nSi ze+1]. m_pColor[index*m_nSize]):
Преобразование пикселов в растрах Шаблонный алгоритм преобразования цветов, представленный в предыдущем разделе, в действительности перебирает цвета, а не пикселы растра. Впрочем, различия касаются в основном растров с палитрой, для которых алгоритм преобразования цветов просто перебирает все элементы цветовой таблицы (вместо всех пикселов растра). Существует большой класс алгоритмов обработки графических изображений, требующих обслуживания каждого пиксела. Допустим, при построении гистограммы вам придется перебрать все пикселы, чтобы вычислить истинное распределение цветов в растре. При делении цветного растра на несколько каналов желательно, чтобы результаты представляли собой отдельные изображения в оттенках серого, с которыми можно выполнять дальнейшие операции. В алгоритмах цветоделения, используемых графическими редакторами, также организуется перебор всех пикселов. В этом разделе мы построим общий алгоритм преобразования пикселов растра и продемонстрируем его на нескольких практических примерах. На этот раз вместо шаблона будут использоваться указатель на функцию, виртуальная функция и абстрактный базовый класс. Вариант с применением шаблонов из предыдущего раздела обеспечивал очень хорошее быстродействие, за которое приходилось расплачиваться созданием нескольких копий двоичного кода для каждого экземпляра шаблона. Другое ограничение, обусловленное спецификой компилятора, заключается в том, что шаблон воплощается в виде простой функции. Мы не можем использовать класс C++, инкапсулирующий данные вместе с кодом. В частности, для реализации гамма-коррекции требовались глобальные переменные.
Родовой класс преобразований пикселов В листинге 12.5 приведен класс KPixelMapper — абстрактный базовый класс, на основе которого создаются различные алгоритмы преобразования пикселов. Этот класс предназначен для обработки отдельных пикселов или одиночных строк развертки.
679
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: m_nSize = nEntrySize: m nCount - nCount; virtual bool StartLine(int lin«)
{
return true:
virtual virtual virtual virtual virtual virtual virtual virtual
void MaplbpptBYTE * pBuffer. int width); void Map2bpp(BYTE * pBuffer. int width): void Map4bpp(BYTE * pBuffer. int width): void Map8bpp(BYTE * pBuffer. int width); void Map555(BYTE * pBuffer. int width); void Map565(BYTE * pBuffer. int width); void Map24bpp(BYTE * pBuffer. int width); void Map32bpp(BYTE * pBuffer. int width);
Листинг 12.5. Абстрактный базовый класс KPixelMapper
// Абстрактный класс для преобразования // отдельных пикселов и строк развертки class KPixelMapper { BYTE * m_pColor: // Указатель на цветовую таблицу BGR... int m_nSize; // Размер элемента таблицы (3 или 4) int m nCount: // Количество элементов в таблице
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;
Преобразование пикселов в растрах
bool KImage::PixelTransform(KPixelMapper & map) if ( mjpRGBTRIPLE ) map.SetColorTable((BYTE *) mjpRGBTRIPLE. sizeof(RGBTRIPLE). mjiClrUsed): else if ( mjpRGBQUAD ) map.SetColorTable((BYTE *) mjpRGBQUAD. sizeof(RGBQUAD). mjiClrUsed): for (int y=0; y<mjiHeight; y++) unsigned char * pBuffer = (unsigned char *) mjpBits + mjiBPS * y; if ( ! map.StartLine(y) ) break; switch ( mjiImageFormat )
case DIB_1BPP: void KPixelMapper::Map24bpp(BYTE * pBuffer. int width) for (; width>0: width--)
{
MapRGB( pBuffer[2], pBuffer[l]. pBuffer[0] ); pBuffer +- 3:
Все основные методы класса являются виртуальными. Метод MapRGB представляет собой чисто виртуальную функцию, обрабатывающую один пиксел в формате RGB. Классом KPixelMapper он не реализуется, поскольку предполагается, что производный класс предоставит собственную реализацию для выбранного алгоритма. Метод Maplndex работает с пикселами в формате индекса цветовой таблицы. Наша стандартная реализация преобразует цветовой индекс в значение RGB и вызывает MapRGB. Методы Maplbpp, Map2bpp, ..., Map32bpp обеспечивают обработку строк развертки для всех распространенных форматов DIB-растров. Их стандартная реализация перебирает все пикселы в строке развертки и вызывает для каждого пиксела MapRGB или Maplndex. Все эти методы оформлены в виде виртуальных функций, поэтому производный класс может реализовать их по-своему. Например, производный класс может решить, что 24-разрядные изображения для него особенно важны, переопределить Мар24Ьрр и заменить вызовы MapRGB подставляемым кодом для получения максимального быстродействия. Учтите, что для быстродействия критическую роль играет внутренний цикл. В листинге приведены два обработчика строк развертки для 1- и 24-разрядного формата. Обе функции, MapRGB и Maplndex, возвращают логический признак изменения параметров, переданных по ссылке. На основании полученного значения вызывающая сторона может решить, следует ли изменять исходный пиксел. Чтобы воспользоваться классом KPixel Mapper для преобразования DIB-растра, мы создаем новую функцию KImage:: Pixel Transform, которая должна создать экземпляр класса KPixelMapper и передать ему строки развертки. Функция PixelTransform приведена ниже — как видите, она устроена очень просто.
map.Maplbpp(pBuffer. mjiWidth); break;
case DIB_2BPP: map.Map2bpp(pBuffer. m_nWidth):
break; case DIB_4BPP: map.Map4bpp(pBuffer. mjiWidth); break: case DIB_8BPP: map.Map8bpp(pBuffer. m_nWidth): break: case DIBJ.6RGB555: // 15-разрядный формат RGB. 5-5-5 map.Map555(pBuffer. ejiWidtn); break; case DIBJ6RGB565: // 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;
681
682
Глава 12. Графические алгоритмы и растры Windows
Мы наблюдаем четкое разделение обязанностей: класс, производный от KPixel Mapper, занимается преобразованием отдельных пикселов, сам класс KPixel Mapper преобразует строки развертки, а метод KImage: :Pixe1Transform преобразует весь DIB-растр. Если вы захотите поддерживать растровый формат, отличный от формата BMP, вам придется лишь написать свою собственную функцию PixelTransform. Если потребуется реализовать новый графический алгоритм из области преобразования пикселов, достаточно написать класс, производный от KPixel Mapper.
Родовой класс цветоделения Давайте займемся вполне практической задачей — реализацией алгоритма цветоделения, то есть построения в каждом из каналов изображений в оттенках серого по цветному изображению. Общая идея заключается в отображении RGBпиксела на байт, сохраняемый в 8-разрядном растре с цветовой таблицей в оттенках серого. Как обычно, наша реализация должна быть как можно более универсальной, чтобы она могла поддерживать разные типы цветоделения. На этот раз нам понадобится простая функция, управляющая классом цветоделения (Operator в листинге 12.6). Листинг 12.6. Цветоделение с применением класса KPixelMapper
683
Преобразование пикселов в растрах
BITMAPINFO * KChannel::Split(KImage & dib. Operator oper) m_0perator = oper; mjiBPS - (dib.GetWidthO + 3) / 4 * 4; // Размер строки развертки // для 8-разрядного DIB int headsize = sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD): BITMAPINFO * pNewDIB = (BITMAPINFO *) new BYTE [headsize + m_nBPS * 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: с++) pNewDIB->bmiColors[c].rgbRed = c: pNewDIB->bmiColors[c].rgbGreen = c: pNewDIB->bmiColors[c].rgbBlue - c;
typedef BYTE (* Operator)(BYTE red. BYTE green. BYTE blue);
// Родовой класс цветоделения, производный от KPixelMapper // Управляется функций Operator, передаваемой KChannel::Split class KChannel : public KPixelMapper { Operator m_0perator; int mjiBPS; BYTE * m_pBits: BYTE * m_pPixel:
// Вернуть true, если данные изменились virtual bool MapRGB(BYTE & red. BYTE & green. BYTE & blue) { * m_pPixel ++ - m_0perator(red, green, blue): return false: virtual bool StartLinednt line) { m_pPixel - m_pBits + line * mjiBPS; // Первый пиксел // строки развертки return true: public: BITMAPINFO * Split(KImage & dib, Operator oper);
m_pBits - (BYTE*) & pNewDIB->bmiColors[256]; if ( pNewDIB==NULL ) return NULL: dib.PixelTransformC* this); return pNewDIB;
•
BITMAPINFO * KImage::SplitChannel(Operator oper) KChannel channel: return channel.SplitC* this, oper); Класс KChannel является производным от KPixelMapper. Центральное место в нем занимает метод Split, получающий ссылку на DIB и Operator. Метод Split создает 256-цветный DIB-растр, размеры которого совпадают с размерами исходного растра, заполняет палитру оттенков серого и сохраняет адрес массива пикселов в переменной m_pBits, которая будет использоваться при переборе пикселов. Затем вызывается метод KDIB:: Pixel Transform, перебирающий все пикселы растра, что в конечном счете приводит к вызову KChannel: :MapRGB. Наша реализация MapRGB вызывает Operator для отображения RGB-пиксела в байт и сохраняет полученное значение в качестве значения пиксела создаваемого 256-цветного растра. Метод StartLine вызывается в начале каждой строки развертки, что
684
Глава 12. Графические алгоритмы и растры Windows
позволяет программе правильно устанавливать начальную позицию приемной строки. Класс работает с одним каналом. Для обработки нескольких каналов следует либо организовать последовательную обработку, либо воспользоваться новой реализацией, которая создает несколько 8-разрядных DIB-растров и получает новый тип Operator, возвращающий сразу несколько результатов.
CreateNewVi ew(m_DIB.Spli tChannel(TakeL). "Lightness Channel"); CreateNewView(m_DIB.SplitChannel (TakeS), "Saturation Channel"): 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;
Пример выделения каналов Работать с классом KChannel несложно; все, что от вас потребуется, — предоставить нужную функцию. Ниже приведено несколько примеров функций для выполнения распространенных операций в моделях RGB, CMYK и HLS. // Выделение красного канала в RGB inline BYTE TakeRecKBYTE 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
685
Преобразование пикселов в растрах
"Black Channel"): "Cyan Channel"); "Magenta Channel"); "Yellow Channel");
i
Функция Spl i tChannel возвращает указатель на упакованный DIB-растр. Функция KDIBView. :CreateNew использует его для создания нового дочернего окна MDI, в котором этот DIB-растр выводится. При выборе одной из команд выделения каналов в главном окне MDI создается несколько новых окон; в каждом окне выводится новый DIB-растр в оттенках серого. На рис. 12.3 показан результат деления куба RGB на каналы RGB. Обратите внимание: в оттенках серого светлые цвета обладают более высокой интенсивностью, темные цвета — более низкой интенсивностью. Это объясняет и то, почему один из трех ромбов на каждом изображении окрашен в чистый белый цвет — потому что на этой грани исходного цветного куба соответствующий канал обладал максимальной интенсивностью (255).
return 255 - red: // Выделение оттенка в HLS inline BYTE TakeHtBYTE 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");
Рис. 12.3. Выделение цветовых каналов RGB
686
Глава 12. Графические алгоритмы и растры Windows
Гистограмма Чтобы наглядно показать, что класс «Pixel Mapper является родовым классом для преобразования пикселов, мы построим совершенно другой производный класс — генератор гистограмм. // Класс для построения гистограмм RGB. производный от KPixelMapper class KHistogram : public KPixelMapper
int int int i n't
m_FreqRed[256] ; m_FreqGreen[256] ; m_FreqB1ue[256]; m_FreqGray [256] ;
// Вернуть true, если данные изменились virtual bool MapRGB(BYTE & red. BYTE & green, BYTE & blue) {
m_FreqRed[redJ ++; m_FreqGreen[green] ++; m_FreqBlue[blue] ++; m_FreqGray[(red * 77 + green * 150 + blue * 29 + 128 ) / 256] return false;
687
Пространственные фильтры
фильтр сглаживания может генерировать выходной пиксел, вычисляя среднее значение для блока размерами 3 x 3 пиксела, что позволяет отфильтровать случайный шум. Пространственный фильтр получает исходный растр и строит по нему растрприемник. Обычно пространственный фильтр обрабатывает блоки, состоящие из N х N пикселов, где N — нечетное числов. В большинстве распространенных пространственных фильтров N = 3. Центр блока соответствует текущему обрабатываемому пикселу, а остальные пикселы — его соседям. При нечетном N блок симметричен относительно центрального пиксела. При обработке всего изображения пространственный фильтр не может применяться к пикселам, расположенным на расстоянии менее (N - 1)/2 пикселов от края, поскольку в этом случае некоторые пикселы блока N х N выходят за пределы изображения. Проблема решается либо прямым копированием пиксела источника в приемник, либо заполнением пикселов, расположенных близко от края, однородным цветом. Классы и функции, приведенные выше, не подходят для работы с пространственными фильтрами. Необходимо новое решение, которое позволяло бы использовать в качестве входных данных для каждого пиксела блок из N х N пикселов. В листинге 12.7 приведен абстрактный класс KFilter. Листинг 12.7. Класс KFilter для работы с пространственными фильтрами
public: void Samp1e(KImage & dib);
// Абстрактный класс для применения пространственных фильтров // на уровне отдельных пикселов и строк развертки class KFilter int m_nHalf;
void KHistogram::Sample(KImage & dib)
memset(m_FreqRed. memset(m_FreqGreen. memset(m_FreqBlue. memset(m_FreqGray.
0 0. 0. 0.
sizeof(m_FreqRed)): sizeof(m_FreqGreen)); sizeof(m_FreqBlue)): sizeof(m_FreqGray)):
dib.PixelTransform(* this);
virtual BYTE Kernel(BYTE * pPixel. int dx. int dy) = 0: public: int GetHalf(void) const { return mjiHalf; } KFilter(void) { mjiHalf = 1: } virtual - KFilterO { }
Класс KHi stogram подсчитывает относительные частоты составляющих RGB и уровня серого в четырех целочисленных массивах. Реализация MapRGB просто увеличивает соответствующие счетчики. После вызова KHistogram::Sample накопленные данные гистограмм можно вывести в графическом виде — это поможет пользователю понять, какие изменения следует внести в изображение.
ространственные фильтры В приведенном выше алгоритме значение выходного пиксела определяется одним входным пикселом. Существует другой класс графических алгоритмов, в которых, выходной пиксел вычисляется по смежным пикселам. Такие алгоритмы обычно называются пространственными фильтрами (spatial filters). Например,
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 OescribeFilter(HDC hDC. int x. int y) void KFilter::Filter8bpp(BYTE * pDst. BYTE * pSrc. int nWidth, int dy) { memcpy(pDst. pSrc, mjiHalf); pDst += mjnHalf; pSrc += mjiHalf: for (int i=nWidth - 2 * mjiHalf: i>0: i--) * pDst ++ = Kernel(pSrc++. i, dy);
Продолжение •
688
Глава 12. Графические алгоритмы и растры Windo
Листинг 12.7. Продолжение memcpy(pDst. pSrc. m nHalf); void KFilter::Filter24bpp(BYTE * pDst, BYTE * pSrc. int nWidth { memcpy(pDst. pSrc. mjiHalf * 3): pDst +- mjiHalf * 3; pSrc +- mjiHalf * 3; for (int i=nWidth - 2 * mjiHalf; { * pDst ++ = Kernel(pSrc++. 3. * pDst ++ = Kernel(pSrc++. 3. * pDst ++ = Kernel(pSrc++. 3, } memcpy(pDst, pSrc. mjiHalf * 3);
int dy)
i>0; i--) dy); dy); dy);
}
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;
* * * *
}
pDst ++ = Kernel(pSrc++, pDst ++ = Kernel(pSrc++, pDst ++ = Kernel(pSrc++. pDst ++ = * pSrc++:
4. dy); 4, dy); 4. dy): // Копировать альфа-канал
} memcpy(pDst. pSrc. mjnHalf * 4);
Класс KFilter выглядит значительно проще класса KPixelTransform — в основном из-за того, что он работает только с 8-, 24- и 32-разрядными растрами в оттенках серого. Пространственные фильтры выполняют с пикселами математические операции, не имеющие нормальной интерпретации для изображений с палитрой. В принципе можно было организовать поддержку для 15- и 16-разрядных изображений, однако это существенно увеличит объем работы. Класс KFilter работает с одноканальным изображением в оттенках серого. 24и 32-разрядные изображения рассматриваются как совокупность нескольких каналов, обрабатываемых независимо друг от друга. Чисто виртуальная функция KFilter::Kernel определяет принцип работы пространственного фильтра. Для Kfilter::Kernel пиксел представляет собой один байт в интервале 0...255, определяющий интенсивность данного канала. Функция получает указатель на текущий пиксел и смещения следующих пикселов в той же строке и том же столбце. Зная эти три величины, реализация Kernel может обратиться к любому из соседних пикселов при помощи несложных операций сложения и вычитания. Функция возвращает байт, который записывается в выходное изображение вызывающей стороной. Как видите, 15- и 16-разрядные строки развертки плохо вписываются
Пространственные фильтры
689
в эту модель. Переменная m_nHalf содержит значение (N-l)/2, поэтому для фильтров 3 x 3 она обычно равна 1. Методы FilterSbpp, Filter24bpp и Filter32bpp обрабатывают три типа строк развертки, которые мы поддерживаем: 8-, 24-и 32-разрядные. Они получают указатель на строки развертки приемника и источника, ширина строки развертки в пикселах и смещение следующей строки. В каждой строке развертки первые и последние mjiHal f пикселов просто копируются. Остальные пикселы передаются методу Kernel поканально, а полученные результаты записываются в приемную строку развертки. Функции объявлены виртуальными, чтобы их можно было переопределить в производных классах. В класс KImage добавлен новый метод KImage:: Spatial Filter, предназначенный для передачи DIB классу KFilter. Метод создает новый приемный массив пикселов, копирует в него несколько первых и последних необрабатываемых строк развертки и вызывает один из методов фильтрации KFilter для обработки остальных строк. В завершение старый массив пикселов заменяется новым. Приведенную реализацию можно изменить таким образом, чтобы она генерировала новый растр или сохраняла результат в обработанном массиве пикселов источника, чтобы дополнительные затраты памяти не превышали размера нескольких строк развертки. bool KImage::SpatialFilter(KFilter & pFilter) {
BYTE * pDestBits = new BYTE[m_nImageSize]: if ( pDestBits==NULL ) return false: for (int y=0; y<m_nHeight: y++) { BYTE * pBuffer = (BYTE *) mjpBits + mjiBPS * y; BYTE * pDest - (BYTE *) pDestBits + mjiBPS * y; if (
(y>=filter.GetHalf()) && (y<(m_nHeight- filter.GetHalfO)) ) switch ( mjiImageFormat ) { case DIB_8BPP: filter.Filter8bpp(pDest. pBuffer. mjiWidth. mjiBPS): break; case DIB_24RGB888: // 24-разрядный RGB pFilter->Filter24bpp(pDest. pBuffer. mjiWidth, mjiBPS): break: case OIB_32RGBA8888: // 32-разрядный формат RGBA case OIB_32RGB888: // 32-разрядный формат RGB pFilter->Filter32bpp(pDest. pBuffer. mjiWidth. mjiBPS): break; default: delete [] pDestBits: return false:
690
Глава 12. Графические алгоритмы и растры Window
else memcpy(pDest, pBuffer. mjiBPS):
} memcpy(m_pBits, pDestBits. m_nlmageSize); array delete [] pDestBits;
Если флаг проверки границ не установлен, компилятору не нужно генерировать соответствующий код. При этом увеличение объема кода оказывается минимальным, поскольку для каждого фильтра переопределяется всего одна функция. Давайте воспользуемся нашими классами для определения нескольких пространственных фильтров и посмотрим, на какие чудеса они способны.
Фильтры сглаживания и резкости
return true;
. }
Пространственный фильтр 3 x 3 обычно описывается матрицей 3 х 3 и весом. Числа матрицы 3 x 3 умножаются на цветовые значения соответствующих пикселов блока, а сумма делится на общий вес. Результат может выйти за границы интервала 0...255, используемого для хранения интенсивности цветового канала, поэтому результат иногда приходится усекать. Некоторые фильтры перед усечением прибавляют к результату константу. Ниже приведет шаблон для класса, поддерживающего пространственные фильтры 3 х 3 с дополнительным весом и прибавлением константы: template class K33Filter : public KFilter { virtual BYTE Kernel (BYTE * P. int dx. int dy) int r - ( P[-dy-dx] P[ -dx] PC dy-dx] / weight
691
Пространственные фильтры
* kOO + P[-dy] * kOl + P[-dy+dx] * k02 * klO + P[0] * kll + P[ +dx] * k!2 + * k20 + P[dy] * k21 + P[ dy+dx] * k22 ) + add;
На рис. 12.4 после первого исходного рисунка показаны результаты применения трех пространственных фильтров: сглаживания, сглаживания по Гауссу и резкости (для наглядности масштаб равен 3:1). Сглаживание |1 1 1
1 1 / 9
Гауссово сглаживание
Заострение
| 8 1 0| * | Л Ь 1|
| в -1 8| «1-1 9 - 1 1
/ 8
/ 5
0
1
0|
8 -1
if ( checkbound ) if С r < 0 ) return 0; else if ( r > 255 ) return 255; return r:
Класс K33Filter имеет 12 параметров. Первые девять параметров определяют матрицу коэффициентов 3 х 3, за которой следует вес, прибавляемая константа и логическое значение, управляющее проверкой границ. Вообще говоря, реализацию можно было построить на вещественных вычислениях — код остается прежним, изменится только тип данных. Однако в этом случае нам придется выполнять девять умножений с плавающей точкой и преобразовывать вещественное число в целое. Шаблон КЗЗПНег существенно улучшает быстродействие пространственного фильтра за счет применения только целых параметров. Мы работаем лишь с целыми числами, и каждый фильтр получает собственный набор параметров шаблона. С точки зрения компилятора девять умножений и одно деление представляют собой легко оптимизируемые операции с константами.
Рис, 12.4. Фильтры сглаживания и резкости
Три показанных на рисунке фильтра определяются следующим образом: TCHAR szSmooth[] = _T("Smooth"); TCHAR szGuasianSmooth[] = _Т("Quasian Smooth"); TCHAR szSharpening[] = _T("Sharpening"): K33Filter< 1, 1. 1. 1. l. 1. 1. 1, 1. 9. 0. false. szSmooth > fi1ter33_smooth: K33Filter< 0. 1. 0. 1, 4. 1. 0. 1. 0. 8. 0. false. szGuasianSmooth > filter33_guasiansmooth; K33Filter< 0. -1. 0. -1. 9.~-l. 0. -1. 0, 5. 0, true. szSharpening > filter33_sharpening; Вверху слева изображена исходная картинка. Справа от нее показан результат применения сглаживающего фильтра. Его матрица 3 x 3 состоит из одних единиц, а вес равен 9. Следовательно, этот фильтр присваивает пикселу среднее
692
Глава 12. Графические алгоритмы и растры Windows
значение пикселов в блоке 3 x 3 . Сглаживающий фильтр называется низкочастотным фильтром, поскольку он сохраняет низкочастотные участки и отфильтровывает высокочастотные искажения. В частности, он может использоваться для сглаживания линий, фигур и растров, выводимых средствами GDI. На рисунке видно, как сглаживающий фильтр маскирует зазубренные края исходной картинки. После применения сглаживающего фильтра на границах глифа появляются серые пикселы. Фильтр сглаживания по Гауссу также относится к категории низкочастотных фильтров. Вместо равномерного распределения этот фильтр назначает больший весовой коэффициент центральному пикселу. Фильтры этого типа могут определяться и для большего радиуса; на рисунке показан фильтр 3 x 3 . Фильтр резкости вычитает соседние пикселы из текущего, чтобы подчеркнуть переходы в изображении. Он относится к категории высокочастотных фильтров, которые выделяют высокочастотные компоненты изображения и оставляют низкочастотные участки без изменений. Регулируя весовой коэффициент центрального пиксела, можно менять степень резкости. Для монохромного изображения, показанного на рисунке, результат применения фильтра резкости практически незаметен.
693
Пространственные фильтры
ми 1 и -1 определяют направление рельефного выделения. В нашем примере продемонстрированы два направления. Во втором примере результат умножения делится на 2, поэтому степень рельефности изображения уменьшается. Фильтр Лапласа
Рельеф 90°, 50 % 1 О | В -1
0| 0| В|
/ 2 * 128
Выделение границ и рельеф На рис. 12.5 показаны результаты применения фильтра Лапласа и двух рельефных фильтров. Эти фильтры определяются следующим образом: TCHAR szl_aplasian[] = _T("Laplas1an"): TCHAR szEmbossl35[] = _T("Emboss 135°"): TCHAR szEmboss90[J - _T("Emboss 90° 50Г); K33Fi1ter<-l. -1. -l, -1, 8. -1. -1. -1. -l. 1. 128. true. szLaplasian > filter33_laplasian; K33Filter< 1. 0, 0, 0. 0. 0. 0. 0. -1. 1, 128. true. szEmboss!35 > filter33_emboss!35; K33F11ter< 0. 1. 0. 0. 0. 0. 0, -1. 0. 2. 128, true. szEmboss90 > filter33_emboss90: По виду матрицы фильтр Лапласа похож на высокочастотный фильтр, но он генерирует абсолютно другое изображение. Фильтр Лапласа относится к категории фильтров выделения границ с нулевой суммой коэффициентов матрицы. Фильтр выделения границ заменяет равномерно окрашенные области черным цветом, а области с изменениями — цветом, отличным от черного. В приведенном примере фильтр прибавляет к каждому каналу 128, чтобы отрицательный результат не заменялся нулем. В результате прибавления 128 равномерно окрашенные области становятся серыми. Следующие два фильтра, называемые рельефными фильтрами, преобразуют цветное изображение в оттенки серого со своеобразными объемными эффектами. В одном углу матрицы рельефного фильтра стоит 1, а элемент в противоположном углу равен -1. Применение рельефного фильтра можно рассматривать как вычитание изображения, смещенного на определенную величину от оригинала. Результат увеличивается на 128, чтобы нулевая точка переместилась в середину шкалы оттенков серого. Относительные позиции пикселов со значения-
Рис. 12.5. Выдедение границ и рельефные фильтры
Морфологические фильтры На рис 126 показаны три новых пространственных фильтра: фильтры сжатия и расширения, а также контурный фильтр. Чтобы результат был более наглядным, изображения выводятся в масштабе 2:1. Это так называемые морфологические фильтры. Они отличаются от предыдущих фильтров, основанных на линейной комбинации пикселов. Морфологический фильтр использует матрицу N х N для проверки соседних пикселов. Результат проверки определяет цвет пиксела, находящегося в центре. Фильтр сжатия генерирует черный цвет лишь в том случае, если все пикселы блока окрашены в черный цвет. В противном случае генерируется белый цвет. Таким образом, в результате применения фильтра сжатия белые участки изображения расширяются. Фильтр расширения генерирует белый цвет лишь в том случае, если все пикселы блока окрашены в белый цвет. В противном случае генерируется черный цвет. Таким образом, в результате применения фильтра расширения белые участки изображения сужаются.
694
Глава 12. Графические алгоритмы и растры Windows
Сжатие
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, smallerCm. smaller-Cm, smaller(m. smaller(m, smallertm. smallerCm,
Контур
pPixel[-dy]): pPixel[-dy+dx]): -dx]): pPixel[ pPixel[ pPixel[ dy-dx]); pPixel[dyJ); pPixel[ dy+dx]):
return min(pPixel[0]. m); // / 2 ;
Рис. 12.6. Морфологические фильтры
Контурный фильтр сначала выполняет сжатие, а затем вычитает из полученного изображения оригинал. Для равномерно окрашенных областей контурный фильтр генерирует черный цвет (0), поскольку сжатое изображение совпадает с оригиналом. Новые белые пикселы, возникшие в результате сжатия, остаются белыми. В результате возникает белый контур исходного изображения на черном фоне. На рисунке показано, к каким результатам приводит применение всех трех .фильтров к монохромному изображению текстового символа. Черный цвет является основным, а белый — фоновым, поэтому при сжатии белые фоновые участки увеличиваются, а основные черные — уменьшаются. Расширение приводит к обратным последствиям. В нашем примере линии буквы «т» при сжатии становятся тоньше, а при расширении — толще. Контурный фильтр оставляет белый контур буквы. Эти морфологические фильтры создавались для работы с монохромными изображениями. При работе с цветными изображениями, разделенными на несколько каналов в оттенках серого, расширение имитируется через вычисление минимума, а сжатие — через вычисление максимума. Ниже приведена наша реализация фильтра расширения. Функция KErosion:: Kernel находит минимальное значение по девяти пикселам блока 3 х 3 и возвращает его вызывающей стороне. Для цветных изображений также можно было вычислить минимальное значение по восьми пикселам, окружающим центральный пиксел, и вернуть в качестве результата среднее арифметическое центрального пиксела и минимума. В этом варианте эффект расширения несколько снижается. Чтобы создать фильтр сжатия, достаточно вместо минимума вычислить максимум. // Минимум - расширение темных областей class KErosion : public KFilter
Обработка изображений — весьма интересная тема. Впрочем, книга все же посвящена графическому программированию, поэтому нас в первую очередь интересует прямой доступ к массивам пикселов 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 и проверить в нем бит RC_PALETTE. Если этот бит установлен, графическое устройство работает в режиме с поддержкой палитры. Подробную информацию о текущих параметрах видеоадаптера можно получить при помощи функции EnumDisplaySettings. Если приложению потребуется изменить параметры экрана, вызовите функцию ChangeDisplaySettings. Ниже приведена функция SwitchSbpp из программы Palette этой главы; если текущий видеорежим не поддерживает аппаратную палитру, программа предлагает пользователю переключиться в 256-цветный режим. BOOL SwitchSbpp(void) { HOC hDC - GetDC(NULL):
int hasPalette = (GetDeviceCaps(hDC. RASTERCAPS) & RC_PALETTE): ReleaseDC(NULL, hDC): if ( hasPalette ) // Палитра поддерживается return TRUE: int rslt = MessageBoxCNULL. _TГSwitch to 256 color mode?"), _T("Pa 1 ette"), MB_YESNOCANCEL); if ( rslt==IDCANCEL ) return FALSE: if ( rslt—IDYES ) // Выбрано переключение в 256-цветный режим DEVMODE dm; dm.dmSize
- sizeof(dm): // Важно, предотвращает GPF
Системная палитра
699
dm.dmDriverExtra = 0; EnumDisplaySettingsCNULL. ENUM_CURRENT_SETTINGS. &dm): // Текущие параметры dm.dmBitsPerPel - 8; // Перейти к кодировке 8 бит/пиксел ChangeDisplaySettingst&dm. 0): // ПЕРЕКЛЮЧИТЬ return TRUE; }
Функция SwitchSbpp при помощи GetDeviceCaps проверяет, поддерживает ли текущий первичный экран палитру, и если не поддерживает — выводит запрос на изменение видеорежима. Если пользователь соглашается на изменение параметра, функция EnumDisplaySettings возвращает структуру DEVMODE с текущими параметрами устройства. Присвоив полю dmBitsPerPel структуры DEVMODE значение 8, функция вызывает ChangeDisplaySettings и передает измененную структуру DEVMODE для переключения в 256-цветный режим. При этом всем окнам верхнего уровня посылается сообщение WM_DISPLAYCHANGE.
Получение системной палитры В 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 Ippe): UINT GetSystemPaletteUse(HDC hDC); UINT SetSystemPaletteUse(HDC hDC. UINT uUsage): Структура PALETTEENTRY определяет цвет по его компонентам RGB. Поле реП ags используется при создании логических палитр (см. следующий раздел). Функция GetSystemPaletteEntries возвращает блок элементов текущей системной палитры графического устройства. Первый параметр определяет манипулятор контекста устройства. Следующие два параметра определяют первый и последний копируемый элемент, а последний параметр содержит указатель, по которому записывается массив. Если точное количество элементов в системной палитре неизвестно, его можно получить вызовом GetSystemPaletteEntries (hDC, О, О, NULL). Системная палитра является ресурсом уровня графического устройства, который совместно используется всеми созданными для него контекстами. Для графического видеоадаптера системная палитра всегда одна и та же. Приложе-
700
Глава 13. Палитры
ния могут модифицировать системную палитру по определенным правилам, поэтому ее содержимое, вообще говоря, не является чем-то постоянным и жестко заданным. После изменения системной палитры операционная система отправляет сообщение WM_PALETTECHANGED всем.окнам верхнего уровня, чтобы дать им возможность отреагировать на изменения. При необходимости окна верхнего уровня должны сами отправить сообщения своим дочерним окнам. Чтобы лучше понять динамическую природу системной палитры, мы создадим маленькое временное окно для вывода системной палитры и отслеживания всех изменений. В листинге 13.1 приведен класс KPaletteWnd. Метод CreatePaletteWindow этого класса создает временное окно для вывода всех цветов системной палитры. При выводе используется инициализированный 256-цветный аппаратно-зависимый растр (DDB). Предполагается, что видеоадаптер использует для представления DDB-растра одну цветовую плоскость с кодировкой 8 бит/пиксел. Данные инициализации DDB состоят из однородных цветных блоков 16 х 16 с цветами в интервале от 0 до 255. Поскольку предполагается, что данные соответствуют внутреннему формату DDB, создание и вывод DDB не требуют преобразований цветов. Следовательно, байт DDB со значением 0 будет соответствовать первому элементу системной палитры. Обработчик сообщения WM_PALETTECHANGED просто обновляет изображение в окне. Листинг 13.1. Класс для наглядного представления изменений в системной палитре
HGDIOBJ hOld = SelectObject(hMemDC. hBitmap): StretchBlt(hDC,10,10.256.256.hMemDC.0.0.80.80.SRCCOPY); SelectObject(hMemDC. hOld): DeleteObject(hBitmap): DeleteObject(hMemDC): EndPaintthWnd. & ps):
} return 0: case WM_PALETTECHANGED: {
InvalidateRectChWnd. NULL. TRUE); return 0:
case WM_NCDESTROY: ReleaseDC(m_hWnd. m_hDC): return 0; } return DefW1ndowProc(hWnd. uMsg, wParam. IParam);
public: void CreatePaletteWindow(HINSTANCE hlnst) { if ( ! SwitchSbppO ) return;
class KPaletteWnd : public «Window { HDC mJiDC; TCHAR m_name[MAX_PATH];
mjiEntry: mjnGeneration;
virtual LRESULT WndProc(HWND hWnd, UINT uMsg. WPARAM wParam, LPARAM IParam) { switch ( uMsg ) { case WM_PAINT: { PAINTSTRUCT ps;
HDC hDC = BeginPaintthWnd. & 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 ( Ui*5)==0) || ((j*5)==0) ) data[i][j] = 255;
HBITMAP hBitmap - CreateBitmap(80. 80. 1. 8. data);
// Отмена
CreateEx(0. JC'SysPalette"). _T("System Palette"), WSJMRLAPPEDWINDOW | WS_CLIPCHILDREN, CWJJSEDEFAULT. CWJJSEDEFAULT. 290. 340. NULL. NULL, hlnst); ShowWindow(nShow); UpdateWindowO;
PALETTEENTRY m_Entry[256]:
int int
701
Системная палитра
}:
Код на компакт-диске несколько сложнее того, что приведен в листинге 13.1. При обработке сообщения WM_PALETTECHANGED используется вызов функции GetSystemPaletteEntries, возвращающий все содержимое системной палитры, чтобы при получении сообщения об изменении палитры можно было проанализировать новые цвета палитры и сравнить их с предыдущими цветами палитры. На рис. 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] Original 217 web colors, 7 grayscale colors
т
•mmmmmm
ттшшшжшшшшт^ к mmmmmmmm тшшштшшшяшшшшт mmmmmmmmmmmmt&-mm i
208 entries changed by window(180212] i 15 web colors, 19 grayscale colors
mmmmmmm m\ mmmmmmmmmmmmmmmm mmmmmmmmmmmmmmmm штштжтттт&тттттт mmmmmmmmmmmmmmmm mmmmmmmmmmmmmmmm
mmmmmmmmmmmm mmmmmm mmmi
mmmmmm
703
Системная палитра
функциями API, как GetSysColor и SetSysCol or. Следовательно, если приложение хочет уменьшить количество статических цветов до SYSPAL_NOTSTATIC, оно должно сохранить всех текущие системные цвета, изменить количество статических цветов, заменить системные цвета своими, а потом восстановить исходные системные цвета. Количество статических цветов следует изменять только в крайних случаях. Например, если «медицинское» приложение захочет отобразить 256 оттенков серого цвета рентгеновского снимка в 256-цветном режиме, ему придется использовать режим SYSPALJOSTATIC или SYSPAL_NOSTATIC256. Расположение 20 статических цветов выглядит довольно любопытно. Шестнадцать цветов взяты из 16-цветной палитры VGA; еще четыре определяются текущей цветовой схемой. В табл. 13.1 перечислены статические цвета для двух цветовых схем: традиционной и схемы Spruce («Ель» в русской версии Windows). Таблица 13.1. Статические цвета Индекс
Значение RGB
Название
Применение по умолчанию
0x00
0x00, 0x00, 0x00
Черный
COLOR_WINDOWFRAME, COLOR MENUTEXT, COLOR WINDOWTEXT, COLOR 3DDKSHADOW, COLOR INFOTEXT
0x01
0x80, 0x00, 0x00
Темно-красный
0x02
0x00, 0x80, 0x00
Темно-зеленый
0x03
0x80, 0x80, 0x00
Темно-желтый
0x04
0x00, 0x00, 0x80
Темно-синий
0x05
0x80, 0x80, 0x80
Темно-малиновый
0x06
0x00, 0x80, 0x80
Темно-голубой
0x07
OxCO, OxCO, OxCO
Светло-серый
0x08
OxCO, OxDC, OxCO 0x59, 0x97, 0x64
Денежный зеленый
COLOR_ACTIVECAPTION, COLOR_HIGHLIGHT, COLORJTNSHADOW, COLOR_GRAYTEXT
0x09
OxA6, OxCA, OxFO OxA2, OxC8, OxA9
Небесный
COLOR_MENU, COLOR_ACTIVEBORDER, COLORJNACTIVEBORDER, COLORJTNFACE, COLORJDLIGHT
OxF6
OxFF, OxFb, OxFO OxDO, ОхЕЗ, OxD3
Кремовый
COLOR_SCROLLBAR, COLOR_APPWORKSPACE, COLOR_INACTVECAPTIONTEXT, COLOR BTNHIGHLIGHT Продолжение
mm
Рис. 13.1. Анализ изменений в системной палитре
На рисунке окно с системной палитрой показано в двух состояниях. В первом состоянии выводится системная палитра с 217 web-цветами (один цвет дублируется) и 7 дополнительными оттенками серого. После запуска приложения с красочной заставкой палитра драматически изменяется: 208 цветов системной палитры изменились так, чтобы палитра позволяла как можно лучше отобразить заставку. Палитра часто изменяется и при запуске и закрытии других приложений, даже при переключении между дочерними окнами в приложениях MDI.
Статические цвета Похоже, цвета в начале и в конце палитры никогда не изменяются. В этих двух частях палитры хранятся системные статические цвета, зарезервированные операционной системой для пользовательского интерфейса. Операционная система Windows обычно резервирует 20 статических цветов, хотя их количество можно уменьшить. Функция GetSystemPalettellse возвращает флаг, который обозначает количество статических цветов, используемых системой. Если возвращается значение SYSPALJIOSTATIC, система использует два статических цвета — черный и белый; если возвращаемое значение равно SYSPAL_STATIC, используются 20 статических цветов. В Windows 2000 появился новый флаг SYSPAL_NOSTATIC256, означающий полное отсутствие зарезервированных статических цветов. Функция SetSystemPaletteUse изменяет текущий режим статических цветов при помощи описанных выше флагов. Статические цвета используются такими
704
Глава 13. Палитры
Таблица 13.1. Продолжение Индекс
Значение RGB
OxF7
ОхЗА, ОхбЕ, ОхА5 0x21, 0x3 F, 0x21
OxF8
0x80,0x80,0x80
• OxF9
OxFF, OxOO, 0x00
Название
Применение по умолчанию
COLOR BACKGROUND Темно-серый Красный
OxFA
OxOO, OxFF, 0x00
Зеленый
OxFB
OxFF, OxFF, OxOO
Желтый
OxFC
OxOO, OxOO, OxFF
Синий
OxFD
OxFF, OxOO, OxFF
Малиновый
OxFE
OxOO, OxFF, OxFF
Голубой
OxFF
OxFF, OxFF, OxFF
Белый
COLORJJINDOW, COLOR_CAPTIONTEXT, COLOR_HIGHLIGHTTEXT, COLOR INFOBK
Из 20 статических цветов 8 темных цветов размещаются в первых 8 позициях системной палитры, а 8 светлых цветов — в последних 8 позициях. Эти позиции используются для «чистых» цветов: красного, зеленого, синего, малинового, желтого, их темных вариантов и четырех оттенков серого. При использовании 20 статических цветов эти 16 всегда находятся на одних и тех же местах. Они размещаются в разных концах палитры для того, чтобы придать смысл базовым растровым операциям между ними. Очень важно, чтобы черный цвет занимал позицию 0, а белый — позицию 255, поскольку от этого зависит работа растровых операций с применением маски. Позицию 1 занимает темно-красный цвет (RGB(0x80, OxFF, OxOO)); если инвертировать индекс, он переходит в OxFE, что соответствует голубому цвету (RGBCOxOO, OxFF, OxFF)). Он не совсем совпадает с дополняющим цветом темно-красного в цветовом пространстве RGB (RGB(Ox7F, OxFF, OxFF)), но достаточно близок к нему. При объединении красного и зеленого цвета получается чистый желтый цвет, поскольку OxF9 | OxFA = OxFB. Четыре цвета в средней части палитры могут изменяться в соответствии с выбранной цветовой схемой. В табл. 13.1 приведены их RGB-цвета для двух цветовых схем. Эти цвета широко используются в пользовательском интерфейсе Windows. В последнем столбце таблицы показано, каким системным цветам они соответствуют по умолчанию. В 256-цветном режиме операционная система Windows всегда пытается использовать статические цвета в качестве системных.
Логическая палитра Хотя системная палитра состоит из 256 цветов, если не предпринять особых мер, ваше приложение может работать лишь с 20 статическими цветами. Сис-
Логическая палитра
705
темная палитра является ресурсом уровня системы, а не контекста устройства. В контекстах устройств системной палитре соответствуют логические палитры. Логическая палитра управляет преобразованием цветов, используемых в графических командах GDI, в цветовые индексы кадрового буфера графической поверхности. Логическая палитра является одним из атрибутов контекста устройства. Логические палитры, как и логические кисти, логические перья, логические шрифты и т. д., образуют отдельный класс объектов GDI. Ниже приведены описания структур данных и функций, предназначенных для работы с логическими палитрами. UINT GetPaletteEntries(HPALETE hpal. UINT iStartIndex. UINT nEntries. LPPALETTEENTRY Ippe); HPALETTE CreateHalftonePalette(HDC hDC): HPALETTE SelectPalette(HDC hDC. HPALETTE hpal. BOOL bForceBackground); UINT RealizePalette(HDC hDC): BOOL ResizePalette(HPALETTE hpal. UINT nEntries); BOOL UnrealizeObjectCHGDIOBJ hgdiObj); BOOL ResizePalette(HPALETTE hpal. UINT nEntries); typedef struct tagLOGPALETTE { WORD pal Version: WORD palNumEntries: PALETTEENTRY palPalEntry[l]: } LOGPALETTE: HPALETTE CreatePalette(CONST LOGPALETTE * Iplgpl):
Палитра по умолчанию Для получения логической палитры, связанной с контекстом устройства, следует воспользоваться функцией GetCurrentObject(hDC, OBJ_PAL). Новому контексту устройства по умолчанию назначается стандартная логическая палитра, возвращаемая функцией GetStockObject(DEFAULT_PALETTE). Палитра по умолчанию содержит ровно 20 статических цветов, приведенных в табл. 13.1; это ограничивает количество цветов, доступных приложению. Например, если использовать макрос PALETTEINDEX или PALETTERGB для определения цвета в контексте устройства с палитрой по умолчанию, из всех однородных цветов останутся доступными только 20 статических. Приведенная ниже функция WebColors выводит 216 web-цветов. На рис. 13.2 показан результат применения этой функции для палитры по умолчанию. На верхнем рисунке, в котором цвета задаются макросами RGB, большинство элементов получено смешением (кроме черного, красного, зеленого, синего, желтого, голубого, малинового и белого цветов, присутствующих в системной палитре). На нижнем рисунке, в котором использовались макросы PALETTERGB, для каждого цвета из 20 цветов палитры выбирается наиболее подходящий. В обоих случаях результат далек от ожидаемого.
706
Глава 13. Палитры
•Si1
не
ii
Рис. 13.2. Вывод web-цветов с применением палитры по умолчанию void WebColors(HDC hDC. int x. int y. int crtyp) { for (Int r=0: r<6: r++) for (int g=0; g<6: g++) for (int b=0: b<6; Ы-+) { COLORREF cr; switch ( crtyp ) {
}
Логическая палитра
707
считается фоновой (background); в противном случае, при выполнении ряда других условий, палитра считается основной (foreground). Перед использованием палитру необходимо «реализовать». Реализацией логической палитры называется процесс заполнения системной палитры в соответствии с требованиями приложения и построения таблицы соответствия между индексами логической и системной палитр. При реализации основной палитры нестатические цвета удаляются из системной палитры; цвета, отсутствующие среди статических, включаются в системную палитру вплоть до заполнения всех 256 элементов. Затем строится таблица соответствия цветов логической палитры индексам системной палитры, по которой цвета пикселов будут преобразовываться в индексы цветов кадрового буфера. Фоновой палитре уделяется меньше внимания; из системной палитры ничего не удаляется, а запрашиваемые цвета включаются лишь в неиспользованные позиции системной палитры. Основная палитра предназначена для текущего активного окна, обладающего фокусом ввода, а фоновая палитра обеспечивает сколько-нибудь приемлемый вид остальных окон, находящихся на экране. Код следующего фрагмента создает полутоновую палитру, выбирает ее, реализует и снова выводит диаграмму web-цветов. void TestHalftonePalette(HDC hDC. HINSTANCE hlnstance) { HPALETTE hPal = CreateHalftonePalette(hDC): break; HPALETTE hOld = SelectPalette(hDC. hPal. FALSE): RealizePalette(hOC):
case 0: cr = RGB(r*51. g*51, b*51); break; case 1: cr - PALETTERGB(r*51. g*51. b*51): break; case 2: cr = PALETTEINDEX(r*36+g*6+b); break:
WebColors (hDC. 10. 10. 0): WebColors (hDC. 10. 130. 1):
HBRUSH hBrush = CreateSolidBrush(cr); RECT rect = { r * 110 + g*16+ x. b*16+ y. r * 110 + g*16+15+x. b*16+15+y}; FillRect(hDC. & rect. hBrush); DeleteObject(hBrush);
SelectPa1ette(hDC. hOld. TRUE): DeleteObject(hPal); }
Результат показан на рис. 13.3. На верхнем рисунке, использующем макросы RGB, цвета смешиваются из небольшого количества однородных цветов. GDI попрежнему ограничивается 20 статическими цветами. На нижнем рисунке, использующем макрос PALETTERGB, все 216 цветов выводятся однородными.
Полутоновая палитра Чтобы увеличить количество цветов, можно воспользоваться полутоновой палитрой. Логическая полутоновая палитра создается функцией CreateHalftonePalette. Странно, что полутоновые палитры не входят в число стандартных объектов GDI — в этом случае они могли бы совместно использоваться всеми процессами в системе. Созданная полутоновая палитра подчиняется тем же правилам, что и другие объекты GDI; после завершения работы ее необходимо удалить. Логическая палитра выбирается в контексте устройства функцией Select Palette. Обратите внимание: родовая функция SelectObject не подходит, поскольку при выборе палитры передается дополнительный параметр — признак фоновой палитры. Если последний параметр SelectPalette равен TRUE, палитра
Рис. 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 Значение
Описание
О
Стандартная процедура. Искать цвет RGB в системной палитре. Если цвет отсутствует, включить его в палитру
PC_RESERVED
Зарезервировать в системной палитре одну позицию, которая может использоваться для анимации. Не сопоставлять другие цвета с зарезервированной позицией
PX_EXPLICIT
Не изменять системную палитру. Первые два байта структуры PALETTEENTRY образуют индекс в системной палитре
PC NOCOLLAPSE
Искать соответствие в системной палитре лишь при отсутствии свободных элементов; в противном случае использовать новый элемент
Логическая палитра
709
Как говорилось выше, системная палитра содержит 20 статических цветов, которые не могут заменяться приложением. Следовательно, если приложение захочет реализовать логическую палитру из 256 элементов, вполне возможно, что некоторые цвета не будут включены в палитру. GDI обрабатывает запрос в порядке следования элементов в структуре LOGPALETTE. Важные цвета следует размещать в первых позициях структуры LOGPALETTE. В следующем фрагменте создается 256-цветная палитра оттенков серого цвета без каких-либо специальных требований. Если выбрать и реализовать такую палитру, когда система использует 20 статических цветов, 16 цветов в конце таблицы не удастся реализовать однородными цветами. Если, например, «медицинское» приложение захочет вывести рентгеновский снимок в 256 оттенках серого, оно должно уменьшить количество статических цветов до двух (черный и белый) вызовом SetSystemPaletteUse(hDC, SYSPAL_NOSTATIC). После этого весь пользовательский интерфейс Windows будет выводиться в оттенках серого. Чтобы содержимое экрана нормально воспринималось, приложение должно позаботиться о правильной настройке системных цветов. HPALETTE CreateGrayscalePalette(void)
LOGPALETTE * pLogPal = (LOGPALETTE *) new BYTE[s1zeof(LOGPALETTE) + 255 * sizeof (PALETTEENTRY)]: pLogPal->palVersion = 0x0300: pLogPal->palNumEntries = 256: for (int 1=0: i<256; i++)
PALETTEENTRY entry = { i . i . i . 0 } : pLogPal->palPalEntry[i] = entry; HPALETTE hPal = CreatePalette(pLogPal); delete [] (BYTE *) pLogPal : return hPal : } Логическая палитра с флагом PC_EXPLICIT — случай довольно интересный. Она предназначена не для изменения системной палитры, а для того, чтобы цвета системной палитры могли использоваться в качестве индексов логической палитры. При выборе и реализации логической палитры с флагом PC_EXPLICIT цвета, определяемые макросом PALETTEINDEX, ассоциируются с индексами системной палитры, заданными в структуре LOGPALETTE; даже макрос PALETTERGB работает аналогично PALETTEINDEX. После создания палитры можно увеличить или уменьшить количество цветов в ней при помощи функции ResizePalette. При уменьшении размера палитры удаляемые элементы использоваться не могут, но остальные элементы остаются без изменений. При увеличении размера палитры новые элементы заполняются черным цветом. Для инициализации новых элементов применяется функция SetPaletteEntries.
710
Глава 13. Палитры
Сообщения палитры Когда окно реализует основную логическую палитру, из системной палитры удаляются нестатические цвета, а на их место записываются новые цвета из логической палитры. Если на экране остается окно приложения, использовавшего нестатические цвета старой системной палитры, изображение в нем сильно искажается. Например, красный цвет может превратиться в зеленый, а зеленый становится желтым. Чтобы системная палитра могла нормально использоваться сразу несколькими окнами, Windows рассылает окнам верхнего уровня сообщения, информирующие о важных изменениях в палитре.
WM_QUERYNEWPALETTE Пока окно находится в неактивном состоянии, другие окна могут изменить содержимое системной палитры, что приводит к искажению изображения. Если окно готово к получению фокуса клавиатуры, Windows отправляет ему сообщение WM_QUERYNEWPALETTE, чтобы окно могло восстановить свой нормальный вид. Если окно использует нестандартную палитру, оно должно реализовать ее как основную и перерисовать все окно, чтобы восстановить его в оптимальном виде. Палитра, задействованная приложением, должна быть создана заранее и храниться в переменной класса окна или в глобальной переменной. Следующая функция показывает, как обрабатывается сообщение WM_QUERYNEWPALETTE. LRESULT KWindow::OnQueryNewPalette(void) { if ( m_hPalette==NULL ) return FALSE; HOC HOC = GetDC(m_hWnd); HPALETTE h01d= SelectPa1ette(hDC. mJiPalette. FALSE); BOOL changed - RealizePalette(hDC) !- 0; SelectPalettethDC. hold. FALSE); ReleaseDC(m_hWnd. hDC);
if ( changed ) { InvalidateRect(m_hWnd. NULL. TRUE): // Перерисовать }
}
return changed;
В наш класс окна верхнего уровня добавляется новая переменная m_hPal ette, равная NULL, если только производное окно не захочет использовать палитру. При работе в режимах High Color и True Color, а также в том случае, если вы ограничиваетесь статическими цветами, переменная m_hPal ette остается равной NULL. Получив сообщение WM_QUERYNEWPALETTE, функция окна вызывает Kwindow: :OnQueryNewPalette или переопределенную функцию. Метод OnQueryNewPalette создает новый манипулятор контекста устройства, выбирает палитру в качестве основной и реализует ее. Если реализация палитры прошла успешно (это означает,
Сообщения палитры
711
что устройство поддерживает палитру), клиентская область окна объявляется недействительной, что обеспечивает ее перерисовку правильными цветами, функция возвращает TRUE, если палитра была реализована, и FALSE в противном случае.
WM_PALETTEISCHANGING Непосредственно перед тем, как приложение реализует свою логическую палитру, Windows рассылает окнам верхнего уровня сообщение WM_PALETTEISCHANGING, сообщая тем самым о предстоящих изменениях в системной палитре. Впрочем, это вовсе не означает, что реализация палитры откладывается в ожидании подтверждения. Когда активное окно реализует свою палитру, из-за изменений в системной палитре окна на заднем плане могут сильно исказиться. Предполагается, что сообщение WM_PALETTEISCHANGING позволяет окнам заднего плана подготовиться к изменениям в системной палитре. Например, приложение может просто стереть свое окно одним из статических цветов, чтобы изображение не менялось при модификации палитры, а затем перерисовать его снова с применением фоновой палитры. В одной из статей MSDN сказано, что сообщение WM_PALETEISCHANGING является пережитком устаревшей архитектуры, и его следует просто игнорировать. Эксперименты показали, что в Windows 2000 это сообщение не рассылается. Даже в профессиональных пакетах переключение палитры сопровождается кратковременным искажением цветов.
WM_PALETTECHANGED Изменение системной палитры может сопровождаться полным искажением цветов во всех окнах, кроме активного, поэтому всем перекрывающимся (overlapped) и всплывающим (popup) окнам в системе рассылается сообщение WM_PALETTECHANGED. Окна должны отреагировать на это сообщение и попытаться по мере возможности восстановить свое изображение. Параметр wParam сообщения WM_PALETTECHANGED содержит манипулятор окна, изменившего системную палитру. Окно, обрабатывающее это сообщение, должно проверить этот манипулятор и убедиться в том, что палитра была изменена не им самим, а каким-то другим окном, поскольку в противном случае ничего делать не нужно. Существует два способа восстановить содержимое окна. Первый, более быстрый способ — реализовать свою логическую палитру как фоновую и вызвать функцию UpdateColors GDI, чтобы улучшить изображение на уровне пикселов. BOOL UpdateColors(hDC);
Функция UpdateColors перебирает все пикселы поверхности устройства и отображает их цветовые индексы исходной системной палитры на наиболее Подходящие индексы новой системной палитры. Вероятно, во внутренней реализации UpdateCol ors строит таблицу отображения старой системной палитры на новую, а затем перебирает пикселы и осуществляет замену по таблице.
712
Глава 13. Палитры
Поскольку UpdateColors работает с кадровым буфером устройства, содержащим приближенное представление рисунка, многократное применение UpdateColors приведет к постепенному ухудшению изображения. Например, если исходный рисунок отображался в цвете, то после того, как приложение переключается на палитру оттенков серого, UpdateColors отображает все пикселы в оттенках серого. Но когда другое окно реализует полутоновую палитру, функция UpdateColors не может нормально восстановить цветное изображение по оттенкам серого. Второй способ обработки сообщения WM_PALETTECHANGED заключается в перерисовке окна с реализацией фоновой палитры. Как было сказано выше, фоновая палитра не удаляет из системной палитры ни одного элемента, а лишь пытается использовать свободные элементы и подогнать свои логические цвета под существующий набор. Если новая системная палитра хорошо сбалансирована, можно добиться вполне приличного качества. Ниже приведен пример обработчика сообщения WM_PALETTECHANGED. Мы проверяем, не были ли изменения в палитре внесены текущим окном, для чего сравниваем манипулятор окна с wParam. Если манипуляторы не совпадают и окно использует палитру, эта палитра выбирается и реализуется. Программа подсчитывает, сколько раз была вызвана функция UpdateCol ors. При небольшом значении счетчика вызывается функция UpdateColors, обеспечивающая ускоренное обновление; в противном случае окно перерисовывается заново, чтобы улучшить качество изображения. LRESULT KWindow::OnPaletteChanged(HWND hWnd. WPARAM wParam) {
713
Сообщения палитры
как создать логическую палитру, реализовать ее и использовать для отображения растра и как организовать обработку сообщений палитры с помощью описанных выше функций. В листинге 13.2 приведен полный код класса окна DIB, производного от KWindow. Метод CreateDIBWindow, получающий среди прочих параметров неупакованный DIB-растр, создает временное окно. Параметр option позволяет сравнить работу программы с палитрой и без нее, при обработке сообщений палитры и при блиттинге с масштабированием. Обработчик сообщения WM_CREATE создает полутоновую палитру, если на это указывает значение параметра option. Обработчик WM_PAINT использует палитру для вывода растра. Обработчик WM_PALE1TECHANGED восстанавливает поврежденное изображение, также руководствуясь значением параметра option. Обработчик WMjQUERYNEWPALETTE реализует полутоновую палитру. Созданная палитра уничтожается в обработчике сообщения WMJOESTROY. Листинг 13.2. Вывод растров с учетом палитры typedef епшп
paljro = 0x00. paljialftone = 0x01. paljntmap = 0x02,
// Без палитры // Использовать полутоновую палитру // Использовать палитру DIB/DIB-секции
pal_react = 0x04, pal_stretchHT= 0x08
// Реагировать на сообщение WM_PALETTECHANGED // Использовать режим STRETCH_HALFTONE
if ( ( hWnd != (HWND) wParam ) && mJiPalette ) {
HOC hDC = GetDC(hWnd);
HPALETTE CreateDIBSectionPalette(HDC hDC, HBITMAP hDIBSec);
HPALETTE hOld = SelectPaletteChDC. mJiPalette. FALSE); if ( RealizePalette(hDC) ) if ( mjiUpdateCount >=2 ) { InvalidateRectChWnd. NULL, TRUE); m_nUpdateCount = 0: else UpdateColors(hDC); mjnUpdateCount ++: SelectPalette(hDC. hOld. FALSE); ReleaseDC(hWnd. hDC): return 0;
Тестовая программа Давайте объединим все сказанное в небольшом классе окна, предназначенного для вывода DIB с помощью полутоновой палитры. Класс KDIBWindow показывает,
class KDIBWindow : public KWindow { const BITMAPINFO * m_pBMI: * const BYTE * m_pBits; int m_nOption: virtual LRESULT WndProc(HWND 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 ( (mjiOption & 3)==paljialftone ) mJiPalette = CreateHalftonePalette(hDC): else mJiPalette - NULL: ReleaseDCtmJiWnd. hDC):
}
return 0:
Продолжение ri>
714
Глава 13. Палитры
Листинг 13.2. Продолжение case WM_PAINT: {
715
Сообщения палитры
TCHAR title[32]: wsprintf (title. _T( "DIB Window Ud)"). m_nOption):
PAINTSTRUCT ps:
CreateExCO. JT'DIBWindow"), title.
HOC hDC = BeginPaint(hWnd. & ps); HPALETTE hOld = SelectPalette(hDC. m_hPalette FALSE)' RealizePalette(hOC):
CWJJSEDEFAULT. CW_USEDEFAULT . m_pBMI->bmiHeader.biWidth m_pBMI->bmiHeader.biHe-ight + 48. NULL. NULL, hlnst): ShowWindow(SW_NORMAL); UpdateWindowO:
if ( m_nOption & pal_stretchHT ) { SetStretchBltMode(hDC. STRETCHJALFTONE); else SetStretchBltMode(hDC. STRETCHJJELETESCANS): StretchDIBitsthDC. 10. 10. m_pBMI->bmiHeader.biWidth. m_pBMI->bmiHeader.biHei ght. 0. 0, m_pBMI->bmiHeader.biWidth. m_pBMI->bmi Header.bi Hei ght. m_pBits. m_pBMI. DIB_RGB_COLORS. SRCCOPY);
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN.
28.
На рис. 13.4 показаны два варианта изображения. В левом окне изображение получено без применения полутоновой палитры (option = paljio). Цветные пикселы изображения заменяются 20 статическими цветами, поэтому изображение получается серым и скучным. В правом окне была использована полутоновая палитра (option = pal_halftone); рисунок стал очень красочным, с плавными переходами цветов. Хотя на страницах книги цвета не различаются, о качестве изображения можно судить даже по оттенкам серого.
EndPaintChWnd, & ps)} return 0: case WM_PALETTECHANGED: if ( m_nOption & pal_react ) return OnPaletteChangedthWnd. wParam); break: case WM_QUERYNEWPALETTE:
return OnQueryNewPaletteO; case WMJCDESTROY-: DeleteObject(m_hPalette): m_hPa1ette = NULL; return 0-
} return DefWindowProcChWnd. uMsg, wParam, IParam): public: void CreateDIBWindow(HINSTANCE hlnst. const BITMAPINFO * pBMI const BYTE * pBits. int option) ' if ( pBMI==NULL ) return: m_nOption = option; m_pBMI - pBMI: m_pBits - pBits:
Рис. 13.4. Вывод DIB с полутоновой палитрой и без нее Рисунок 13.5 иллюстрирует последствия обработки сообщений палитры. Левое окно (option = pal_halftone) игнорирует сообщение WM_PALETTECHANGED, упуская шанс восстановить изображение при модификации системной палитры. Правое окно (option = pal_halftone|pal_react) обновляет цвета или перерисовывает изображение с фоновой палитрой. Вероятно, комментарии излишни. На экране очевидны некоторые различия, обусловленные различиями фоновой и основной палитр, хотя на бумаге рисунки снова выглядят почти одинаково. Если у окна верхнего уровня имеются дочерние окна (особенно в приложениях MDI), вы должны правильно организовать пересылку сообщений палитры, поскольку без этого сообщения не дойдут до дочерних окон.
Глава 13. Пали
Палитра и растры
717
Дппаратно-зависимые растры и палитры Самый простой способ преобразования растра формата BMP в DDB-растр основан на применении функции LoadBitmap или Loadlmage. Функция LoadBitmap преобразует BMP-файл, подключенный к EXE/DLL в виде ресурса, в DDB. Функция Loadlmage преобразует в DDB либо растровый ресурс, либо внешний BMP-файл, хотя Loadlmage также позволяет загрузить изображение в DIB-секцию. Среди параметров этих функций не передается ни контекст устройства, ни логическая палитра. При построении DDB функции LoadBitmap и Loadlmage используют только 20 статических цветов. Все цветные пикселы растра заменяются ближайшим подходящим цветом из этого маленького набора. Пример показан на рис. 13.4 слева. Для построения многоцветного DDB-растра необходима логическая палитра, управляющая преобразованием цветов из DIB в DDB. Палитра может быть полутоновой, специализированной или сгенерированной на базе системной палитры. В листинге 13.3 приведена новая функция загрузки растра с поддержкой палитры. Рис. 13.5. Вывод DIB без обработки WM_PALETTECHANGED
Сообщение WM_QUERYNEWPALETTE посылается окну верхнего уровня лишь при получении им фокуса. Главное окно MDI должно пересылать это сообщение активному дочернему окну MDI. Если дочерние окна MDI используют разные палитры, любое дочернее окно при получении фокуса должно иметь возможность реализовать свою палитру в качестве основной. Сообщение WM_PALETTECHANGED тоже рассылается только окнам верхнего уровня. Главное окно MDI должно переслать его всем своим дочерним окнам, чтобы все они имели возможность отреагировать на изменения системной палитры.
Палитра и растры По сравнению с векторной графикой, использующей перья и кисти, растровая графика порождает больше проблем в видеорежимах с палитрой. Например, обычная загрузка растра функцией LoadBitmap уже не подходит, поскольку изображение, которое может содержать тысячи цветов, будет аппроксимироваться несколькими статическими цветами. Если растр содержит цветовую таблицу, для получения оптимального результата ее следует преобразовать в логическую палитру Windows и правильно использовать. При выводе растров High Color и True Color в 256-цветном режиме приемлемый результат достигается только построением оптимальной палитры и полутоновой обработкой изображения в соответствии с содержимым палитры. В этом разделе рассматриваются стандартные проблемы, возникающие при выводе растров в видеорежимах с палитрой.
Листинг 13.3. Загрузка DDB-растра с поддержкой палитры static BYTE * GetDIBPixelArray(BITMAPINFO * pOIB)
{
return (BYTE *) & pDIB->bmiColors[GetDIBColorCount(pDIB->bmiHeader)]:
// Создание логической палитры, содержащей все цвета // текущей системной палитры HPALETTE CreateSystemPalette(void) {
LOGPALETTE * pLogPal = (LOGPALETTE *) new char[sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * 255]:
pLogPa1->palVersion = 0x300; pLogPal->palNumEntries - 256; HOC hDC = GetDC(NULL); GetSystemPaletteEntriesthDC. 0. 256. pLogPal->palPal Entry): ReleaseDC(NULL. 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 = CBITMAPINFO *) LockResource(hGlobal): bNeedFree = false:
}
else HANDLE handle - CreateFile(pBitmapName. GENERIC_READ. FILE_SHARE_READ. NULL. OPENJXISTING, FILE_AnRIBUTE_NORMAL. NULL); if ( handle — INVALID_HANDLE_VALUE ) return NULL; BITMAPFILEHEADER bmFH: DWORD dwRead - 0; ReadFile(handle, & bmFH, sizeof(bmFH). & dwRead. NULL); if ( (bmFH.bfType — Ox4D42) && (bmFH.bfSize<= GetFileSizeChandle, NULL)) )
{
pDIB - (BITMAPINFO *) new BYTE[bmFH.bfSize]: if ( pDIB ) { bNeedFree = true; ReadFile(handle. pDIB. bmFH.bfSize. & dwRead. NULL);
CloseHandle(handle): } return pDIB; // Загрузка ресурса или файла под управлением палитры HBITMAP PaletteLoadBitmapCHINSTANCE hlnst. LPCTSTR pBitmapName HPALETTE hPalette) { bool bDIBNeedFree; BITMAPINFO * pDIB = LoadDIB(hInst. pBitmapName. bDIBNeedFree): int width int height
- pDIB->bmiHeader.biWidth; = 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); StretchDIBits(hMemDC. 0. 0, width, height. 0. 0, width, height. GetDIBPixelArray(pDIB), pDIB. DIB_RGB_COLORS, SRCCOPY); SelectPalettednMemDC. hOld, FALSE): SelectObjecUhMemDC, hOldBmp); DeleteObject(hMemDC); if ( bDIBNeedFree ) delete [] (BYTE *) pDIB; return По сравнению с LoadBitmap функция PaletteLoadBitmap получает дополнительный параметр — манипулятор логической палитры. Логическая палитра выбирается в совместимом контексте устройства перед преобразованием загруженного DIB-растра в DDB, поэтому сгенерированный DDB-растр может использовать все цвета логической палитры. Функция LoadDIB загружает растр из ресурса или внешнего файла в виде упакованного DIB-растра. Вспомогательная функция CreateSystemPalette создает логическую палитру, содержащую все цвета текущей системной палитры. Манипулятор, переданный PaletteLoadBitmap, должен соответствовать логической палитре, используемой при выводе растра. Например, если приложение является игровой программой, работающей с полутоновой палитрой, то растры в игре должны загружаться с полутоновой палитрой. Главное окно программы должно обрабатывать сообщения палитры, чтобы обеспечить выбор полутоновой палитры при выводе растров. DDB-растры широко применяются при выводе графики на панелях инструментов, кнопках, элементах управления, в меню и т. д. Обычно вывод происходит под управлением операционной системы, хотя также возможен вариант с прорисовкой владельцем. Операционная система применяет при выводе DDB палитру по умолчанию, поэтому если приложение хочет использовать более 20 статических цветов, цвета растра должны соответствовать содержимому текущей системной палитры. Другими словами, при каждом изменении системной палитры эти растры приходится восстанавливать заново. Ниже приведена функция, позволяющая вывести панель инструментов более чем с 20 цветами. Она реализуется в классе KToolbarB, производном от класса KToolbar. Функция KToolbar: -.SetBitraap должна вызываться при каждом изменении системной палитры. Она загружает растр с применением текущей системной палитры и использует сообщение TB_REPLACEBITMAP для замены текущего растра панели инструментов. Теперь вы сможете задействовать больше цветов на панелях инструментов в 256-цветном режиме. BOOL KToolbarB::SetBitmap(HINSTANCE hlnstance, int resourcelD) { HPALETTE hPal - CreateSystemPaletteO:
720
Глава 13. Палитры HBITMAP hBmp = PaletteLoadBitmapthlnstance, MAKEINTRESOURCE(resourcelD). hPal); DeleteObject(hPal):
Листинг 13.4. Преобразование цветов DIB в логическую палитру HPALETTE CreateDIBPalette(const BITMAPINFO * pDIB) {
if ( hBmp ) { TBREPLACEBITMAP rp; rp.hlnstOld rp.nlDOld rp.hlnstNew rp.nlDNew rp.nButtons
= m_ResInstance; = m_ResId; = NULL: = (UINT) hBmp; = 40;
SendMessage(m_hWnd. TB_REPLACEBITMAP. 0. (LPARAM) & rp);
721
Палитра и растры
BYTE * pRGB: int nSize; int nColor; if ( pDIB->bmiHeader.MSize==sizeof(BITMAPCOREHEADER) ) // OS/2 { pRGB • (const BYTE *) pDIB + sizeof(BITMAPCOREHEADER); nSize - sizeof(RGBTRIPLE): nColor - 1 « ((BITMAPCOREHEADER *) pDIB)->bcBitCount: else
if ( m_Res!nstance==NULL ) DeleteObject( (HBITMAP) m_Res!d):
nColor = 0;
m_Res Instance m_Res!d
if ( pDIB->bmiHeader.biBitCount<=8 ) nColor = 1 « pDIB->bmiHeader.biBitCount;
NULL: (UINT) hBmp:
if ( pDIB->bmiHeader.biClrUsed ) nColor - pDIB->bmiHeader.biClrUsed;
return TRUE:
if ( pDIB->bmiHeader.biClrIinportant ) nColor = pDIB->bmiHeader.biClrlmportant:
else return FALSE:
pRGB - (BYTE *) & pDIB->bmiColors; nSize - sizeof(RGBQUAD):
\ппаратно-независимые растры и палитры . В отличие от аппаратно-зависимых растров, каждый аппаратно-независимый растр (DIB) содержит полную цветовую информацию, что позволяет вывести его на любом устройства. В режимах High Color и True Color каждый пиксел содержит полные данные цвета; в других режимах индексы отображаются на значения RGB по цветовой таблице. Главная проблема при выводе DIB в системах с палитрой заключается в выборе палитры, используемой при выводе растра. Вывод DIB с палитрой по умолчанию позволяет использовать только 20 статических цветов. Полутоновая палитра хорошо подходит для вывода деловой графики с насыщенными и равномерно распределенными цветами. Для растров с неравномерным распределением цветов в пространстве RGB специализированная палитра подходит лучше, чем палитры общего назначения (такие, как полутоновая палитра). Если количество цветов в растре не превышает 256, цветовая таблица растра легко преобразуется в логическую палитру. Для растров High Color или True Color Windows позволяет задать цветовую таблицу для вывода на устройствах с палитрой (хотя вряд ли удастся вспомнить хоть одно приложение, которое бы пользовалось этой возможностью). В листинге 13.4 приведена функция для построения логической палитры на базе цветовой таблицы DIB.
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->pa!NumEntries - nColor; for (int 1-0: ipalPalEntry[i].peBlue pLogPal->pa!Pa1Entry[i].peGreen pLogPal->palPalEntry[i].peRed pLogPal->palPa!Entry[i].peFlags
= -
pRGB[0]: pR6B[l]: pRGB[2]; 0;
Продолжение
722
Глава 13. Палитры
Листинг 13.3. Продолжение pRGB += nSize:
}
hPal = CreatePalette(pLogPal); } delete [] (BYTE *) pLogPal; return hPal;
Палитра и растры
723
Функция ищет в DIB цветовую таблицу и определяет количество цветов, необходимых для вывода растра. Учитывая, что в нормальных условиях можно реализовать только 236 цветов, отличных от статических, в цветовой таблице DIB не рекомендуется использовать более 236 нестатических цветов. Поле bidrlmportant предусмотрено специально для сокращения количества необходимых цветов. Цвета в таблице желательно отсортировать по частоте использования. Если некоторые из них не войдут в палитру, исключение должно начинаться с наименее используемых цветов. Функция CreateDIBPalette использует цветовую таблицу DIB только для построения логической палитры. Вопрос построения оптимальной палитры для изображений High Color и True Color рассматривается в следующем разделе, посвященном более общей теме — сокращению количества цветов в растре. А пока в том случае, если DIB не содержит цветовой таблицы, наша программа будет использовать полутоновую палитру. Эффект от заполнения палитры данными из цветовой таблицы DIB может быть очень заметным. Взгляните на рис. 13.6; первое изображение выведено с полутоновой палитрой без режима полутонового масштабирования (см. главу 10). Второе изображение выводилось с полутоновой палитрой и полутоновым масштабированием; качество рисунка заметно улучшилось. Последний рисунок был получен с применением специализированной палитры, построенной на основе цветовой таблицы, без полутонового масштабирования. Возможно, вас удивит то, что при использовании палитры, построенной на базе цветовой таблицы, режим полутонового масштабирования совершенно не улучшает качества изображения. Результат получается практически таким же, как при использовании полутоновой палитры с включением полутонового масштабирования.
Индекс палитры в цветовой таблице DIB
Рис. 13.6. Вывод DIB с полутоновой палитрой, с полутоновой палитрой в режиме HALFTONE и со специализированной палитрой
При выводе DIB таким функциям, как StretchDIBits, обычно передается флаг DIB_RGB_COLORS. Этот флаг сообщает GDI, что цветовая таблица DIB действительно "содержит значения RGB. GDI ассоциирует значения RGB из цветовой таблицы с цветами логической палитры, а затем преобразует индексы логической палитры в индексы системной палитры, записываемые в кадровый буфер. Поиск подходящих цветов в палитре проходит довольно медленно. В GDI предусмотрены две функции, позволяющие приложениям самостоятельно подбирать цвета: UINT GetNearestPalettelndexCHPALETTE hPal. COLORREF crColor); COLORREF GetNearestColorCHDC HOC. COLORREF crColor); Функция GetNearestPalettelndex просматривает все цвета логической палитры в поисках ближайшего совпадения для заданного эталона. Степень близости определяется расстоянием между двумя цветами в цветовом пространстве RGB. Для двух цветов RGB(rl,gl,bl) и RGB(r2,g2,b2) расстояние вычисляется по формуле
724
Глава 13. Палитры
С целью нахождения ближайшего совпадения GDI может просто использовать квадрат расстояния, чтобы обойтись без медленного вычисления квадратного корня. Функция GetNearestColor находит для заданного эталона ближайший цвет из системной палитры и возвращает его. . Конечно, GDI не подбирает цвета для каждого пиксела. При выводе DIB с флагом DIB_RGB_COLORS GDI подбирает замену для всех цветов цветовой таблицы и использует результат для вывода всех пикселов растра. Если текущая логическая палитра построена на базе цветовой таблицы растра, GDI позволяет исключить первый этап поиска. Чтобы воспользоваться этой оптимизацией, приложение должно заменить значения RGB в цветовой таблице DIB индексами логической палитры, а затем при использовании DIB передать флаг DIB_PAL_COLORS вместо DIB_RGB_COLORS. С флагом DIB_PAL_COLORS цветовая таблица DIB интерпретируется как массив индексов логической палитры. Следующая функция создает структуру BITMAPINFO с цветовой таблицей, содержащей индексы палитры. BITMAPINFO * IndexColorTableCBITMAPINFO * pOIB. HPALETTE hPal) { int nSize; int nColor; const BYTE * pRGB = GetColorTable(pDIB. nSize. nColor); if ( pDIB->bmiHeader.MBitCount>8 )// Без изменений return pOIB: // Создать новую структуру BITMAPINFO для модификации BITMAPINFO * pNew - (BITMAPINFO *) new BYTE[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*nColor]: pNew->bmiHeader - pDIB->bmiHeader; WORD * plndex = (WORD *) pNew->bmiColors;
Палитра и растры
ям 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 CreateDIBSectionPalette(HDC hDC. HBITMAP hDIBSec) { HDC hMemDC = CreateCompatibleDC(hDC); HGDIOBJ hOld = SelectObjectthMemDC. hDIBSec); RGBQUAD Color[256]:
for (int i=0; i
int nEntries = GetDIBColorTable(hMemDC. 0. 256. Color);
plndex[i] = GetNearestPa1etteIndex(hPal. R6B(pRGB[2]. pRGB[l]. pRGB[0])): else plndex[i] = 1;
HPALETTE hPal = LUTCreatePaletteUBYTE *) Color. sizeof(RGBQUAD), nEntries);
SelectObjectChMemDC, hOld); DeleteObject(hMemDC):
return pNew;
} Функция получает указатель на BITMAPINFO и логическую палитру. Она создает новую структуру BITMAPINFO, копирует данные формата и размеров, после чего строит цветовую таблицу с индексами палитры. Если манипулятор логической палитры не задан, предполагается, что растр будет выводиться с логической палитрой, созданной на базе цветовой таблицы, поэтому мы просто отображаем элементы цветовой таблицы растра в соответствующие позиции новой таблицы. Если логическая палитра задана, в ней ищутся элементы, ближайшие к значени-
725
return hPal;
}
Если для создания DIB использовалась функция CreateDIBSection, приложение не располагает действительной структурой BITMAPINFO, которая могла бы быть задействована для построения логической палитры.
724
Глава 13. Палитры
С целью нахождения ближайшего совпадения GDI может просто использовать квадрат расстояния, чтобы обойтись без медленного вычисления квадратного корня. Функция GetNearestColor находит для заданного эталона ближайший цвет из системной палитры и возвращает его. • Конечно, GDI не подбирает цвета для каждого пиксела. При выводе DIB с флагом DIB_RGB_COLORS GDI подбирает замену для всех цветов цветовой таблицы и использует результат для вывода всех пикселов растра. Если текущая логическая палитра построена на базе цветовой таблицы растра, GDI позволяет исключить первый этап поиска. Чтобы воспользоваться этой оптимизацией, приложение должно заменить значения RGB в цветовой таблице DIB индексами логической палитры, а затем при использовании DIB передать флаг DIB_PAL_COLORS вместо DIB_RGB_COLORS. С флагом DIB_PAL_COLORS цветовая таблица DIB интерпретируется как массив индексов логической палитры. Следующая функция создает структуру BITMAPINFO с цветовой таблицей, содержащей индексы палитры. BITMAPINFO * IndexColorTable(BITMAPINFO * pDIB. HPALETTE hPal) { int nSize: int nColor; const BYTE * pRGB = GetColorTable(pDIB. nSize, nColor): if ( pDIB->bmiHeader.MBitCount>8 ) // Без изменений return pOIB: // Создать новую структуру BITMAPINFO для модификации BITMAPINFO * pNew = (BITMAPINFO *) new BYTE[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*nColor]; pNew->bmiHeader = pDIB->bmiHeader; WORD * pIndex = (WORD *) pNew->bmiColors;
Палитра и растры
ям 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 CreateDIBSectionPalette(HDC hDC. HBITMAP hDIBSec) { HDC hMemDC = CreateCompatibleDC(hDC); HGDIOBJ hOld = SelectObjecUhMemDC, hDIBSec);
for (int i-0: i
RGBQUAD Color[256]:
return pNew;
SelectObject(hMemDC. hOld); DeleteObject(hMemDC);
int nEntries = GetDIBColorTable(hMemDC. 0. 256. Color); HPALETTE hPal = LUTCreatePaletteUBYTE *) Color. sizeof(RGBQUAD). nEntries);
} Функция получает указатель на BITMAPINFO и логическую палитру. Она создает новую структуру BITMAPINFO, копирует данные формата и размеров, после чего строит цветовую таблицу с индексами палитры. Если манипулятор логической палитры не задан, предполагается, что растр будет выводиться с логической палитрой, созданной на базе цветовой таблицы, поэтому мы просто отображаем элементы цветовой таблицы растра в соответствующие позиции новой таблицы. Если логическая палитра задана, в ней ищутся элементы, ближайшие к значени-
725
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, 0В», где R, G и В ограничиваются 7 битами. Все пикселы, у которых старшие биты RGB равны 1, относятся к последнему подузлу «1R, 1G, 1В». Деление узлов дерева продолжается до тех пор, пока не будет достигнут девятый уровень. Узлы второго уровня, находящиеся непосредственно под корнем, делятся по второму биту составляющих RGB; узлы третьего уровня делятся по третьему биту и т. д. Представление 24-разрядного пространства RGB октантным деревом теоретически связано с огромными затратами памяти. Дерево содержит 1 корень, 8 узлов второго уровня, 64 узла третьего уровня и 16,7 миллиона узлов девятого уровня. Полное октантное дерево состоит из 19 173 961 узла. Если каждый узел представляется 50 байтами, для хранения дерева потребуется 914 Мбайт памяти
727
Квантование цветов
(точнее — места на жестком диске). Фокус заключается в том, чтобы увеличивать дерево только в случае необходимости, и сокращать его при нехватке памяти. Собственно, именно по этой причине мы и используем дерево; работать с громадным массивом было бы гораздо проще. Синий
X
R,GfB
Рис. 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 = Pixels = SigmaRed = SigmaGreen =
leaf: 0: 0: 0: Продолжение
728
Глава 13. Палитры
Листинг 13.5. Продолжение SigmaBlue = 0: memset(Child. 0. sizeof(Child)):
} RemoveAl1(void); int PickLeaves(RGBQUAD * pEntry. int * pFreq. int size);
}: KNode::RemoveAlК void) { for (int i=0; i<8: i++) if ( Child[i] ) { Child[i]->RemoveAll(); ChildEi] = 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;
= Pixels; = = = =
( SigmaRed + Pixels/2 ) / Pixels; ( SigmaGreen + Pixels/2 ) / Pixels: ( SigmaBlue + Pixels/2 ) / Pixels; 0:
else int sum = 0:
for Cint i=0; i<8; i++) if ( Child[i] ) sum += Child[i]->PickLeaves(pEntry+sum. pFreq+sum, size-sum); return sum;
Переменная IsLeaf указывает, является ли узел листовым. Листовой узел определяется как узел, не имеющий потомков. В исходном состоянии дерева все листовые узлы находятся на девятом уровне. В процессе слияния узлы более высокого уровня тоже могут стать листовыми. Массив Child содержит 8 указателей на 8 потомков узла, не являющегося листовым. В остальных переменных
729
Квантование цветов
хранится количество пикселов и суммы их компонентов RGB для всех пикселов поддерева, корнем которого является текущий узел. Например, переменная Pixels корневого узла содержит общее количество пикселов во всем дереве. Следует учитывать, что сумма хранится в 32-разрядном целом без знака, поэтому октант24 ное дерево позволяет хранить не более 2 пикселов. Конструктор класса KNode устроен очень просто — он ограничивается инициализацией переменных класса. Метод RemoveAl 1 удаляет все узлы текущего поддерева, используя обычную рекурсию. Метод Pi deleaves собирает итоговую информацию, накопленную в дереве. Он заполняет массив структур PALETTEENTRY значениями RGB и заносит в целочисленный массив сведения о распределении цветов. Для этого мы просто перебираем узлы дерева и преобразуем каждый листовой узел в структуру PALETTEENTRY, значение RGB которой вычисляется усреднением значений RGB всех пикселов. Количество пикселов, представляемых каждым узлом, также сохраняется в массиве частот. Эта дополнительная величина может использоваться для сортировки массива PALETTEENTRY по частоте цветов. Класс октантного дерева KOctree приведен в листинге 13.6. Листинг 13.6. Класс октантного дерева, используемого для квантования цветов class KOctree {
typedef enum { MAXMODE = 65536 }: KNode int int
pRoot; Total Node; TotalLeaf:
void ReducetKNode * pTree, unsigned threshold): public: KOctreeO { pRoot = new KNode(false): Total Node = 1: TotalLeaf = 0: } -KOctreeO { if ( pRoot ) { pRoot->RemoveAll(): pRoot = NULL;
void AddColor (BYTE r. BYTE g. BYTE b): void ReduceLeavesCint limit); int GenPalettetRGBQUAD *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 += г: 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 ) f pNode->Child[index] = new KNode(mask==2); Total Node ++; if ( mask==2 ) Total Leaf ++; // Следовать дальше pNode = pNode->Child[index]; for (int threshold=l: TotalNode>MAXMODE: threshold++ ) Reduce(pRoot. threshold): // Объединить узел с листовыми узлами-потомками // и количеством пикселов, не превышающим threshold // Объединить листовой узел с количеством пикселов, // не превышающим threshold, с ближайшим соседом void KOctree::Reduce(KNode * рТгее, unsigned threshold) { if ( pTree==NULL ) return; bool childallleaf = true:
Квантование цветов
731
// Рекурсивно вызвать для всех не-листовых потомков for (int 1=0: i<8: i++) if ( pTree->Child[i] && ! pTree->Child[i]->lsLeaf ) { Reduce(pTree->Child[i]. threshold); if ( ! pTree->Child[i]->IsLeaf ) childallleaf = false: // Если все потомки являются листовыми узлами, // а количество пикселов не превышает порогового - объединить if ( childallleaf & (pTree->Pixels<=threshold) ) { for (int 1=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 (1=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 ) Продолжение
732
Глава 13. Палитры
Листинг 13.6. Продолжение pNode->Pixels pNode->SigmaRed pNode->SigmaGreen pNode->SlgmaBlue
+= += ++=
target. Pixels; target. Si gmaRed: target. SigmaGreen; target. Si gmaBlue:
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 threshold-1: 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 устанавливается константой MAXNODE. В настоящее время эта константа равна 65 536; обычно этого хватает для точного представления 16-разрядных изображений. Максимально допустимое дерево занимает около 3 Мбайт памяти. Если дерево содержит слишком много узлов, AddColor вызывает метод Reduce, чтобы произвести сокращение. Сокращение выполняется с постепенным повышением порога, начальное значение которого равно 1. На первом проходе объединяются все листовые узлы, содержащие один пиксел. Если после первого прохода по-прежнему остается слишком много узлов, порог увеличивается и процесс повторяется. Метод Reduce реализует алгоритм сокращения в три этапа. Сначала все не листовые подузлы сокращаются рекурсивным вызовом Reduce. Если после этого все подузлы текущего узла являются листовыми, а общее количество пикселов не превышает порога, все подузлы удаляются, а текущий узел помечается как листовой. Вспомните, что говорилось выше: AddColor добавляет информацию на каждый уровень дерева, поэтому каждый узел содержит сводные данные обо всех своих подузлах. На последнем этапе Reduce проверяет все листовые подузлы с небольшим количеством пикселов и объединяет их с одним из соседних узлов. Слияние соседних узлов выполняется методом Merge. Метод просто находит ветвь к листовому узлу и включает в нее данные RGB. Более рациональный алгоритм должен обеспечивать поиск ближайшего совпадения. Рассмотренные функции строят дерево и выполняют усечение, необходимое в том случае, если дерево становится слишком большим. После того как дерево построено, метод ReduceLeaves постепенно сокращает его до тех пор, пока количество листовых узлов не окажется ниже допустимого. Для сокращения дерева с увеличивающимся пороговым значением применяется уже знакомый метод Reduce. Мелкие узлы постепенно сливаются в большие узлы более высокого уровня, а большие узлы не участвуют в слиянии до тех пор, пока порог не поднимется до достаточно большой величины. Идея заключается в том, чтобы ограниченное число листовых узлов как можно точнее представляло распределение цветов в изображении. Таким образом, узлы с большим количеством пикселов попадут в итоговый набор цветов с большей вероятностью, нежели узлы с малым количеством пикселов. Метод GetPal ette завершает квантование, заполняя массив структур PALETTEENTRY и массив частот. Он вызывает метод ReduceLeaves, чтобы уменьшить количество листовых узлов до заданной величины, и метод KNode::PickLeaves для заполнения двух массивов. Нам остается лишь передать все пикселы растра классу KOctree для построения дерева, а затем сгенерировать палитру по данным листовых узлов. Класс KPaletteGen приведен в листинге 13.7. Листинг 13.7. Класс KPaletteGen: построение палитры с применением октантного дерева
class KPaletteGen : public KPixelMapper
734
Глава 13. Па/ KOctree octree:
PALETTEENTRY Ра11б[] = // Для изображения тигра с рис. 13.4
// Вернуть true, если данные изменились virtual boo! MapRGB(BYTE & red. BYTE & green, BYTE & blue) octree.AddColor(red. green, blue): return false;
public: void AddBitmapCKImage & dib) dib.PixelTransform(* this);
int GetPalette(RGBQUAD * pEntry. int * pFreq. int size) return octree.GenPalette(pEntry, pFreq. size);
int GenPalette(BITMAPINFO * pDIB, RGBQUAD * pEntry int size) '
735
Квантование цветов
{ 59. 52. { 55. 41. { 76. 51. { 99. 77. { 101. 97. { ИЗ. 108. { 153, 113, { 140. 119. { 166, 136. { 206, 148, { 170. 154. { 173, 149. { 212. 173. { 234, 207. { 232. 222. { 250. 244,
47 41 42 54 87 84 84 110 113 115
}, }. }. }, }, }. }, }. }, }.
150 }. 142 }, 148 }. 170 }. 209 }.
235 }.
// // // // // // // // // // // // // // // //
0. 3874 1. 1792 2. 2893 3. 2823 4, 5567 5. 1652 6. 5417 7, 2475 8. 4136 9. 2521 10, 2312 11, 1899 12, 3749 13. 1610 14. 2659 15. 2781
На рис. 13.8 представлено изображение тигра с палитрами из 16, 64 и 236 цветов, сгенерированными алгоритмом квантования по октантному дереву без полутонирования. int * pFrea
KImage dib: KPaletteGen pal gen; dib.AttachDIB(pDIB. NULL. 0): palgen.AddBitmap(dib): return palgen.GetPalettefpEntry. pFreq. size):
Класс KPaletteGen является производным от класса KPixelMapper, созданного в главе 12 для преобразования пикселов DIB. Вероятно, вы еще не забыли что класс KPnxelMapper должен только реализовать метод MapRGB, который будет вызываться для каждого пиксела растра. Метод KPaletteGen::MapRGB просто добавляет цветной пиксел в экземпляр класса KOctree. Метод AddBitmap перебирает все пикселы растра и вызывает MapRGB для каждого пиксела. Метод GetPalette возвращает окончательную цветовую таблицу. Глобальная функция GenPalette генерирует цветовую таблицу для упакованного иш-растра, для чего она использует классы KImage и KPaletteGen Палитра, сгенерированная алгоритмом квантования по октантному дереву, обеспечивает очень хорошее качество даже в сравнении с профессиональными графическими пакетами. Ниже приведена 16-цветная цветовая таблица, построенная для изображения тигра с рис. 13.4. Для каждого элемента цветовой таблицы приведены значения RGB и количество пикселов, представляемых данным элементом. Как видите, количества представляемых пикселов неплохо сбалансировзны.
Рис. 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 приведен класс KCol orMatch, реализующий линейный поиск цветов методом «грубой силы». Метод KCol orMatch:: Col orMatch ищет в массиве структур RGBQUAD цвет, ближайший к заданному в цветовом пространстве RGB. Листинг 13.8. Класс KColorMatch: простой подбор цветов class KColorMatch
{ public:
RGBQUAD int
* m_Colors: mjiEntries:
int squarednt i) { return i * i;
737
Сокращение цветовой глубины растра
if ( d < dis ) { dis = d: best = i: return best; void Setupdnt nEntry. RGBQUAD * pColor)
{
mjiEntries m Colors
= nEntry; = pColor;
В листинге 13.9 приведен простой класс для сокращения цветовой глубины растра, основанный на классах KColorMatch и KPixel Mapper. Класс KColorReduction поддерживает только построение 8-разрядных DIB-растров, однако он легко расширяется для работы с другими форматами. Его главный метод, ConvertSbpp, создает новый 8-разрядный растр, строит оптимальную цветовую таблицу с помощью алгоритма квантования по октантному дереву, а затем использует метод KImage: : Pixel Transform для обращения к алгоритму подбора цветов. Листинг 13.9. KColorReduction: сокращение цветовой глубины поиском ближайшего цвета class KColorReduction : public KPixelMapper { protected: int BYTE * BYTE * KColorMatch
mjiBPS: m_pBits: m_pPixel : m_Matcher:
public: BYTE ColorMatchdnt red. int green, int blue) { int dis = Ox7FFFFFFF: BYTE best - 0: if ( red<0 ) red=0: else if ( red>255 ) red=255; if ( green<0 ) 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 - squaretred - m_Colors[i].rgbRed); if ( d>dis ) continue; d +- squaretgreen - m_Colors[i].rgbGreen): if ( d>dis ) continue: d +- squareCblue - tn_Colors[i].rgbBlue):
// Вернуть true, если данные изменились virtual boo! MapRGBtBYTE & red. BYTE & green. BYTE & tilue)
{
*m_pPixel ++ = m_Matcher. Col orMatch (red. green, blue): return false:
virtual bool Startl_ine(int line)
{
m_pPixel = m_pBits + line * m_nBPS: // первый пиксел строки развертки return true:
public: BITMAPINFO * Convert8bpp(BITMAPINFO * pDIB);
BITMAPINFO * KColorReduction: :Convert8bpp(BITMAPINFO * pDIB)
Продолжение
738
Глава 13. Палитры
Листинг 13.9. Продолжение
{
m_nBPS = (pDIB->bmiHeader.biWidth + 3) / 4 * 4; // 8-разрядная // строка развертки int headsize = sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD): BITMAPINFO * pNewDIB = (BITMAPINFO *) new BYTE[headsize + m nBPS * abs(pDIB->bmiHeader.biHeight)]:
73S
Сокращение цветовой глубины растра
личие от алгоритма GDI заключается в распределении расхождения между ис ходным и найденным цветом по соседним пикселам, что влияет на подбор цве тов для этих пикселов. В алгоритме Флойда—Стейнберга ошибка делится на четыре неравные частр (3/16, 5/16, 1/16 и 7/16), прибавляемые к четырем соседним пикселам. Дл5 уменьшения количества сетчатых узоров, возникающих при смешивании, чет ные и нечетные строки развертки сканируются в противоположных направле ниях. На рис. 13.9 изображена схема распределения ошибок в алгоритме Флой да—Стейнберга.
memset(pNewDIB, 0. headsize): pNewDIB->bmiHeader.biSize pNewD!B->bmiHeader.bi Wi dth pNewDIB->bmiHeader.biHeight pNewDIB->bmiHeader.biPlanes pNewDIB->bmiHeader.biBitCount pNewDIB->bmiHeader.biCompression
= = = = = =
'
sizeof(BITMAPINFOHEADER): pDIB->bmiHeader.biWidth: pDIB->bmiHeader.biHeight: 1; 8: BI_RGB;
3/16
l
p
7/16
7/16
5/16
1/16
1/16
5/16
3/16
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.AttachD!B(pDIB. NULL. 0): dib. Pixel Transform^ this): return pNewDIB;
} Класс KColorReduction обеспечивает почти тот же результат, что и при выводе растра средствами GDI без применения режима HALFTONE. Удивляться не приходится, поскольку GDI использует практически такой же алгоритм, хотя и лучше оптимизированный. В режиме HALFTONE GDI может использовать полутонирование для создания плавных переходов между оттенками цвета. Алгоритм поиска ближайших совпадений подбирает цвет для каждого пиксела независимо от других, тогда как полутоновый алгоритм пытается генерировать блоки пикселов, средний цвет которых аппроксимирует цвет исходного изображения. Режим масштабирования HALFTONE поддерживается только в системах семейства NT. Полутоновый алгоритм, используемый GDI, основан на простом смешении цветов. Существуют и более качественные алгоритмы — например, алгоритм рассеивания ошибок (error-diffusion) Флойда—Стейнберга. В этом алгоритме цвет каждого пиксела суммируется с накапливаемой ошибкой, изначально равной 0. Для полученного цвета обычным образом подбирается соответствие в цветовой таблице, а возвращаемый индекс сохраняется в итоговом растре. Основное от-
Обратное сканирование
Рис. 13.9. Распределение ошибок в алгоритме Флойда—Стейнберга
В листинге 13.10 приведена наша реализация алгоритма распределения опп бок. Класс KErrorDiffusionColorReduction является производным от класса KColor Reduction, что позволяет нам использовать готовый код подбора цветов и постро< ния 8-разрядного растра. Вместо функции отображения пикселов переопредели ется механизм обработки 24-разрядных строк развертки. Алгоритму рассеяни ошибок нужны дополнительные переменные для хранения накапливаемой оши! ки и флага, управляющего направлением сканирования строки. Реализация &j горитма на уровне строк развертки выглядела бы гораздо проще и работала б быстрее, но для полноты решения мы должны предоставить реализации дл строк развертки в других форматах. Листинг 13.10. Алгоритм рассеяния ошибок Флойда—Стейнберга class KErrorDiffusionColorReduction : public KColorReduction
{
int int int bool
* red_error; * green_error: * blue_error; 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);
Сокращение цветовой глубины растра
741
void KErrorDiffusioncolorReduction::Map24bpp(BYTE * pBuffer. int width) int next_red. next_green. next_blue;
public: if ( m_bForward ) {
BITMAPINFO * Convert8bpp(BITMAPINFO * pDIB): inline void ForwardDistributetint 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;
// //
X 7/16 3/16 5/16 1/16
} else nexterror = curerror[l]; } inline void BackwardDistributeOnt error, int * curerror, int & nexterror) { if ( (error<-2) | (error>2) ) // Ошибка -2..2 не распределяется { nexterror = curerror[-l] + error * 7 / 16: curerror[ 1] += error * 3 / 16: curerror[ 0] += error * 5 / 16: curerror[-l] += error / 16:
// //
7/16 X 1/16 5/16 3/16
}
else nexterror = curerror[-l];
} BITMAPINFO * KErrorDiffusioncolorReduction::Convert8bpp(BITMAPINFO * 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:
next_red = red_error[0]; next_green = green_error[0]; next_blue = blue_error[0]; for (int i=0: { int red int green int blue
i<width; 1++) = pBuffer[2]: = pBuffer[l]; = pBuffer[0]:
BYTE match = m_Matcher.ColorMatch( red+next_red. green+next_green, blue+next_blue ); ForwardDistributetred - m_Matcher.m_Colors[match].rgbRed . red_error +i. next_red): ForwardDistribute(green - m_Matcher.m_Colors[match].rgbGreen, green_error+i, next_green); ForwardDistributetblue - 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_Co1ors[match].rgbGreen. green_error+i, next_green): BackwardDistributeCblue - m_Matcher.m_Colors[match].rgbBlue. blue_error+i. next_blue): Продолжение!:
742
Глава 13. Палитры
Листинг 13.10. Продолжение * m_pPixe! --= match; pBuffer -= 3:
Класс рассеяния ошибок содержит четыре дополнительные переменные. В трех из них хранятся массивы ошибок для каналов RGB. Функция ConvertSbpp выделяет память под массивы из кучи и инициализирует ее нулями. Обратите внимание: инициализация обеспечивает возможность индексации 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-цветов и оттенков серого цвета, изменения видеорежима, построения палитры на базе растра, квантования цветов, распределения ошибок и т. д.
Что такое шрифт?
745
рассмотрим наборы символов, кодировки, глифы, шрифты вообще и их конкретную разновидность — шрифты TrueType, а также технологию внедрения шрифтов.
Что такое шрифт?
Глава 14 Шрифты С этой главы начнется наше знакомство со шрифтами и текстовыми операциями в графическом программировании Windows. Шрифты и их применение в печати имеют долгую и интересную историю. Давно, в 2400 году до нашей эры, индусы освоили изготовление резных штампов. Около 450 года нашей эры китайцы научились оставлять на бумаге оттиски штампов, намазанных чернилами, положивших начало современному книгопечатанию. В 1049 году китайцы разработали методику печати с применением глиняных литер, а в 1241 году корейцы перешли на металлические литеры. Еще два века спустя, в 1452 году, Гутенберг открыл новую эпоху в книгопечатании. С его изобретения — печатного станка — начался массовый выпуск типографских литер, используемых при наборе страниц текста. С этого времени полный набор символов одной гарнитуры и кегля стал называться в печатном деле «шрифтом». В 1976 году некий профессор решил выпустить второе издание своей книги, опубликованной за несколько лет до этого с применением тех же свинцовых матриц, что и у Гутенберга. К своему удивлению, он узнал, что старая технология постепенно уходит в прошлое, а новая — фотооптические наборные машины — еще не обеспечивает приемлемого качества. Профессор отказался использовать столь несовершенную технологию для представления плодов своего 15-летнего упорного труда и взялся за решение старых типографских проблем на базе компьютерных технологий. Четыре года спустя он разработал новый способ описания шрифтов математическими формулами, что привело к появлению полноценных наборных систем. С помощью одной из таких систем он и опубликовал свою работу, издание которой задержалось на 4 года. Профессора звали Дональд Кнут (Donald E. Knuth), шрифтовая программа называлась METAFONT, а для верстки использовался пакет ТеХ. Более того, все плоды труда Кнута вместе с полными исходными текстами были доступны для всех желающих, поэтому пользователи всего мира могли конструировать шрифты для любого языка и создавать электронные макеты книг. Шрифты и текст традиционно считаются весьма сложной темой. Эта глава посвящается шрифтам, а следующая — операциям с текстом. В этой главе мы
Компьютерная верстка всегда считалась одной из главных областей применения персональных компьютеров. В школьные годы и на протяжении всей жизни всем нам приходится создавать всевозможные документы и готовить к публикации книги. Процесс компьютерной верстки сильно зависит от поддержки шрифтов и текстовых операций на уровне операционной системы. Впрочем, шрифты и текст не относятся к базовым функциям систем компьютерной графики — в некоторых книгах, посвященных теоретическим основам компьютерной графики, они вообще не упоминаются. Скорее, шрифты и текст следует рассматривать как объекты применения общих принципов для решения целого класса практических задач. Как правило, шрифтовые и текстовые средства операционной системы реализуются с применением базовых графических примитивов (пикселы, линии, кривые, фигуры и растры). Вы даже можете создать собственные средства для работы со шрифтами и текстом на базе этих примитивов. Одним из основных инструментов компьютерной верстки являются шрифты — своего рода шаблоны для представления символов языка, с которым вы работаете. Традиционно шрифт определяется как полный набор литер одной гарнитуры и одного кегля, что соответствует специфике применения шрифта в типографском деле. Литерой называется прямоугольный блок (обычно металлический), на лицевой поверхности которого находится рельефное изображение символа. Цифровые технологии заметно расширили смысл термина и возможности шрифтов. В этом разделе мы рассмотрим базовые концепции и термины, относящиеся к работе со шрифтами в контексте графического программирования Windows.
Наборы символов и кодировки Набор символов (character set) в системе Windows определяется... просто как совокупность символов. У каждого набора есть имя и числовой идентификатор. Например, стандартный набор символов Windows называется ANSI_CHARSET, его идентификатор равен 0, и он содержит символы 7-разрядной стандартной кодировки ANSI, определенной в Windows для западных языков. В окне DOS-сеанса используется набор OEM_CHARSET с идентификатором 255; он содержит те же 7-разрядные символы ANSI с дополнительными символами, которые были определены компанией IBM на ранних порах существования DOS. Наборы символов с однобайтовыми идентификаторами вряд ли можно считать хорошим решением, особенно в эпоху глобальных электронных коммуникаций в Интернете. На смену им пришла концепция кодировок, или кодовых страниц (code pages). Кодировкой называется схема представления символов из
746
Глава 14. Шрифты
заданного набора одним или несколькими байтами информации. Таким образом, с формальной точки зрения кодировка представляет собой отображение последовательности битов в набор символов. Кодировки, в отличие от наборов символов, обозначаются двухбайтовыми числовыми идентификаторами, что обеспечивает поддержку большего количества языков. В табл. 14.1 перечислены наборы символов и соответствующие им кодировки, поддерживаемые операционной системой Windows. Первые 14 наборов, от SHIFJIS_CHARSET до EASTEUROPE_SET, связаны с кодировками однозначным соответствием. Например, для набора SHIFTJIS_CHARSET используется кодировка 932 (сокращение JIS означает Japanese Industry Standard, то есть «японский промышленный стандарт»). Набор символов GB2312_CHARSET соответствует кодировке 932 (GB — сокращение китайского национального стандарта). Последним трем наборам, ANSI_CHARSET, OEM_CHARSET и MAC_CHARSET, соответствуют разные кодировки в зависимости от локального контекста системы/процесса. Они отображаются на разные кодировки в зависимости от того, где действительно находится ваш компьютер или, по крайней мере, где компьютер «думает», что находится. Если в стандартном локальном контексте используется английский язык, то набор ANSI_CHARSET соответствует кодировке 1252, OEM_CHARSET соответствует кодировке 437, a MAC_CHARSET — кодировке 10000.
747
Что такое шрифт?
Имя набора символов
Идентификатор Кодировка набора символов
Применение
RUSSIAN_CHARSET
204
1251, кириллица (Windows)
Славянские страны
THAI_CHARSET
222
874, тайский
EASTEUROPE_CHARSET
238
1250, Windows Latin 2
Центральная Европа
ANSI_CHARSET
0
1252, Windows Latin 1 1250, Windows Latin 2
США, Великобритания, Канада и т. д. Венгрия, Польша и т. д.
1256, арабский (Windows)
OEM CHARSET
255
Таблица 14.1. Наборы символов и кодировки Имя набора символов
Идентификатор набора символов
Кодировка
SHIFTJIS_CHARSET
128
932, японский
Япония
HANGUL_CHARSET
129
949, корейский
Корея
JOHAB_CHARSET
130
1361
GB2312 CHARSET
134
936, китайский (уп-
Применение
Китай, Сингапур
рощенное письмо) CHINESEBIG5 CHARSET
136
950, китайский (традиционное письмо)
GREEK CHARSET
161
1253, греческий (Windows)
TURKISH CHARSET
162
1254, турецкий (Windows)
163
1258, вьетнамский (Windows)
VIETNAMESE CHARSET HEBREW CHARSET
177
1255, иврит (Windows)
ARABIC CHARSET
178
1256, арабский (Windows)
BALTIC CHARSET
186
1257, прибалтийский (Windows)
Тайвань, Гонконг
Турция
MAC CHARSET
77
Ирак, Египет, Йемен и т. д.
437, MS_DOS Latin 1
США, Великобритания, Канада и т. д.
852, MS_DOS Latin 2
Венгрия, Польша и т. д.
864, MSJ3OS арабский
Ирак, Египет, Йемен и т. д.
10000, Mac (англоязычные страны)
США, Великобритания, Канада и т. д.
10029, Mac (Центральная Европа)
Венгрия, Польша и т. д. Украина, Россия и т. д.
10007, Mac (кириллица)
Большинство кодировок содержит 256 символов. Один символ в них представляется всего одним байтом, поэтому эти кодировки называются однобайтовыми. Первые 128 символов однобайтовой кодировки обычно совпадают с символами 7-разрядного стандарта ANSI. Первые 3-2 символа соответствуют неотображаемым управляющим кодам, за ними следует пробел, знаки математических операций и служебные символы, цифры и буквы английского алфавита в верхнем и нижнем регистре. Содержимое следующих 128 символов сильно изменяется в зависимости от кодировки. Именно здесь хранятся буквы национальных алфавитов, дополнительные знаки, символы псевдографики и даже недавно появившийся знак «евро». На рис. 14.1 приведено содержимое кодировки 1252 (Windows Latin 1). Как видно из рисунка, вторая половина кодировки содержит символы национальных алфавитов, денежные знаки, апострофы и кавычки и т. д. Некоторые символы, помеченные пустыми прямоугольниками, не используются. Первый символ, 0x80, недавно был закреплен за знаком «евро». Для сравнения на рис. 14.2 изображена кодировка Windows для работы с кириллицей (1251). Обратите внимание: знак «евро» находится в другой позиции, поскольку символ с кодом 0x80 уже занят.
748
Глава 14. Шрифты
00 П 10
п
П
D
а
П
П
! п # $ % 30 0 1 2 3 4 5 40 А В с D Е 50 р Q R s Т и 60 а Ь с d е 70 р q г S t и 80 € D „
а
20
@
7
90
D
АО ВО
0
СО
А DO D ЕО а FO б
а а п ) *
П D П П D D D П D
i ± А N а п
Ф
/
£ а
2
3
А О а 6
А О a 6
А О a 6
П f
б
П
(
7 8 9 F G Н I V W X Y f g h i V w X У + • %0 t + ~ тм • - — i ¥ i § © 1 , М 1 А & с Е Е 6 О X 0 и о а ж 9 ё ё 0 6 -н 0 и Л
j z j
П П +
а а , <
D П =
К^ L М \\ ] k 1 m
[
z S s
{ < •> a « о » Е Ё
а а _ / > ? N о Л
П
0
П 1 } (Е П Z П ае а z Y - - ® ~
У<
'/2
I
I
и и и
Y i
ё i и и и У ё
3
/4
1,
1
I В i
\>
У
I Р
Рис. 14.1. Кодировка Windows Latin I (1252) 00 П 10 П 20 30 40 50 60 70 80 90 АО
D П D D П D I п # 0 1 2 3 А В С р Q R S a Ь с р q г s ъ г j f с " '
@ 1
1)
ВО
О
СО
А Р a
DO ЕО FO
Р
У ± Б С б с
а а а п а а а D $ % t
4 D Т d t
5 б Е F и V е f и V
ээ
"
У J п I i г В Г Д т У Ф в г д т У Ф
•
Г М Е X е X
1
G W
П D п D D D D П ( ) * + 8 9 ? H I J К X Y Z [ h i j k
g w X t + € - — П i i § E ё f Ж 3 И ц ЧШ ж 3 и ц ч ш
D D П а а П _ / •> < = > 9 L М N О \ ] Л 1 m П 0
{ 1 У %, Jb < Н> тм Jb > н> © С « № e » j И К л М Щ Ъ Ы Ь и к л М щ ъ ы ь z
Рис. 14.2. Кодировка Windows Cyrillic (1251)
Хотя однобайтовых кодировок хватает для представления символов большинства мировых языков, три восточных языка содержат слишком большое количество символов, не укладывающееся в границы однобайтовой кодировки. В китайской письменности используются тысячи иероглифов, часть из которых была позаимствована в Японии и Корее. Символы больших иероглифических наборов представляются несколькими байтами, поэтому такие кодировки обычно называются двухбайтовыми или многобайтовыми. В многобайтовом наборе символов (MultiByte Character Set, MBCS) символы 7-разрядного набора ASCII представляются одним байтом, а иероглифы китайского, японского и корейского языка — двумя байтами. Текстовая строка в кодировке MBCS всегда анализируется слева направо. Если первый (префиксный) байт меньше 128 (0x80), значит, перед нами однобайтовый символ из первой половины кодировки Windows Latin I (1252). Если префиксный байт равен 128 и выше, необходима дополнительная проверка, поскольку в двухбайтовых символах могут использоваться только байты из определенных интервалов. Если префиксный байт принадлежит к допустимому интервалу, происходит дополнительная проверка второго байта. Для кодировки 936, используемой в Китае и Сингапуре, оба байта должны лежать в интервале [OxA1...0xFE], что позволяет представить до 8836 двухбайтовых символов. Кодировка 949, используемая в Корее, устроена чуть сложнее. В ней префиксный байт принадлежит интервалу [0x81..OxFE], а второй байт должен входить в один из интервалов [0x41..Ох5а], [0x61..Ох7а] и [Ох81..0хЕЕ]. Количество допустимых символов увеличивается до 19 278. На рис. 14.3 приведен небольшой фрагмент традиционной китайской кодировки. Обратите внимание: китайские иероглифы в кодировке 950 сортируются по количеству черт. На рис. 14.3 изображены относительно простые иероглифы, содержащие не бо1 лее четырех черт .
т
A440 A450
а Ц
} К Ъ к h ц - ® I S s I Н О П э Юя Н 0 п э ю я ~
749
Что такое шрифт?
A460
ь
Я
ill
A470 A4AO A4BO A4CO
ff
Я
A4DO A4EO A4FO
я ьь
Л А
75 К 7
Y
р ±
Iав я-
хи
Е
4 Ж
ЛА
Ф тс
К
я-
Я В
Jt
.Л
А Л Л
т Е В
ЯР
-к
а
ж /Б
it
/L
а
И 91 ±
ги
Рис. 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 содержит 11 172 иероглифа хангыль, используемых в Корее. На рис. 14.4 изображены символы, входящие в зону условных знаков Unicode.
2600 2610 D 2620
а
X П П П
О
2630 2640 2650 2660
«Р
0 Л жП П
©
¥
Т V
С
ж
ПР
00
0
*
л
щ я
Рис. 14.4. Зона условных знаков Unicode
Хотя операционная система Windows проектировалась для поддержки разных кодировок и языков, для работы с конкретными кодировками и языками нужны дополнительные файлы, которые могут отсутствовать в стандартном варианте установки вашей системы. Дополнительные пакеты устанавливаются при помощи приложения Regional Settings (Язык и стандарты) панели управления либо с компакт-диска операционной системы, либо с web-сайта Microsoft. Функция EnumSystemCodePages API перечисляет все кодовые страницы, поддерживаемые или установленные в вашей системе.
751
Что такое шрифт?
Глифы Наборы символов и кодовые страницы определяют лишь логическую группировку и представление символов, а не их внешний вид. Символ — всего лишь абстрактная концепция, а не конкретное представление. Нарисованный на бумаге символ обретает графическую форму, которая называется глифом (glyph). Например, в кодировке Windows Latin 1 английская прописная буква А имеет индекс 0x41, однако она может выглядеть по-разному, как показано на рис. 14.5.
Рис. 14.5. Различные глифы для буквы А
Взаимосвязь между глифом и символом Между символами и глифами в шрифте обычно существует однозначное соответствие. Один символ представляется ровно одним глифом, а один глиф представляет ровно один символ. Впрочем, это не всегда так. Встречаются символы, которые представляются комбинацией нескольких глифов, а один и тот же глиф может использоваться в разных символах. Такие глифы характерны для китайских или корейских иероглифов, которые часто состоят из нескольких частей, хотя в качественных шрифтах лучше использовать несколько версий одного глифа. Глифы символов также могут изменяться в зависимости от контекста, в котором записывается символ. Например, символы, находящиеся в начале или в конце предложения, могут оформляться специальными глифами. В частности, контекстные формы глифов широко используются в арабских языках, а при вертикальной записи китайского текста изменяется ориентация скобок. Если некоторые комбинации символов расположены по соседству, они могут быть преобразованы в один глиф, называемый лигатурой. В общем случае символ представляется одним или несколькими глифами, которые могут использоваться несколькими символами; также допускается объединение нескольких символов в лигатуру по специальным правилам. На рис. 14.6 продемонстрирована связь между символами и глифами. В первой строке приведена буква О с разными диакритическими знаками, за которой следуют китайские иероглифы с общим левым ключом. Эти примеры показывают, что один символ может соответствовать нескольким глифам. Во второй строке приведены некоторые лигатуры, используемые в датском, норвежском, французском и английском языках. Третья строка показывает, как круглые и квадратные скобки преобразуются в вертикальные глифы при традиционном вертикальном китайском письме, которое продолжает использоваться в особых случаях (например, в свадебных приглашениях). В последней строке изображены четыре группы глифов для трех арабских символов. Каждый арабский символ может иметь до четырех контекстных глифов для изолированной, начальной, конечной и промежуточной форм.
752
Глава 14. Шрифты
00000 A+E-/E C+E-ffi f+i=fi f+l=fl
(
4d±\ т)
V 4-М'. 1 1т л
Рис. 14.6. Связь между символами и глифами
Элементы глифа Глифы с постоянными атрибутами обычно группируются. Для букв латинского алфавита к таким атрибутам относятся толщина черт, стиль штриха, применение засечек, выравнивание по базовой линии, форма овалов и петель, величина надстрочных и подстрочных частей и т. д. Базовой линией (baseline) называется воображаемая линия, предназначенная для вертикального выравнивания глифов. Латинские буквы обычно выравниваются по базовой линии; исключение составляют буквы с подстрочными элементами (например, f, g, j и Q). Высота строчной буквы х называется х-высотой и обычно определяет высоту основной части всех глифов строчных букв. Некоторые строчные глифы поднимаются над высотой буквы х; их выносные элементы называются надстрочными (ascender). Некоторые строчные глифы спускаются ниже базовой линии; соответствующие элементы глифов называются подстрочными (descender). Кроме того, глифы могут обладать засечками (serifs ) — маленькими поперечными черточками на концах основных линий. Маленький шарик на конце черты (как в буквах а, с, f и у) называется каплевидным элементом (ball, или ball terminator). Внутрибуквенным просветом (counter) называется область, полностью или частично окруженная глифом (как в буквах р, d или е). Термин «полуовал» (bowl) относится к базовой форме таких букв, как С, G и D. На рис. 14.7 изображены некоторые элементы глифов с засечками.
Засечка
Надстрочный выносной элемент
Полуовал
Внутрибуквенный просвет
Подстрочный выносной элемент Рис. 14.7. Структурные элементы глифа для латиницы
753
Глифы других языков могут иметь аналогичную структуру или содержать другие элементы, унаследованные по историческим причинам.
Шрифт После знакомства с наборами символов, кодировками и глифами можно дать определение шрифта. Шрифтом называется совокупность глифов, обладающих сходным графическим стилем, для которой определено отображение символов поддерживаемых кодировок в глифы. Шрифт может поддерживать одну или несколько кодировок; для каждого символа каждой кодировки он устанавливает соответствие с группой глифов, образующих графическое представление символа. Глифы и правила отображения символов в глифы относятся к базовым компонентам шрифта. Шрифты обладают множеством других атрибутов. Так, у каждого шрифта имеется полное имя (например, Times New Roman Bold или Courier New Italic). Имена шрифтов обычно защищаются авторским правом. Например, компания Microsoft обладает правами на шрифт Wingdings, а шрифт Courier New Italic принадлежит Monotype Corp. Шрифты обычно хранятся в физических файлах в подкаталоге шрифтов системного каталога. На панели управления имеется приложение Fonts (Шрифты) для просмотра, установки и удаления шрифтов в системе. Чтобы получить список всех шрифтов, установленных в системе, необходимо перебрать ключи реестра. Код приведенного ниже фрагмента перечисляет все шрифты в системе и использует собранные данные для заполнения списка. void ListFonts(KListView * pList) const TCHAR Key_Fonts[] - _T("SOFTWARE\\Microsoft\\Windows NT" "\\CurrentVersion\\Fonts"); HKEY hKey: if ( RegOpenKeyEx(HKEY_LOCAL_MACHINE. Keyjonts. 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 ( RegEnumValue(hKey. i. szValueName. & nValueNameLen. NULL. & dwType. szValueData, & nValueDataLen) != ERROR_SUCCESS ) break; pList->Add!tem(0. szValueName): pList->Add!tem(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
1
Произвольный шаг шрифта
FIXED_PITCH
2
Моноширинный шрифт
VARIABLE_PITCH
4
Пропорциональный шрифт
FF_DONTCARE
0«4
Шрифт с произвольными атрибутами
FF_ROMAN
1«4
Шрифт с переменной толщиной линий и засечками
FFJWISS
2«4
Шрифт с переменной толщиной линий без засечек
FF_MOOERN
3«4
Шрифт с постоянной толщиной линий
FFJCRIPT
4«4
Рукописный шрифт
FF_DECORATIVE
5«4
Затейливый оформительский шрифт
В моноширинных шрифтах все глифы имеют одинаковую ширину. Моноширинные шрифты обычно применяются в окнах DOS-сеансов, при выводе листингов и вообще всюду, где необходимо обеспечить выравнивание по вертикали. В пропорциональных шрифтах глифы обладают разной шириной; буквы i или 1 занимают гораздо меньше места, чем т. Текст, выведенный пропорциональным шрифтом, лучше воспринимается человеческим глазом, поэтому в книгах, элек-
755
Что такое шрифт?
тронной документации и на web-страницах используются пропорциональные шрифты. Шрифты семейства Roman обладают переменной толщиной линий и засечками. В семействе Swiss используется переменная толщина линий, но без засечек. Шрифты семейств Roman и Swiss обычно являются пропорциональными. Семейство Modern содержит шрифты с постоянной толщиной линий, как правило — моноширинные. Шрифты семейства Script имитируют рукописный текст. Все остальные экзотические шрифты отнесены к семейству Decorative. На рис. 14.8 приведены примеры шрифтов некоторых семейств.
Roman Swiss Modern
Roman Swiss Modern
Roman Swiss Modern
Roman Swiss
О&этр/ •Semratiuc
Script ffiecorative
Script
Script DECORATIVE
Modern
Рис. 14.8. Классификация семейств шрифтов
В приложениях обычно удобнее работать с семействами шрифтов, нежели с отдельными шрифтами, поскольку семейств меньше и из них удобнее выбирать. В GDI существует функция EnumFontFamiliesEx для перечисления всех семейств шрифтов, доступных в системе. int EnumFontFamiliesEx (HOC hDC. LPLOGFONT IpLogFont. FONTENUMPROC IpEnumFontFamExProc, LPARAM IParam, DWORD dwFlags):
В первом параметре передается контекст устройства. Некоторые графические устройства (например, лазерные принтеры или принтеры PostScript) могут поддерживать аппаратные шрифты, предназначенные только для данного устройства. Второй параметр указывает на структуру LOGFONT, поля которой 1 fCharset и IfFaceName определяют набор символов и гарнитуру,-интересующие приложение. Если указать набор символов DEFAULT_CHARSET, семейства шрифтов, поддерживающие несколько наборов, будут многократно включены в список. При указании конкретного набора символов в перечислении участвуют только семейства шрифтов, содержащие заданную категорию глифов (например, для набора SYMBOL_ CHARSET — глифы символических знаков). Поле IfPitchAndFamily структуры LOGFONT должно быть равно нулю. Параметр IpEnumFontFamExProc указывает на глобальную функцию, вызываемую для каждого перечисляемого семейства шрифтов — такое решение плохо соответствует стилю C++. Впрочем, у нас есть параметр 1 Рагат с данными, передаваемыми вызывающей стороной функции косвенного вызова; этим параметром можно воспользоваться для стыковки C++ с Win32. Последний параметр dwFlags должен быть равен 0. Функция EnumFontFamlliesEx играет ключевую роль при заполнении списков доступных шрифтов в диалоговых окнах приложений. С ее помощью можно получить перечень всех семейств, поддерживающих конкретный набор символов, или всех наборов, поддерживаемых для конкретной гарнитуры. В листинге 14.1
756
Глава 14.
приведен вспомогательный класс для работы с этой функцией. Реализация по умолчанию сохраняет результаты перечисления в списке. Листинг 14.1. Перечисление семейств шрифтов
class KEnumFontFamily KListView * m_pList; int static CALLBACK EnumFontFamExProctENUMLOGFONTEX *lpelfe NEWTEXTMETRICEX *lpntme. int FontType. LPARAM IParam) if ( IParam ) return ((KEnumFontFamily *) lParam)->EnumProc(lpelfe. Ipntme, FontType); else return FALSE: public: LOGFONT int unsigned
m_LogFont[MAX_LOGFONT]; mjiLogFont; m_nType:
if ( (FontType & m_nType)==0 ) return TRUE;. if ( mjiLogFont < MAX_LOGFONT ) m_LogFont[m_nLogFont ++] = lpelfe->elfLogFont; (const (const (const (const
char char char char
*) *) *) *)
lpelfe->elfFullName)lpelfe->elfScript)lpelfe->elfStyle)-' lpelfe->elfLogFont.lfFaceNaine);
m_pList->AddItem(4. lpelfe->elfLogFont.lfHeight)m_pList->AddItem(5. lpelfe->elfLogFont.lfWidth)-' m_pList->AddItem(6. lpelfe->elfLogFont.lfWeight); return TRUE;
void EnumFontFamilies(HDC hdc. KListView * pList BYTE charset - DEFAULT_CHARSET. TCHAR * FaceName - NULL unsigned type - RASTERJONTTYPE I TRUETYPE FONTTYPE I DEVICE_FONTTYPE) ~ m_pList m_nType
pList; - type;
LOGFONT If; memset(& If. o. sizeof(lf));
If.lfCharSet = charset: If.lfFaceNameCO] = 0; If.lfPitchAndFamily = 0 ;
if ( FaceName ) Jxscpy(1 f. 1 fFaceName, FaceName) ; Enum FontFamiliesEx(hdc. & If. (FONTENUMPROC) EnumFontFamExProc. (LPARAM) this, 0): На рис. 14.9 сопоставлены результаты перечисления шрифтов и их семейств. Перечисление шрифтов, основанное на просмотре системного реестра, выводит список всех физических шрифтов в системе. Мы видим четыре шрифта семейства Arial, четыре шрифта семейства Courier New и т. д. При перечислении семейств некоторые семейства встречаются в списке многократно, если они поддерживают разные наборы символов. Например, семейство шрифтов Arial поддерживает 9 разных наборов. ЦЩ^^й^ ^ '"Г" ' ' .Nwne
virtual int EnumProc(ENUMLOGFONTEX *lpelfe. NEWTEXTMETRICEX *lpntme int FontType}
mj>List->AddItem(0. m_pList->AddItem(l. m_pList->Add!tem(2. m_pList->AddItem(3.
757
Что такое шрифт?
Tahoma (TrueType) Microsoft Sans Serif Regular (TrueType) : SimSun 81 NSimSun (TrueType) SimHei (TrueType) MrngLrU S, PMingLiU (TrueType) Roman (All res) ! Script (All res) Modern (All res) Arial (TrueType) ] Arid Bold (TrueType) Arial Bold Italic (TrueType) Arial Italic (TrueType) Courier New (TrueType) Courier New Bold (TrueType) Courier New Bold Italic (TrueType) Courier New Italic (TrueType) Lucida Console (TrueType)
' '' f**
«Jo&i
TAHOMA.TTF MICROSS.TTF simsun.ttc simneitlf mingliu.ttc ROMAN.FQN SCRIPT. FON MODERN.FON ARIALTTF ARIALBD.TTF ARIALBI.TTF ARIALI.TTF COUR.TTF COURBD.TTF COURBI.TTF COURI.TTF LUCON.TTF
i±
*
•w»-
Ш1ЙШ fdNw* Arial Arial Aiial Aiial Arid Aiial Arial Arial . Aiial Courier New Courier New Courier New Courier New Courier New Courier New Courier New ' «J
Hw*
Western Hebrew Arabic Greek Turkish Baltic Central European Cyrillic Vietnamese Western Hebrew Arabic Greek Turkish Baltic Central European 1
„i«X| Style
Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular Regular
1 f«*to? Aiial Aiial Arial Arial Aiial Arial Anal Aiial Arial Courier New Courier New Courier New Courier New Courier New Courier New Courier New
}«**1* 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36
V
и
»
Рис. 14.9. Сравнение шрифтов и семейств шрифтов
При поддержке двухбайтовых кодировок (для японского, китайского и корейского языков) функция EnumFontFamiliesEx возвращает два семейства. Например, семейство шрифтов Gulim поддерживает набор HANGUL_CHARSET; в процессе перечисления для него будут указаны семейства Gulim и @Gulim. Семейства шрифтов, имена которых начинаются с символа @, обладают особыми средствами для поворота двухбайтовых глифов, что позволяет имитировать вертикальную письменность, используемую в Китае, Японии и Корее. С помощью флагов семейств и шага шрифта, перечисленных в табл. 14.2, пытаются классифицировать шрифты и семейства всего одним байтом, что, конечно, не обеспечивает необходимой точности. Значительно более точный способ описания шрифта предоставляет структура PANOSE, в 10 байтах которой кодируются сведения обо всех важнейших характеристиках шрифтов — типе семейства, наличии засечек, насыщенности, пропорциональности, контрасте и т. д.
758
Глава 14. Шрифты
Растровые шрифты Существуют разные способы представления глифов шрифта. В простейшем варианте пикселы, образующие глиф, представляются в виде растрового изображения. Такие шрифты называются растровыми. Возможны и другие решения — например, описывать контуры глифа прямыми линиями (векторные шрифты). Большинство шрифтов, используемых в системе Windows в наши дни, относят. ся к категории TrueType или Open Type. В этих шрифтах для представления контура глифа и управления процессом вывода применяются значительно более сложные средства. В этом разделе мы познакомимся с растровыми шрифтами. Другие категории шрифтов рассматриваются в разделах «Векторные шрифты» и «Шрифты 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). Ресурс растровой шрифта поддерживает только один однобайтовый набор символов. Он содержиглифы всех символов из интервала, заданного полями Fi rstChar и LastChar. В каж дом шрифтовом ресурсе определяется символ по умолчанию, используемый npi выводе символов, не принадлежащих поддерживаемому интервалу (поле 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.00 // Размер всего ресурса // Для растровых шрифтов Туре & 1 == О // Номинальный размер в пунктах // Номинальное вертикальное разрешение // Номинальное горизонтальное разрешение
// 0 для пропорционального шрифта // Семейство // Ширина символа 'х' // Максимальная ширина // Первый символ, определенный в шрифте // Последний символ, определенный в шрифте // Замена для символов, не входящих в интервал // Количество байт на строку растра // Смещение строки с именем устройства // Смещение строки с именем гарнитуры // Адрес загруженного растра // Смещение графических данных
760
Глава 14. Шрифты
BYTE Reserved: } FontHeader20:
// 1 байт, не используется
После заголовка ресурса шрифта следует таблица символов (вернее, таблица глифов). Для растровых шрифтов версии 2.0 каждому символу из поддерживаемого интервала в таблице символов соответствует два 16-разрядных целых: для ширины и для смещения глифа. В этом проявляется серьезный недостаток архитектуры шрифтовых ресурсов версии 2.00: из-за 16-разрядного смещения объем ресурса ограничивается 64 килобайтами. Таблица символов содержит (LastCharFf rstChar+2) элементов. Лишний элемент остается пустым. typedef struct {
}
SHORT Glwidth; SHORT Gloffset; GLYPHINFO_20;
Версия 2.00 поддерживала только монохромные глифы. Хотя версия 3.00 рассчитана на поддержку глифов с 16 и 256 цветами и даже глифов в формате True Color, на практике такие шрифты не встречаются. В монохромных глифах для представления одного пиксела достаточно одного бита. С другой стороны, порядок этих битов в глифах не имеет ничего общего с теми растровыми форматами, о которых говорилось выше. Первый байт глифа содержит первые 8 пикселов первой строки развертки, второй байт — первые 8 пикселов второй строки развертки и т. д. до завершения первого столбца из 8 пикселов. Затем следуют данные второго столбца из 8 пикселов, третьего столбца и т. д. до полной ширины глифа. Подобная структура когда-то считалась стандартным элементом оптимизации, ускоряющим вывод символов. Ниже приведена функция для вывода одного глифа растрового шрифта. Функция находит таблицу GLYPHINFO после заголовка, вычисляет индекс глифа в таблице, а затем преобразует глиф в монохромный DIB-растр и выводит его функциями, предназначенными для работы с DIB. int CharOutCHDC hDC. int x, int y. int ch, KFontHeader20 * pH, int sx=l. int sy=l) GLYPHINFO_20 * pGlyph BitsOffset + 5):
(GLYPHINFO_20 *) ( (BYTE *) & pH->
if ( (chFirstChar) || (ch>pH->LastChar) ) ch = pH->Defau1tChar: 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].Gloffset: for (int i=0: i<(width+7)/8: i++) for (int j=0: j
" B b I
« S % 8r ' ( ) x + . • . / 0 1 2 3 4 5 6 7 8 9 : : C D E F G H I J K L M N O P Q R S T U V W X Y Z c d e f g h i j kl m n o p q r s t u v w x y z i I I I I I I I !I I I I M I I I I I I I I I I
< = [ \ ] l } I I
> ? " _ ~I I I
i с i » * : §" ® « - - ® • ±' > - * i • . • i » и я и i
А А А А ' А А Я С Ё Ё Ё Ё ! 1 I I DSUUflOdxaOrjOUYfrB a a a a a l s c e e i e i i i i 3 f i 6 6 6 6 b - > - e u u u i i i > b v 10 pts, 96x96 dpi, 0x16 pixel, avgw 6, maxw 14. charset 0
! ©A a I I
" # J %&' ( ) BCDEFGHI b c d e f ghi I I I I I I I I
* J j I
+ , - . / KLMNO kl m n o I I I I I
0 P p I
1 I QR q t ' '
3 S s I
4 T t I
5 6 7 8 9 : ; < UVWXYZ[ \ u v w x y z f | I I I I I I I I
I I
§ " © i А А А А А А Д д Ё Ё Ё Ё ! i I T D N U 6 6 6 6 * 0 U U O u t P G a a a a a a e e i j e e e e i i i ' i & n 6 6 6 6 b ^ a > u u u u y b y
Рис. 14.10. Глифы в растровом формате
Из приведенного примера становится ясно, что же такое шрифт. Растровый шрифт в формате Windows 2.00 представляет собой набор шрифтовых ресурсов, разработанных для разных кеглей. Каждый шрифтовой ресурс состоит из монохромных растровых глифов, однозначно отображаемых на символы заданного однобайтового набора. Растровые шрифты поддерживают простое отображение символов набора в индексы глифов в интервале поддерживаемых символов. Глифы легко конвертируются в растровые форматы, поддерживаемые на уровне GDI, и выводятся на графических устройствах. В растровых шрифтах также хранятся простейшие текстовые метрики.
762
Глава 14. Шрифты
Растровые шрифты хорошо подходят (как по качеству, так и по быстродействию) для вывода небольших символов на экран; в этом и состоит одна из причин, по которой они еще существуют. Для разных кеглей растровый шрифт должен содержать разные шрифтовые ресурсы. Например, растровые шрифты Windows обычно содержат ресурсы для кеглей 8, 10, 12, 14, 18 и 24 пункта. Для других кеглей или устройств с другим разрешением глифы приходится масштабировать по нужным размерам. Масштабирование растров всегда порождает проблемы, поскольку увеличение приводит к появлению новых пикселов. На рис. 14.11 пока•зан результат масштабирования глифа растрового шрифта.
763
Векторные шрифты
<векторный_глиф> <отрезок> <маркер> <отн_смещение>
::= <отрезок> { <отрезок> } ::= <маркер> { <отн_смещение> <отн_смещение> } ::- 0x80 ::- <знаковый_байт>
Располагая этой информацией, мы можем написать собственную функцию вывода глифов векторных шрифтов средствами GDI: int VectorCharOut(HDC hDC. int x. int y. int ch. const KFontHeader20 * pH. int sx-1. int sy-1) typedef struct { short offset; short width: } VectorGlyph; const VectorGlyph * pGlyph = (const VectorGlyph *) ( (BYTE *) & pH->BitsOffset + 4); if ( (chFirstChar) || (ch>pH->LastChar) ) ch = pH->DefaultChar; else Ch .= pH->FirstChar;
Рис. 14.11. Масштабирование глифа растрового шрифта
В этом примере по обеим осям выполняется целочисленное масштабирование, то есть каждый пиксел глифа просто дублируется нужное количество раз. На рисунке четко видны возникающие дефекты. Масштабирование с дробным коэффициентом может привести к появлению черт разной толщины, поскольку одни пикселы будут дублироваться п раз, а другие — п + 1 раз. Конечно, масштабирование растровых шрифтов не позволяет добиться хорошего качества при выводе на экран и печати. Приходится искать другие способы кодировки шрифтов, обеспечивающие плавное масштабирование без дефектов.
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 )
Векторные шрифты В растровых шрифтах глифы представляются растровыми изображениями и потому не могут нормально масштабироваться до больших размеров. В другом простом способе представления глиф описывается последовательностью линейных отрезков, которые затем рисуются при помощи пера. Такие шрифты называются векторными. В системе Windows векторные шрифты используют тот же формат шрифтовых ресурсов .fnt и структуру заголовка шрифта. В современных векторных шрифтах поле Version структуры FontHeader20 равно 0x100, а поле Туре равно 1. Главные различия между растровым и векторным шрифтами заключаются в формате данных глифа. В векторном шрифте каждый глиф описывается серией координат, начиная с точки (0,0). При небольших размерах сетки для хранения одной точки достаточно двух байт со знаком. Специальный маркер 0x80 сообщает о начале нового отрезка. Выражаясь более формально, описание векторного глифа в синтаксисе BNF выглядит следующим образом:
move = true; pStroke++: length --:
} if ( (pStroke[0]==0) && (pStroke[l]~0) && (pStroke[2]==0) ) break: dx += pStroke[0]: dy += pStroke[l]: if ( move ) MoveToExChDC. 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, maxw 33, charset Z55
11
#$%&' ( ) * + , - . /
2 3 ^ 5 6 7 8 Я : ; < = > ?
(P
ев с JB £ з ь м$ i к i тп б> && z uv wxy $ [ \ ] - _ f
u - o a L & l o s f b i p
t
О
Ш-
OC>
R,
о
-ггь гь о
f
Рис. 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 checksum: ULONG offset: ULONG length: TableEntry:
typedef struct { sfntversion: // 0x10000 для версии 1.0 Fixed USHORT numTables: USHORT searchRange: USHORT entrySelector: USHORT rangeShift: Tableentry entries[l]: // Переменное количество TableEntry TableDirectory: Многие программисты Windows даже не знают о том, что шрифты TrueType первоначально разрабатывались компанией Apple для операционных систем, работающих на процессорах Motorola вместо Intel. Во всех данных шрифтов TrueType используется кодировка, при которой старший байт стоит на первом месте. Если шрифт TrueType начинается с 00 01 00 00 00 17, мы знаем, что перед нами ресурс контурного шрифта («sfnt») в формате версии 1.0 с 23 таблицами. В последнем поле структуры TableDirectory хранится массив структур TableEntry переменной длины, по одной структуре для каждой таблицы в шрифте. Каждая таблица шрифта TrueType содержит логически обособленную информацию — например, данные глифа, отображение символов на глифы, данные кернинга и т. д. Одни таблицы необходимы, присутствие других не обязательно. В табл. 14.3 перечислены самые распространенные таблицы, встречающиеся в шрифтах TrueType. Таблица 14.3. Основные таблицы шрифтов TrueType
Тег
head стар glyf
Название Заголовок шрифта Таблица соответствия между кодами символов и глифами
Описание Глобальная информация о шрифте Отображение кодов символов в индексы глифов
Таблица глифов
Определение контура глифа и инструкции по его размещению в сетке
тахр Максимальный профиль
Сводные данные шрифта для выделения памяти
mmtx
Горизонтальные метрики
Горизонтальные метрики глифа
loca
Индексная таблица
Преобразование индекса глифа в смещение данных в таблице глифов
name
Таблица имен
Информация об авторских правах, имя шрифта, имя семейства, стиль и т. д.
hhea
Горизонтальная структура
Горизонтальная структура глифов: надстрочный интервал, подстрочный интервал и т. д.
767
Шрифты TrueType
Тег
Название
Описание
hmtx
Горизонтальные метрики
Полная ширина и левый отступ
kern
Таблица кернинга
Массив кернинговых пар
post
Данные PostScript
Элемент таблицы PostScript Fontlnfo и имена PostScript для всех глифов
PCLT
Данные PCL 5
Данные шрифта для языка принтеров HP PCL 5: номер шрифта, шаг, стиль и т. д.
OS/2
Метрики, специфические для OS/2 и Windows
Обязательный набор метрик для шрифта TrueType
Все структуры TableEntry в структуре TableDirectory должны быть отсортированы по именам тегов. Например, структура «стар» должна предшествовать «head», а последняя, в свою очередь, должна располагаться перед структурой «glyf». Расположение самих таблиц в файле шрифта TrueType может быть произвольным. В Win32 API существует функция, при помощи которой приложение может запросить данные шрифта TrueType: DWORD GetFontData(HOC 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 if ( pFont==NULL ) return NULL:
(TableDirectory *) new BYTE[nFontSize];
GetFontData(hDC. 0. 0. pFont. nFontSize): return pFont:
768
Глава 14. Шрифты
Функция GetFontData ориентирована на приложения, внедряющие шрифты TrueType в свои документы, чтобы их можно было прочитать на другом компьютере, где данный шрифт может отсутствовать. Предполагается, что приложение запрашивает данные шрифта, сохраняет их в составе документа и устанавливает шрифт при открытии документа. В результате документ выглядит так же, как и на том компьютере, где он был создан. Например, спулер Windows NT/ 2000 при печати на сервере внедряет шрифты TrueType в спулинговые файлы, чтобы документ был правильно напечатан на другом компьютере. После получения служебных данных шрифта TrueType анализ заголовочной структуры TableDi rectory не вызовет никаких проблем. Достаточно проверить версию и количество таблиц, после чего можно переходить к проверке отдельных таблиц. Мы рассмотрим самые важные и интересные таблицы.
Заголовок шрифта Заголовок шрифта (таблица «head») содержит глобальную информацию о шрифте TrueType. Определение структуры заголовка приведено ниже. typedef struct // 0x00010000 для версии 1.0 Fixed Table: Fixed fontRevision; // Задается разработчиком шрифта ULONG checkSumAdjustment; ULONG magicNumber; // Равно Ox5FOF3CF5 USHORT unitsPerEm: // Интервал допустимых значений 16..16384 longDT created: // Дата в международном формате (8 бит) longDT modified; // Дата в международном формате (8 бит) FWord xMin: // Для всех ограничивающих блоков глифов FWord yMin; // Для всех ограничивающих блоков глифов FWord xMax; // Для всех ограничивающих блоков глифов FWord yMax; // Для всех ограничивающих блоков глифов USHORT macStyle: USHORT lowestRecPPEM: // Минимальный читаемый размер в пикселах SHORT fontDirectionHint: SHORT indexToLocFormat; // 0 - короткое смещение, 1 - длинное SHORT glyphDataFormat: // 0 для текущего формата Table head: История шрифта (номер версии, даты создания и последней модификации) хранится в трех полях. Даты хранятся в 8-байтовых полях в виде количества секунд, прошедших с полуночи 1 января 1904 года, поэтому нам никогда не придется беспокоиться о «проблеме Y2K» (и даже «проблеме Y2M»). Шрифт конструируется на эталонной сетке, называемой em-квадратом; глифы шрифта описываются координатами этой сетки. Следовательно, размер эталонной сетки влияет на масштабирование шрифта и его качество. В заголовке шрифта хранятся размеры em-квадрата и данные ограничивающих блоков всех глифов. Размеры em-квадрата могут лежать в интервале от 16 до 16 384, хотя обычно используются значения 2048, 4096 и 8192. Например, для шрифта Wingding размер em-квадрата равен 2048, а ограничивающий блок глифа описывается четверкой [0,-432, 2783,1841].
Шрифты TrueType
769
Среди других данных в таблице заголовка шрифта хранится минимальный читаемый размер шрифта в пикселах, хинт направления шрифта, индекс глифа в формате индексной таблицы, формат данных глифа и т. д.
Максимальный профиль
Шрифт TrueType обладает весьма динамичной структурой. Он может содержать переменное количество глифов, описываемых разным количеством контрольных точек, и неизвестное количество инструкций. Таблица максимального профиля (таблица «тахр») содержит данные о затратах памяти на растеризацию шрифтов, чтобы перед использованием шрифта можно было выделить достаточный объем памяти. Поскольку при растеризации шрифтов важнейшим фактором является быстродействие, динамические структуры вроде массива САггау MFC, нуждающиеся в частом копировании данных, для этого не подходят. Ниже приведена структура, описывающая максимальный профиль шрифта. typedef struct
Fixed Version: // 0x00010000 для версии 1.0 USHORT numGlyphs; // Количество глифов в шрифте // Макс.кол-во точек в простом глифе USHORT maxPoints; // Макс.кол-во контуров в простом глифе USHORT maxContOlirs; // Макс.кол-во точек в составном глифе USHORT maxCompositePoints: USHORT maxCompositeContours; // Макс.кол-во контуров в составном глифе USHORT maxZones: USHORT maxTwilightPoints: // Количество блоков хранения данных USHORT maxStorage; // Количество FDEF USHORT maxFunctionDefs: // Количество IDEF USHORT maxInstructionDefs: // Максимальная глубина стека USHORT maxStackElements: USHORT maxSizeOfInstructions: // Макс.байт в инструкциях глифа USHORT maxComponentElements: // Макс.кол-во компонентов верхнего уровня // Максимальная глубина рекурсии USHORT maxComponentDepth: } Tablejnaxp: В поле numGlyphs хранится общее количество глифов в шрифте, определяющее размер индекса глифа в индексной таблице, а также используемое для проверки индексов. Глифы шрифтов TrueType делятся на составные (composite) v простые (noncomposite). Простой глиф состоит из одного или нескольких контуров, каждый из которых определяется несколькими контрольными точками Составной глиф определяется как результат объединения других глифов. В по лях maxPoints, maxContours, maxCompositePoints и maxCompositeContours хранятся дан ные о сложности определений глифов. Помимо определений глифов, в шрифтах TrueType используются инструк ции для растеризации шрифтов. Инструкции регулируют положение контроль ных точек, чтобы растеризованные глифы были сбалансированными и хорошс выглядели. Инструкции глифов также могут храниться на глобальном уровш в программной таблице шрифта («fpgm») и в программной таблице контроль ных величин («prep»). Инструкции глифов TrueType пишутся на байт-коде сте ковой псевдомашины (вроде виртуальной машины Java). Поля maxStackElement:
770
Глава 14. Шрифты
и maxSizeOfInstructions сообщают стековой машине степень сложности этих инструкций. Пример: шрифт Wingding содержит 226 глифов, максимальное количество контуров в глифе равно 47, а максимальное количество точек в простом глифе равно 268. Составные глифы содержат до 141 точки и 14 контуров. В худшем случае для вывода потребуется 492 уровня в стеке, а самая длинная инструкция состоит из 1119 байт.
Отображение символов в индексы глифов Таблица отображения символов в глифы (таблица «стар») определяет соответствие между символами разных кодовых страниц и индексом глифа — ключевой характеристикой для получения информации о глифе в шрифте TrueType. Таблица «стар» может состоять из нескольких подтаблиц для поддержки разных платформ и кодировок символов. Ниже приведено описание структуры таблицы «стар».
typedef struct USHORT Platform: // Идентификатор платформы USHORT EncodingID; // Идентификатор кодировки 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: Таблица «стар» (структура Table_cmap) начинается с номера версии, количества подтаблиц и заголовков всех подтаблиц. Каждая подтаблица (структура submap) содержит идентификатор платформы, идентификатор кодировки и смещение данных подтаблицы для заданной платформы и кодировки. В операционных системах Microsoft идентификатор платформы равен 3, а рекомендуемый идентификатор кодировки равен 1 (Unicode). Существуют и другие идентификаторы кодировок — 0 для кодировки Symbol, 2 для Shift-JIS (Japanese Industrial Standard), 3 для Big5 (китайский, традиционное письмо), 4 для PRC (китайский, упрощенное письмо) и т. д. Собственно таблица кодировки (Tab! e_Encode) начинается с полей формата, длины и версии, за которыми следуют данные отображения. В настоящее время определены четыре разных формата таблицы. Формат 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 ft Accel : DWORD cGlyphsSupported: DWORD cRanges: WCRANGE ranges[l]; // ranges [cRanges]; } GLYPHSET;
DWORD GetFontUnicodeRanges(HDC hDC. LPGLYPHSET Ipgs): DWORD GetGlyphlndicesCHOC hDC. LPCTSTR Ipstr. int c. LPWORD pgi . DWORD f 1 ) : Обычно шрифт содержит глифы лишь для некоторого подмножества символов кодировки Unicode, причем эти символы могут группироваться по интервалам. Функция GetFontllnicodeRanges заносит в структуру GLYPHSET количество поддерживаемых глифов, количество интервалов Unicode и дополнительную информацию об интервалах для текущего шрифта, выбранного в контексте устройства. Структура GLYPHSET имеет переменный размер, зависящий от количества поддерживаемых интервалов Unicode, поэтому функция GetFontUnicodeRanges (как и другие функции Win32 API, поддерживающие структуры переменного размера) обычно вызывается дважды. При первом вызове в последнем параметре передается указатель NULL; GDI возвращает фактический размер структуры Вызывающая сторона выделяет блок нужного размера и снова вызывает функцию для получения данных. В обоих случаях функция GetFontUni codeRanges возвращает размер блока, необходимого для хранения всей структуры. В 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->ct>This = size: size - GetFontUnicodeRanges(hDC. pGlyphSet): return pGlyphSet:
} Если вызвать функцию GetFontUnicodeRanges для некоторых шрифтов TrueType системы Windows, выясняется, что эти шрифты часто поддерживают свыше тысячи глифов, сгруппированных по сотням интервалов Unicode. Например, шрифт Times New Roman содержит 1143 глифа в 145 интервалах, первым из которых является интервал 7-разрядных печатных ASCII-кодов Ox20..0x7F. Функция GetFontUnicodeRanges использует лишь часть данных о шрифте TrueType, хранящихся в таблице «стар», — а именно общие сведения об отображении символов Unicode в индексы глифов. Функция GetGlyphIndices выполняет непосредственное преобразование текстовой строки в массив индексов глифов. Она получает манипулятор контекста устройства, указатель на строку, длину строки, указатель на массив WORD и флаг. В массиве WORD сохраняются сгенерированные индексы глифов. Если флаг равен CGI_MASK_NONEXISTING_GLYPHS, отсутствующие символы заменяются индексом 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 хМах; // Максимальное значение х для координат Fword yMax: • // Максимальное значение у для координат } GlyphHeader: Для простых (не составных) глифов поле numberOfContours содержит количество контуров в текущем глифе; для составных глифов поле numberOfContours отрицательно. В последнем случае общее количество контуров вычисляется по данным всех глифов, образующих составной глиф. В следующих четырех полях структуры GlyphHeader хранится ограничивающий блок глифа. Для простых глифов за структурой GlyphHeader следует описание глифа. Описание состоит из нескольких значений: индексов конечных точек всех контуров, инструкций и последовательности контрольных точек. Для каждой контрольной точки указываются координаты (х,у) и флаг. Теоретически для задания контрольных точек достаточно той же информации, что и для функции PolyDraw GDI: массива флагов и массива координат. Впрочем, в шрифтах TrueType контрольные точки кодируются весьма изощренным способом. Ниже приведено обобщенное описание глифа. USHORT endPtsOfContours[n]; // п = количество контуров USHORT instructionlength; BYTE instruction[i]: // i = длина инструкции BYTE flags[]: // переменный размер BYTE xCoordinates[]; // переменный размер BYTE yCoordinates[]: // переменный размер Глиф может содержать один или несколько контуров. Например, буква «о» содержит два контура, внутренний и наружный. Для каждого контура в массиве endPtsOfContours хранится индекс конечной точки, по которому вычисляется количество точек в контуре. Например, endPtsOfContours[0] — количество точек в первом контуре, a (endPtsOfContours[l] - endPtsOfContours[0]) — количество точек во втором контуре. За массивом конечных точек следует длина инструкции глифа и массив инструкций. Впрочем, мы сначала разберемся с контрольными точками. Контрольные точки глифа хранятся в трех массивах: флагов, координат х и координат у. Начало массива флагов находится просто, однако не существует ни поля разме-
774
Глава 14. Шрифты
pa массива флагов, ни прямых ссылок на два других массива. Чтобы найти массивы координат и разобраться с ними, вам придется декодировать массив флагов. Выше уже упоминалось, что максимальный размер em-квадрата равен 16 384 единицам, поэтому обычно каждая из координат х и у представляется двумя байтами. Для экономии места (а это главная причина для выбора этого способа кодировки) в описании глифа хранятся относительные координаты. Координаты первой точки задаются относительно (0,0); для всех остальных точек хранятся смещения относительно предыдущей точки. У одних точек эти смещения оказываются достаточно малыми, что позволяет представить их одним байтом; у других точек они равны 0, а у третьих они не помещаются в одном байте. В массиве флагов хранится информация о кодировке отдельных точек в сочетании с другой информацией. Ниже показано, как интерпретируются отдельные биты флагов. typedef enum {
G ONCURVE G_REPEAT
= 0x01. = 0x08.
G XMASK G XADDBYTE G XSUBBYTE G XSAME GJfADDINT
= = = =
G G G G G
• 0x24. = 0x24. • 0x04, = 0x20. = 0x00.
YMASK YADDBYTE YSUBBYTE YSAME YADDINT
0x12, 0x12. 0x02. 0x10. 0x00.
// На кривой или вне кривой // Следующий байт содержит счетчик повторений
775
Шрифты TrueType
Описание глифа расшифровывается за два прохода. На первом проходе мы перебираем массив флагов, находим его конец и определяем длину массива координат т, в результате определяются начальные точки массивов х и у. На втором проходе мы перебираем каждую точку определения глифа и преобразуем ее к более удобному формату. В листинге 14.2 приведена функция расшифровки глифа TrueType, оформленная в виде метода класса TrueType. Листинг 14.2. «TrueType::DecodeGlyph: расшифровка простого глифа int KTrueType::DecodeGlyph(int Index. KCurve & curve. XFORM * xm) const const GlyphHeader * pHeader - GetGlyph(index): if ( pHeader==NULL ) return 0; intnContour = (short) reverse(pHeader->numberOfContours): if ( nContour<0 )
// X - положительный байт
// X - отрицательный байт // X совпадает с прежним значением // X - слово со знаком // // // //
Y Y Y Y
- положительный байт - отрицательный байт совпадает с прежним значением - слово со знаком
В главе 8, посвященной линиям и кривым, упоминалось, что сегмент кубической кривой Безье определяется четырьмя точками: начальной точкой кривой, двумя контрольными точками, лежащими за пределами кривой, и конечной точкой кривой. Контур глифа в шрифте TrueType описывается кривой Безье второго порядка, определяемой двумя концами кривой и одной контрольной точкой. Несколько контрольных точек могут стоять подряд — не для того, чтобы определить кубическую или другую кривую Безье более высокого порядка, а просто для сокращения количества контрольных точек. Например, в последовательность четырех точек «точка кривой — контрольная — контрольная — точка кривой» между двумя контрольными точками неявно добавляется еще одна точка кривой, в результате чего данная последовательность определяет два сегмента кривых Безье второго порядка. Если бит G_ONCURVE установлен, точка находится на кривой; в противном случае она является контрольной точкой, лежащей за пределами кривой. Если установлен бит G_REPEAT, следующий байт массива флагов содержит счетчик повторений, а текущий флаг повторяется заданное количество раз (некая разновидность сжатия RLE в массиве флагов). Остальные биты флагов описывают кодировку соответствующих координат х,у; они показывают, совпадает ли относительная координата с предыдущей, кодируется ли положительным или отрицательным байтом или же требует двухбайтовой величины со знаком.
// Составной глиф
return DecodeCompositeGlyph(pHeader+l. curve): // После заголовка
if ( nContour==0 ) return 0: curve. SetBound(reverse((WORD)pHeader->xMin). reverse((WORD)pHeader->yHin), 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
case 6_XADDBYTE: case G XSUBBYTE:
ft „ i;
un
Продолжение
776
Глава 14. Шрифты
Листинг 14.2. Продолжение break: case G_XADDINT: 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
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 & G_YMASK )
777
Шрифты TrueType
case G_YADDBYTE: dy = PY[Q]- PY ++• break: case GJSUBBYTE: dy = - PY[Q]: PY ++; break: case GJADDINT: dy - (short )( CpY[0] « 8) + pY[l]): 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 & GJMXIRVE) ? KCurve: :FLAG_ON : 0): else curve.Add(x, y, (flag & G_ONCURVE) ? KCurve::FLAG_ON : 0):
rep --: i ++:
curve.CloseO: return curve.GetLengthO:
Класс KTrueType загружает и расшифровывает шрифты TrueType; его полный код находится на прилагаемом компакт-диске. Метод DecodeGlyph выполняет расшифровку одного глифа по индексу и необязательной матрице преобразования, Параметр класса KCurve предназначен для сбора определения глифа в простой 32-разрядный массив точек и простой массив флагов, которые затем легко выводятся средствами GDI. На основе этого метода даже можно построить простейший редактор шрифтов TrueType. Программа вызывает метод GetGlyph, который по индексной таблице находит структуру GlyphHeader заданного глифа. Из таблицы извлекается количество контуров в глифе. Обратите внимание на перестановку байтов в полученной величине, связанную с обратным порядком следования байтов в шрифтах TrueType, Если значение отрицательно (признак составного глифа), вызывается метод DecodeCompositeGlyph. Затем программа находит массив endPtsOfContours, определяет общее количество точек и пропускает инструкции, переходя к началу массива флагов. Теперь мы должны определить начальную точку массива координат х и длину массива однократным перебором массива флагов. Каждая точка может занимать в массиве координат от 0 до 2 байт в зависимости от того, представляется ли ее относительное смещение 0, одно- или двухбайтовой величиной. По адресу и длине массива координат х определяется адрес массива координат у. Затем программа последовательно перебирает все контуры, расшифровывает данные всех точек, преобразует относительные координаты в абсолютные и затем прибавляет точку к объекту кривой, применяя к ней преобразование (если оно было задано).
778
Глава 14. Шрифты
Как говорилось выше, в шрифтах TrueType используются кривые Безье второго порядка, причем между двумя точками кривой может находиться несколько контрольных точек. Чтобы упростить алгоритм вывода кривой, метод KCurve: :Add добавляет лишнюю точку кривой между каждой парой контрольных точек.
signed short argumentl: signed short argument2; if ( flags & ARG_1_AND_2_ARE_WORDS )
argumentl = Str.GetWordO: // (SHORT or FWord) argumentl: argument2 = str.GetWordO: // (SHORT or FWord) argument2:
void AdcKint x. int у. BYTE flag) {
.
if ( m_len && ( (flag & FLAG_ON)==0 ) && ( (m_Flag[mJen-l] & FLAG_ON)==0 ) ) {
} }
Разобравшись с простыми глифами, перейдем к составным. Составной глиф определяется последовательностью преобразованных глифов. Каждое определение преобразованного глифа состоит из трех частей: флагов, индекса глифа и матрицы преобразования. Поле флагов описывает кодировку матрицы преобразования (также спроектированную для экономии памяти), а также содержит признак конца последовательности. Полное двумерное аффинное преобразование определяется шестью величинами. Впрочем, для простого смещения достаточно всего двух величин (dx, dy), которые могут храниться в двух байтах или двух словах. Если одновременно со смещением значения х и у масштабируются в одинаковой пропорции, коэффициент масштабирования можно хранить всего в одном экземпляре. В общем случае используются все шесть величин, но в большинстве конкретных ситуаций несколько байт удается сэкономить. Параметры преобразования хранятся в формате 2.14 с фиксированной точкой; исключением являются параметры dx и dy, хранящиеся в виде целых чисел. Составной глиф строится объединением нескольких глифов, каждому из которых сопоставляется матрица преобразования. Например, если глиф представляет собой точное зеркальное отражение другого глифа, он может быть определен как составной глиф, сгенерированный в результате применения зеркального отражения к другому глифу. В листинге 14.3 приведен код расшифровки составных глифов. Листинг 14.3. KTrueType::DecodeCompositeGlyph
int KTrueType::DecodeCompositeGlyph(const void * pGlyph. KCurve & curve) const
{
else argumentl = (signed char) str.GetByteO; argument2 - (signed char) str.GetByteO:
Append((m_Point[m_len-l].x+x)/2. (m_Point[m_1en-l].y+y)/2. FLA6_ON | FI_AG_EXTRA): // Добавить промежуточную точку Append(x. у, flag):
KDataStream str(pGlyph); unsigned flags:
int len - 0:
779
Шрифты TrueType
signed short xscale, yscale. scaled . scalelO: xscale yscale scaleOl scalelO
= = = =
1: 1; 0: 0:
if ( flags & WE_HAVE_A_SCAIE ) {
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_TWO_BY_TWO ) {
xscale scaleOl scalelO yscale
- str.GetWordO; = str.GetWordO; = str.GetWordO: = str.GetWordO;
if ( flags & ARGS_ARE_XY_VALUES ) { XFORM xm: xm.eDx xm.eDy xm.eMll xm.eM12 xm.eM21 xm.eM22
-
(float) (float) xscale scaleOl scalelO yscale
argumentl: argument2; / (float) 16384.0: / (float) 16384.0: / (float) 16384.0; / (float) 16384.0;
len +- DecodeGlyph(glyphIndex. curve. & xm);
do { flags
- str.GetWordO:
unsigned glyphlndex - str.GetWordO:
else assert(false): Продолжение
780
Глава 14. Шрифты
781
Шрифты TrueType
Листинг 14.3. Продолжение while ( flags & MORE_COMPONENTS ): if ( flags & WE_HAVE_INSTRUCTIONS ) //.Пропустить инструкции unsigned numlnstr = str.GetWordO: for (unsigned 1=0; i
return Ten;
}
Метод DecodeCompositeGlyph получает флаги, индекс глифа и матрицу преобразования для каждого глифа, входящего в составной глиф, и расшифровывает глиф при помощи метода DecodeGlyph. Обратите внимание на передачу матрицы преобразования при вызове DecodeGlyph. Метод завершает работу, обнаружив сброшенный флаг MORE_COMPONENTS. Полный код находится на прилагаемом компактдиске. Расшифрованные глифы шрифтов 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 (pO+2pl)/3 (2pl+p2)/3. p2 POINT P[3] = xO
{ (xO+2*xl)/3, (yO+2*yl)/3 { (2*xl+x2)/3. (2*yl+y2)/3 }, { x2. y2 } }; X2; yO = y2;
return PolyBezierTo(hDC.P.3); Для кривой Безье второго порядка, определяемой тремя точками (р0, р„ р2), соответствующие точки кубической кривой Безье вычисляются по формулам (Ро, (Ро +2 хPl)/3, (2 хPl + р2)/3, р2). На рис. 14.14 показан результат применения кода, реализованного в классе KCurve. На заднем плане изображен em-квадрат, разделенный сеткой на 16 частей по обеим осям. Прямоугольник представляет ограничивающий блок глифа, в данном примере — символа @. Точки обозначены маленькими кружками. Как видно из рисунка, точки на линии чередуются с контрольными точками. Но самое важное — то, что построенные кривые соответствуют контуру, определяемому сложным описанием шрифта.
Рис. 14.14. Описание глифа TrueType
Инструкции глифа При просмотре листингов 14.2 и 14.3 может возникнуть впечатление, что растеризатор шрифтов TrueType легко реализуется преобразованием контуров глифов — скажем, заполнением траектории, которая создается при выводе контуров, функцией GDI StrokeFillAndPath. Такой примитивный растеризатор шрифтов вряд ли принесет какую-нибудь практическую пользу, разве что на устройствах высокого разрешения (например, на принтерах). Рисунок 14.15 поможет вам убедиться в этом.
ABC
ABC
Рис. 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-квадрата в итоговую сетку, чтобы результат лучше выглядел и сохранял сходство с исходным дизайном глифа. Эта методика, называемая подгонкой по сетке, имеет три основные цели.
783
Шрифты TrueType
раметры из контрольной таблицы в инструкциях глифов гарантируют, что эти параметры будут выдерживаться во всех глифах. Инструкции глифов предназначены для стековой псевдомашины. Стековая машина широко используется в интерпретируемых средах из-за простоты своей реализации. В частности, Forth (простой и мощный язык встраиваемых систем), RPL (язык научных калькуляторов HP) и виртуальная машина Java построены на базе стековых машин. Стековая машина обычно не имеет регистров, поскольку все вычисления производятся в стеке (у некоторых стековых машин контрольный стек отделен от стека данных). Например, инструкция PUSH заносит значение в стек, инструкция POP удаляет из стека верхний элемент, а инструкция бинарного сложения удаляет из стека два верхних элемента и заносит в стек их сумму. Виртуальная машина TrueType не относится к числу стековых машин общего назначения. Это специализированная псевдомашина, предназначенная для единственной цели — подгонки контуров глифов по сетке. Кроме значений из таблицы контрольных величин, она использует несколько переменных графического состояния (эталонная точка 0, эталонная точка 1, вектор проекции и т. д.). Мы не будем рассматривать весь набор инструкций глифов TrueType. Вместо этого базовые принципы будут продемонстрированы на простом примере буквы «Н» из шрифта Tahoma. Контур глифа изображен на рис. 14.16. I 1 [ 1 ( i i •
О Устранение случайных зависимостей от расположения контуров в сетке, чтобы при растеризации одинаковая толщина линий сохранялась независимо от их расположения в сетке.
L I I I :<
О Сохранение ключевых размеров внутри одного глифа и между разными глифами.
*6i— ]
[7 •
О Сохранение симметрии и других важных аспектов глифа (например, засечек). Соответствующие требования для шрифта TrueType кодируются в двух местах: в таблице контрольных величин и в инструкциях подгонки по сетке, задаваемых на уровне отдельных глифов. Таблица контрольных величин («cvt») предназначена для хранения массива, элементы которого могут использоваться в инструкциях. Например, для шрифта с засечками в числе контролируемых параметров могут быть высота засечки, ширина засечки, толщина черт прописной буквы и т. д. Эти значения заносятся в таблицу контрольных величин в порядке, известном разработчику шрифта, и позднее инструкции ссылаются на них по индексам. В процессе растеризации шрифтов значения таблицы контрольных величин масштабируются в соответствии с текущим кеглем. Использование масштабированных величин в инструкциях гарантирует, что будут применяться одни и те же значения независимо от их относительной позиции в сетке. Например, если горизонтальную черту [14,0,25,200] задать в виде [14,0,14+CVT[stem_width],0+CVT[stem_height]] с использованием двух значений из таблицы CVT, то ширина и высота останутся постоянными при любом расположении линии в сетке. С каждым определением глифа связывается серия инструкций, называемых инструкциями глифа и управляющих подгонкой глифа по сетке. Ссылки на па-
';
•10J-] 11
1 ..
....... ....
i
i I ' i
i
i
'
1..4...L.L4-
-..i.. !
•3
1
;
•
!
2
i i i i i
Рис. 14.16. Контур буквы «Н» шрифта Tahoma
Буква Н шрифта Tahoma состоит из одного контура с 12 контрольными точками, которые все расположены на линии; другими словами, в данном глифе кривые Безье отсутствуют. Помимо точек глиф содержит 50 байт инструкций, которые занимают больше места, чем координаты. Ниже приведен список координат и инструкций глифа. Координаты
0: 1:
1232. 1034.
О О
784
Глава 14. Шрифты
2: 1034, 3: 349, 349, 4: 151. 5: 151. 6: 7: 349, 349. 8: 9: 1034. 10: 1034. 11: 1232,
729 729 0 0 1489 1489
4. Инструкция MIRP[srpO,md,rd,l] (относительное перемещение эталонной точки) извлекает из стека значения 20 и 3 и перемещает точку 3 относительно точки 5 в соответствии со значением CVT[20]. Тем самым обеспечивается фиксированная ширина горизонтальной черты. 5. Инструкция SHP[rp2,zpl] (сдвинуть точку по эталонной точке) извлекает из стека значение 8 и сдвигает точку 8 на то же расстояние, на которое была сдвинута эталонная точка (точка 3).
50
00: NPUSHB (28):
3 53 8 8 9 2 20 13 64 13 2 100 12
5 10 101 8 3
8
0
30: SRPO 31: MIRP[srp0.nmd.rd, 2] 32: MIRP[srpO,md,rd,l] 33: SHP[rp2.zpl] 34: DELTAP1 35: SRPO
36: 37: 38: 39: 40:
MIRP[srpO,nmd.rd,2] MIRPCsrpO.md.rd.l] SHP[rp2.zpl] SVTCA[y-axis] MIAP[rd+ci]
41: ALIGNRP
42: MIAP[rd+ci] 43: ALIGNRP
44: SRP2 45: IP
46: 47: 48: 49:
785
контрольных величин с индексом 100). Эта инструкция привязывает крайнюю левую точку глифа к заданному расстоянию от базовой точки по оси х.
905 905 1489 1489
Длина инструкций:
Шрифты TrueType
MDAP[rd] MIRP[nrp0.md,rd.l] IUP[y] IUP[x]
50 байт инструкций глифа разделены на 21 инструкцию. Большинство инструкций (кроме первой) состоит из одного байта. У каждой инструкции имеется мнемоническое название, набор флагов в квадратных скобках и ряд дополнительных параметров. Давайте последовательно рассмотрим все инструкции. 1. Инструкция NPUSHB (занести N байт в стек) заносит в стек заданное количество байт. В данном примере в стек заносятся 28 байт из потока инструкций. Верхний элемент стека равен 12. 2. Инструкция SRPO (установить эталонную точку 0) извлекает значение 12 из стека и назначает контрольную точку 12 эталонной точкой 0. Контрольная точка 12 соответствует базовой точке em-квадрата. 3. Инструкция MIRP[srpO,nmd,rd,2] (относительное перемещение эталонной точки) извлекает из стека значения 100 и 5 и перемещает точку 5 так, чтобы ее расстояние от эталонной точки 0 было равно CVT[100] (элемент таблицы
6. Инструкция DELTAP1 (дельта-исключение Р1) извлекает из стека значения 2, 13, 64, 13 и 15 и создает исключения со значением 13 в точках 64 и 15. В результате заданные точки перемещаются на величину, определяемую парными величинами (13). В данном случае номера точек, похоже, неверны. 7. Инструкция SRPO (установить эталонную точку 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 в абсолютную позицию CVTfS] = 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. Инструкция IUP[x] интерполирует остальные точки контура в направлении оси х. Впрочем, это всего лишь упрощенное описание инструкций простейшего глифа. Полный набор инструкций глифов TrueType и их семантика — гораздо более сложная тема. Существует более 100 различных инструкций, 20 переменных графического состояния и несколько типов данных. За полной информацией обращайтесь к руководству «TrueType Reference Manual» на сайте fonts.apple.com.
Горизонтальные метрики Информация, хранящаяся в таблице данных глифа, недостаточна для горизонтального выравнивания последовательности глифов, образующих строку текста, или вертикального выравнивания строк абзаца. Базовые метрики латинских шрифтов TrueType хранятся в двух таблицах: таблице горизонтальной структуры и таблице горизонтальных метрик (таблицы «hhea» и «htmx»). Прежде чем рассматривать эти таблицы, необходимо познакомиться с некоторыми шрифтовыми метриками, показанными на рис. 14.17.
Надстрочный интервал Базовая точка
Шрифты TrueType
787
том всего шрифта, а не отдельного глифа. Эта метрика определяет положение базовой линии текстовой строки от начальной позиции вывода. Подстрочный интервал (descent) также является атрибутом шрифта и определяет расстояние от базовой линии до нижней границы подстрочных элементов (в таких глифах, как Q, q или g). Сумма подстрочного интервала с надстрочным образует высоту строки шрифта, хотя при выводе абзацев могут использоваться дополнительные междустрочные интервалы. У каждого глифа имеется ограничивающий блок, в шрифтах TrueType являющийся частью заголовка глифа. Ограничивающий блок описывается четверкой [xmin,ymin,xmax,ymax], то есть минимальными и максимальными координатами контрольных точек глифа. По горизонтали между базовой точкой и позицией xmin глифа обычно существует небольшой зазор, который называется левым отступом. После размещения глифа в строке базовая точка следующего глифа смещается от позиции хглах на расстояние, называемое правым отступом. Левый и правый отступы, как и ограничивающий блок, относятся к числу атрибутов отдельных глифов. Сумма левого отступа, ширины глифа (xmax - xmin) и правого отступа называется полной шириной. Полная ширина определяет горизонтальное смещение базовой точки после размещения глифа в строке. Следующий глиф выводится от новой базовой точки. Значения левого и правого отступов обычно положительны, что соответствует разделению глифов дополнительными промежутками. Впрочем, иногда они бывают отрицательными для сближения глифов. Например, в шрифте Times New Roman строчная буква «j» имеет отрицательный левый отступ, а строчная буква «f» имеет отрицательный правый отступ. На рис. 14.18 изображена буква «F» курсивного шрифта с отрицательными отступами с обеих сторон.
Высота строки
ч
Базовая линия
Подстрочный интервал ^ xmax-xmin Левый_ отступ"
i
Полная ширина -
\
Правый отступ
Рис. 14.17. Метрики глифа
Надстрочным интервалом (ascent) называется расстояние от верхней границы прописных букв до базовой линии. Надстрочный интервал является атрибу-
Рис. 14.18. Глиф с отрицательными значениями левого и правого отступов
В шрифтах TrueType такие атрибуты, как надстрочный и подстрочный интервалы шрифта, хранятся в горизонтальной заголовочной таблице, а данные уровня глифа (такие, как левый отступ и полная ширина) — в таблице горизонтальных метрик.
788
Глава 14. Шрифты
Определение структуры горизонтальной заголовочной таблицы («hhea») выглядит следующим образом: typedef struct // 0x00010000 для версии 1.0 Fixed version: FWord Ascender; // Типографский надстрочный интервал // Типографский подстрочный интервал FWord Descender; // Типографский междустрочный интервал FWord LineGap: FWord advanceWidthMax: // Максимальная полная ширина FWord minLeftSideBearing; // Минимальный левый отступ FWord minRightSideBearing; // Минимальный правый отступ FWord xMaxExtent; // Мах(левый отступ + (хМах - xMin)) SHORT caretSlopeRise; // Наклон курсора SHORT caretSIopeRun; // 0 для вертикального положения. SHORT reserved[5]: // Присваивается 0. SHORT metricDataFormat; // 0 означает текущий формат USHORT numberofHMetrics; // элементы hMetric в таблице 'htmx' } Tab!e_HoriHeader; В горизонтальной заголовочной таблице («hhea») хранятся надстрочные и подстрочные интервалы шрифта, междустрочный промежуток, максимальная полная ширина, минимальный левый и правый отступы, максимальные габариты (левый отступ + xmax - xmin), хинты для вывода каретки и информация о таблице горизонтальных метрик. В таблице горизонтальных метрик (таблица «htmx») хранится информация горизонтальных метрик уровня глифа. Для каждого глифа должен существовать способ получения левого отступа и полной ширины, по которым правый отступ вычисляется по формуле «полная ширина - левый отступ - (xmax - xmin)». Впрочем, для моноширинных шрифтов с постоянным значением полной ширины хранение нескольких копий полной ширины считается расточительством, поэтому таблица горизонтальных метрик делится на две части: в первой части хранится полная ширина и левый отступ каждого глифа, а во второй — только левые отступы. В таблице должны содержаться сведения обо всех глифах шрифта; количество глифов, имеющих полные горизонтальные метрики, хранится в последнем поле горизонтальной заголовочной таблицы (поле numberOfHMetrics). Ниже приведено описание структуры таблицы горизонтальных метрик. Обратите внимание: обе части представляют собой массивы переменной длины. typedef struct {
FWord advanceWidth; FWord Isb; } IngHorMetric;
typedef struct { longHorMetric hMetrics[l]; FWord leftSideBearing[l]; } Tab!e_HoriMetrics:
// numberOfHMetrics: // С предыдущим advanceWidth
Данные горизонтальных метрик шрифта можно получить средствами GDI при помощи функций GetCharABCWidths, GetCharABCWidthsFloat и GetCharABCWidthsI. В терминологии GDI левый отступ называется метрикой A, xmax - xmin назы-
Ш рифты TrueType
789
вается метрикой В, а правый отступ — метрикой С. Мы рассмотрим эти функции в следующей главе при знакомстве с форматированием текста, поскольку эти функции в большей степени связаны с логическими шрифтами GDI, нежели с физическими шрифтами TrueType.
Кернинг При размещении глифов в строке используются параметры левого и правого отступов, улучшающие ее внешний вид, однако для каждого глифа эти атрибуты являются постоянными величинами. Когда два конкретных глифа находятся по соседству, из-за особенностей их формы эти глифы иногда должны располагаться ближе или дальше друг от друга. Регулировка интервалов между определенными парами глифов называется кернингом. Благодаря кернингу сочетания этих глифов выглядят более естественно. В режиме TrueType данные кернинга берутся из таблицы, созданной разработчиком шрифта. Ниже приведены структуры таблицы кернинга (таблица «kern») для шрифтов TrueType. typedef struct { FWord leftglyph; FWord rightglyph-. FWord move: } KerningPair;
typedef struct { Version: FWord nSubTables: FWord SubTableVersion: FWord Bytesinsubtable: FWord Coveragebits: FWord Numberpairs; FWord SearchRange: FWord EntrySelector; FWord RangeShift: FWord KerningPair KerningPair[l]; // Переменный размер } TableJCerning; Таблица кернинга имеет довольно простую структуру - она состоит из заголовка и простого массива структур KerningPair; каждая структура содержит двг индекса глифов и поправку. Пары кернинга сообщают подсистеме вывода текста о необходимости отрегулировать расстояние между двумя конкретными глифами, следующими в указанном порядке. Например, поля первой пары кернин га шрифта Tahoma равны 4, 180, -94. Это означает следующее: «Если глиф 18( следует непосредственно за глифом 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 I p k r n p a i r ) : Чтобы получить данные кернинга для текущего логического шрифта, выбранного в контексте устройства, сначала вызовите функцию GetKerningPair с параметрами 0 (nNumPairs) и NULL (Ipkrnpair); функция вернет количество определенных пар. Выделите память и вызовите функцию повторно для получения фактических данных кернинга. Учтите, что значение 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 // FWJHIN . . FWJLACK // ULTRA_CONDENSED .. ULTRAJXPANDED
// Возможность внедрения шрифта
// Толщина перечеркивающей линии // Шрифты IBM
Шрифты TrueType
791
PANOSE panose; ULONG ulUnicodeRangel; // Биты 0-31 Интервал символов Unicode ULONG ulUnicodeRange2: // Биты 32-63 ULONG ulUnicodeRange3; // Биты 64-95 ULONG ulUnicodeRange4; // Биты 96-127 achVendID[4]: CHAR // Идентификатор поставщика USHORT fsSelection; // ITALIC.. REGULAR USHORT usFirstCharlndex; // Первый символ UNICODE // Последний символ UNICODE USHORT usLastCharlndex: // Типографский надстрочный интервал USHORT sTypoAscender: // Типографский подстрочный интервал USHORT sTypoDescender: // Типографский междустрочный интервал USHORT sTypoLineGap: // Надстрочный интервал для Wi ndows USHORT usWinAscent; // Подстрочный интервал для Windows USHORT usWinDescent: // Биты 0-31 ULONG ulCodePageRangel; // Биты 32-63 ULONG ulCodePageRange2; } Table_ OS2 Таблица «OS/2» содержит подробную информацию в формате, достаточно близком к структурам шрифтовых метрик GDI — таких, как LOGFONT, TEXTMETRICS, ENUMTEXTMETRIC и OUTLINETEXTMETRIC. Вследствие мультиплатформенной природы шрифтов TrueType обилие непоследовательной информации иногда сбивает с толку. Например, в таблице «OS/2» хранятся два набора надстрочных и подстрочных интервалов, которые не всегда совпадают с одноименными атрибутами, хранящимися в горизонтальной заголовочной таблице.
Другие таблицы Мы подробно рассмотрели важнейшие таблицы шрифта TrueType. Впрочем, шрифты TrueType/OpenType могут содержать другие таблицы, относящиеся к нетривиальным возможностям, используемые на других платформах или просто при печати на принтере. Таблица имен («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: ULONG Version; ULONG DirectoryCount: DWORD Directory[l]; } TTCJteader:
// Ter TTC 'ttcf // Версия ТТС (изначально 0x0001000) // Количество каталогов таблиц // Смещения каталогов (переменный размер)
Хотя коллекции шрифтов экономят память и место на диске, они нарушают работу функции GetFontData. При вызове GetFontData приложение запрашивает данные TrueType для всего шрифта, сохраняет их и передает на другой компьютер, где позднее этот шрифт устанавливается. Однако при работе с коллекцией приложение не знает, являются ли полученные данные полными или же они входят в коллекцию шрифтов TrueType. Что еще хуже, некоторые смещения задаются
Установка и внедрение шрифтов
793
относительно невидимого заголовка коллекции TrueType вместо текущего каталога таблиц. Например, смещения в структуре TableDi rectory задаются относительно начала физического файла, поэтому они зависят от того, откуда были получены данные — из отдельного шрифта или из коллекции. Обходное решение заключается в проверке размеров всех шрифтов в коллекции по тегу ТТС. Сравнивая их с размерами текущего шрифта, можно определить его смещение в коллекции и в дальнейшем использовать его для поиска нужных таблиц.
Установка и внедрение шрифтов Шрифты распространяются в виде файлов. Чтобы шрифт мог использоваться приложениями, он должен быть предварительно установлен операционной системой. В GDI существует несколько функций, управляющих установкой и удалением шрифтов, а также используемых при внедрении шрифтов в приложения или документы. BOOL CreateScalableFontResource(DWORD fdwHidden, LPCTSTR IpszFontRes. LPCTSTR IpszFontFile, LPCTSTR IpszCurrentPath); int AddFontResource(LPCTSTR IpszFileName): BOOL RemoveFontResource(LPCTSTR IpFileName); int AddFontResourceEx(LPCTSTR IpszFileName. DWORD f 1. DESIGNVECTOR * pdv); int RemoveFontResourceExtLPCTSTR IpszFileName, DWORD f1, DESIGNVECTOR * pdv); HANDLE AddFontMemResourceExtLPVOID pbFont. DWORD cbFont. DESIGNVECTOR * pdb, DWORD * pcFonts); int RemoveFontMemResourceEx(HANDLE fh);
Ресурсные файлы шрифтов Основными типами шрифтов в операционных системах Windows считались векторные и растровые шрифты, а форматы TrueType, ОрепТуре и PostScript поначалу воспринимались как что-то постороннее. Несколько ресурсов растровых или векторных шрифтов (обычно относящихся к одной гарнитуре, но с разным кеглем) объединялись в 16-разрядные библиотеки DLL, называемые файлами шрифтовых ресурсов. В этих файлах шрифты подключались к приложению в виде двоичных ресурсов типа FONT (RT_FONT). Непосредственная поддержка установки шрифтов предусмотрена в GDI только для шрифтов в старом 16-разрядном формате файлов шрифтовых ресурсов. Для установки шрифта TrueType необходимо создать ресурсный файл масштабируемого шрифта. Ресурсный файл масштабируемого шрифта имеет тот же формат 16-разрядной библиотеки DLL, однако он не содержит копии шрифта TrueType. Вместо этого в нем указывается имя файла шрифта TrueType, по которому GDI находит данные шрифта. Чтобы создать ресурсный файл масштабируемого шрифта, вызовите функцию CreateScal abl eFontResource и передайте ей
794
Глава 14. Шрифты
целочисленный флаг, имя создаваемого ресурсного файла, имя существующего файла шрифта TrueType и путь к нему (если он не включен в имя). Флаг fdwHidden сообщает GDI, должен ли шрифт быть скрыт от остальных процессов в системе. Функция CreateScalableFontResource записывает на диск небольшой файл шрифтового ресурса. Ресурсным файлам шрифтов TrueType рекомендуется назначать расширение .FOT, чтобы они отличались он ресурсов растровых и векторных шрифтов с расширениями .FON.
Установка открытых шрифтов Функция AddFontResource устанавливает шрифт в системе по имени ресурсного файла, который может соответствовать растровому, векторному или шрифту TrueType. В результате установки шрифта ресурсный файл заносится в системную таблицу шрифтов и начинает использоваться при перечислении шрифтов, подстановке шрифтов, создании логических шрифтов и выводе текста. Шрифт, установленный функцией AddFontResource, доступен для всех приложений, если только шрифтовой ресурс не был создан со специальным флагом, скрывающим его в процессе перечисления шрифтов. Впрочем, шрифт, установленный функцией AddFontResource, доступен только во время текущего сеанса. После перезагрузки шрифт не будет автоматически добавлен в таблицу шрифтов. Чтобы установленный шрифт присутствовал в системе постоянно, информация о нем должна быть включена в реестр. Функция RemoveFontResource решает противоположную задачу — она удаляет шрифтовой ресурс из системной таблицы. При этом работающие приложения необходимо оповестить об изменениях в системной таблице шрифтов. Приложение, изменяющее таблицу шрифтов, должно оповестить об этом все окна верхнего уровня рассылкой сообщения WM_FONTCHANGE. Приложение, использующее список установленных шрифтов, должно обрабатывать сообщение WM_FONTCHANGE • и обновлять содержимое списка.
Установка закрытых шрифтов и шрифтов Multiple Master OpenType В Windows 2000 появились новые функции AddFontresourceEx и RemoveFontResourceEx. Второй параметр AddFontResourceEx управляет «закрытостью» шрифта. При установке бита FP_PRIVATE шрифт не может использоваться другими процессами и становится недоступным после завершения текущего процесса; если установлен флаг FP_NOT_ENUM, шрифт не участвует в перечислении. При установке любого из этих флагов вам уже не придется рассылать сообщение WM_FONTCHANGE и оповещать другие приложения о шрифте, с которым они не могут работать. Функция RemoveFontResourceEx использует тот же параметр, что и AddFontResourceEx, для удаления шрифта, установленного функцией AddFontResourceEx. В последнем параметре передается указатель на структуру DESIGN VECTOR, используемую только для шрифтов Multiple Master OpenType. Шрифты Multiple Master OpenType строятся на базе шрифтовой технологии PostScript Type 1. Несколько шрифтов Multiple Master OpenType могут обладать общими характеристиками, принимающими значения из определенного интервала (такие характе-
Установка и внедрение шрифтов
795
ристики называются осями), что позволяет осуществлять точную регулировку внешнего вида шрифта. Например, ось насыщенности шрифта Multiple Master OpenType может изменяться в интервале от 300 (тонкий) до 900 (тяжелый). Структура DESIGNVECTOR имеет переменный размер и содержит информацию о количестве характеристик и их значениях.
Установка шрифтов из образа в памяти Для установки шрифта TrueType функцией AddFontResource или AddFontResourceEx на диске должны находиться два физических файла — файл шрифта TrueType и ресурсный файл шрифта. Это затрудняет программирование приложений, работающих с закрытыми шрифтами, и полную маскировку закрытых шрифтов от других приложений. Функция AddFontMemResourceEx, появившаяся в Windows 2000, пытается решить эти проблемы, позволяя устанавливать шрифты из образа в памяти. Первые два параметра этой функции задают адрес и размер блока памяти, содержащего один или несколько шрифтовых ресурсов. Третий параметр содержит указатель на структуру DESIGNVECTOR для шрифтов Multiple Master OpenType. Функция 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): допускается только полное внедрение всего шрифта.
Установка и внедрение шрифтов
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-1. ttffile): _tcscpy(fotfile. ttffile): wsprintf(ttffile + _tcslen(ttffile), "\Us.ttf", fontname): wsprintfCfotfile + _tcslen(fotfile). "\Us.fot". fontname); BOOL rslt; switch ( option )
{
case 0: case FR_HIDDEN: rslt = RemoveFontResource(fotfile): break:
О Внедрение растров (0x0200): внедрение разрешается только для растров, содержащихся в шрифте. Если шрифт состоит из одних контуров глифов, он не может внедряться.
case FR_PRIVATE: case FRJIOTJNUM: case FR_PRIVATE | FRJOTJNUM: rslt - RemoveFontResourceEx(fotfile. option. NULL): break;
О Запрет внедрения (0x0002): шрифт не может внедряться в документы.
default:
Учтите, что уровень внедряемости шрифта относится только к внедрению шрифтов в документы, но не в приложения. Согласно MSDN, шрифты не могут внедряться в приложения, а в поставку приложений не могут входить документы, содержащие внедренные шрифты. Функция GetOutlineTextMetrics используется для проверки возможности внедрения шрифтов ТгаеТуре/OpenType. Она возвращает структуру OUTLINETEXTMETRIC, содержимое которой близко к содержимому таблицы метрик OS/2 и Windows (таблица «OS/2») в файле шрифта TrueType. Поле otmfsType этой структуры имеет то же значение, что и описанное выше поле f slype. В листинге 14.4 приведены две функции установки и удаления шрифтов TrueType/OpenType. Функция Install Font получает образ шрифта TrueType/ ОрепТуре в памяти, создает файлы .TTF и .FOT и устанавливает шрифт. Функция RemoveFont исключает шрифт из системного списка и удаляет файлы .TTF и .FOT. Обе функции получают параметр option, который сообщает, должен ли шрифт быть открытым, скрытым, закрытым, не перечисляемым или устанавливаемым прямо из образа в памяти. В зависимости от значения option выбирается функция GDI, вызываемая при установке и удалении шрифта.
797
assert(false); rslt = FALSE;
if ( ! DeleteFile(fotfile) ) rslt = FALSE;
if ( ! DeleteFile(ttffile) ) rslt = FALSE: return rslt: HANDLE InstallFontCvoid * fontdata. unsigned fontsize. const TCHAR * fontname. int option) if ( option & FR_MEM ) DWORD num; return AddFontMemResourceEx(fontdata. fontsize. NULL. & num):
Листинг 14.4. Установка и удаление шрифтов #define FR_HIDDEN fdefine FR MEM
0x01 0x02
TCHAR ttffile[MAX_PATH];
Продолжение
798
Глава 14. Шрифты
799
Установка и внедрение шрифтов
Листинг 14.4. Продолжение TCHAR fotf11e[MAX_PATH]; GetCurrentDi rectory(MAX_PATH-l , ttf f i 1 e) : _tcscpy(fotfile. ttffile); wsprintf(ttffile + _tcslen(ttffile). "\Us.ttf", fontname); wsprintfCfotfile + _tcslen(fotfile), "\Us.fot", fontnarae); HANDLE hFile = CreateFileCttffile, GENERIC_WRITE, 0. NULL. CREATE_ALWAYS. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN. 0); if ( hFile==INVALID_HANDLE_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:
С г
/г
Ozzie Black ^_ Ozzie Black Italic ..QK
J
Рис. 14.19. Демонстрация внедрения шрифтов
При построении рисунка были использованы три бесплатных шрифта TrueType с web-страницы HP FontSmart Homage (www.fonstmart.com): Euro Sign, Ozzie Black и Ozzie Black Italic. Если эти шрифты не установлены в системе, первая строка выводится стандартным шрифтом Symbol, а две других — шрифтом Arial. После установки шрифтов диалоговое окно принимает вид, показанный на рисунке, но после удаления шрифтов окно возвращается к прежнему виду. Если у вас нет этих шрифтов, загрузите их, а если есть — найдите в Интернете какие-нибудь новые бесплатные или условно-бесплатные шрифты. Запустите программу FontEmbed, поэкспериментируйте с разными вариантами установки и проверьте, доступен ли шрифт после установки в текущем приложении и в других приложениях.
case FR_NOT_ENUM:
case FR_PRIVATE | FR_NOT_ENUM: return (HANDLE) AddFontResourceExCfotfile. option, NULL); default: assert(false); return NULL:
Функции, приведенные в листинге 14.4, были использованы в простой демонстрационной программе FontEmbed. Эта программа представляет собой простое приложение на базе диалогового окна (рис. 14.19). В диалоговом окне программы FontEmbed расположены три кнопки. Кнопка Generate генерирует «документ» с внедренными шрифтами TrueType/OpenType, выбранными пользователем, применяя несложный механизм шифрования. Кнопка Load загружает сгенерированный документ и устанавливает внедренные шрифты в системе. Режим использования шрифта определяется группой переключателей. Кнопка Unload удаляет все установленные шрифтовые ресурсы. Справа показаны результаты, полученные при выводе текста внедренными шрифтами.
Системная таблица шрифтов В Windows NT/2000 список шрифтов, постоянно присутствующих в системе, хранится в следующем разделе реестра: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts Во время загрузки системы шрифты загружаются в системную таблицу шрифтов, что дает возможность их использовать. Шрифты в списке соответствуют физическим шрифтам, совместно используемым всеми процессами в системе. На самом деле графический механизм хранит в адресном пространстве режима ядра целых три таблицы — для открытых шрифтов, для закрытых шрифтов и для шрифтов устройств, которые обычно поддерживаются современными принтерами (например, принтерами PostScript). Постоянные шрифты, предоставленные операционной системой, обычно должны быть доступны для всех приложений, поэтому они хранятся в таблице открытых шрифтов. Если файл шрифтового ресурса OpenType/TrueType создается с флагом скрытия, при передаче флага FR_HIDDEN при вызове CreateFontResourceEX или при использовании функции CreateFontMemResourceEx шрифт хранится в таблице закрытых шрифтов. Если флаг FR_NOT_ENUM используется без флага FR_HIDOEN, шрифт заносится в таб-
800
Глава 14. Шрифты
лицу открытых шрифтов. В системном списке шрифтов хранятся полные пути к файлам каждого шрифта. Если шрифт устанавливался из ресурса, находящегося в памяти, для него используется имя псевдофайла типа «MEMORY-1». В расширении отладчика GDI поддерживаются три команды pubft, pvtft и devft для вывода содержимого таблиц шрифтов. Вы можете использовать эти команды в управляющей программе Fosterer (см. главу 3).
Итоги Эта глава посвящена основным принципам вывода текста в графическом Windows-программировании. Она начинается с описания базовых концепций шрифтов: символов, наборов символов, глифов, кодировок и отображения символов в глифы. Далее описываются три основных типа шрифтов системы Windows: растровые, векторные и шрифты TrueType. Мы знакомимся с тем, как в каждом типе шрифта представляются глифы и как происходит их вывод в процессе растеризации. Глава завершается описанием установки и удаления шрифтов, а также внедрения шрифтов в документы. Руководствуясь хорошим пониманием шрифтов, заложенным в этой главе, в главе 15 мы переходим к их практическому применению — выводу текста.
Глава 15 Текст Как было показано в предыдущей главе, шрифты являются основным элементом выводимого текста. В этой главе рассматриваются логические шрифты, функции вывода текста, простейшее форматирование, качественное и точное форматирование и специальные эффекты, используемые при выводе текста.
Примеры программ
Логическиешрифты
К главе 14 прилагаются два примера программ (табл. 14.4). Таблица 14.4. Программы главы 14 Каталог проекта
Описание
Samples\Chapt_14\Fonts
Иллюстрация общих концепций — наборов символов, кодировок, глифов, семейств шрифтов, процесса перечисления и трех основных типов шрифтов (растровые, векторные и шрифты TrueType)
Samples\Chapt_14\FontEmbed
Иллюстрация установки, удаления и внедрения шрифтов в документы
I •
В главе 14 были описаны важнейшие особенности трех основных шрифтовых технологий, применяемых в Windows-программировании, — растровых шрифтов, векторных шрифтов и шрифтов TrueType/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. Расстояние между двумя соседними символами называется полной шириной. Полная ширина делится на три части. Левая часть обычно соответствует пробелу перед крайней левой точкой глифа; она называется метрикой А (левый отступ в терминологии шрифтов TrueType). Средняя часть определяет фактическую ширину глифа в ячейке и называется метрикой В. Правая часть обычно соответствует пробелу после крайней правой точки глифа и называется метрикой С (правый отступ в TrueType). Полная ширина символа равна сумме метрик А, В и С. Метрики А и С могут иметь отрицательные значения для сближения глифов, особенно в курсивных шрифтах.
804
Глава 15. Текст
Стандартные шрифты Логический шрифт представляет собой объект GDI, описывающий требования к конкретному воплощению физического шрифта. Объект логического шрифта, как и другие объекты GDI, находится под управлением GDI, а с точки зрения приложения он представляют собой «черный ящик». Пользовательские приложения работают с логическими шрифтами только через манипуляторы логических шрифтов, относящиеся к типу HFONT. В системе определяются семь стандартных (встроенных) логических шрифтов GDI, используемых операционной системой при выводе пользовательского интерфейса, а также в приложениях. Манипуляторы стандартных логических шрифтов возвращаются вызовами GetStockObject(DEFAULT_GUI_FONT), GetStockObject(SYSTEM_FONT) и т. д. Большинство стандартных логических шрифтов относится к категории растровых шрифтов, используемых для ускоренного вывода заголовков окон, меню, диалоговых окон и т. д. На рис. 15.2 показаны 7 стандартных шрифтов на мониторе с разрешением 96 dpi. Для каждого стандартного шрифта приведен способ получения манипулятора функцией GetStockObject и содержимое структуры LOGFONT, о которой речь пойдет ниже. DialogBaseUnits: baseunixX=8, baseunitY=16 GetDeviceCaps(LOGPIXELSX)=96, GetDeviceCaps(LOGPIXELSX)=96 GetSlockObiect(DEFAULT_GLII_FONT) M1,0, 0, 0. 400, 0, 0,0,0, 0, 0, 0, 0, MS Shell Dig} GetStockObject <12, 8, 0, 0, 400, 0, 0, 0, 255, 1, 2, 2, 49, Terminal}
GetStockObject(ANSI_FIXED_FOHT) {12, 9, 0, 0, 4 0 0 , О, О, О, О, О
2, 2, 1, Courier}
GetStockObject(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, U80, 0, 0, 0, 0, 1, 2, 2, 49, Fixedsys} Рис. 15.2. Стандартные шрифты в разрешении 96 dpi
Стандартные шрифты часто встречаются в заголовках окон, меню и текстах, выводимых в различных элементах управления. Размер стандартного шрифта в пикселах изменяется при изменении логического разрешения экрана. Например, логическое разрешение нормального экрана равно 96 dpi — так называемый режим «мелких шрифтов». При помощи панели управления можно переключиться в режим «крупных шрифтов» с разрешением 120 dpi. При переключении экрана из режима мелких шрифтов в режим крупных шрифтов все стандартные шрифты приходится заново отображать на физические шрифты большего раз-
Логические шрифты
805
мера, для чего обычно перезагружают систему. После этого все заголовки окон, меню, элементы и диалоговые окна увеличиваются в соответствии с изменениями в размере шрифта. Проектирование пользовательского интерфейса, который бы идеально выглядел в обоих режимах (крупных и мелких шрифтов) — задача не из простых. Вы можете воспользоваться функцией GetSystemMetrics для получения различных системных метрик, в том числе текущих размеров строк заголовка и меню. Например, вызов GetSystemMetrics(SM_CYMENU) возвращает высоту строки меню с одной строкой команд. Диалоговые окна проектируются в аппаратно-независимых шаблонных диалоговых единицах. При создании диалогового окна шаблонные единицы преобразуются в экранные пикселы с учетом текущих базовых диалоговых единиц по следующим формулам: pixelX = (templateunitX * baseunitX) / 4: pixelY = (tempiateunitY * 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 IfHeight: LONG IfWidth; LONG IfEscapement: LONG 1 fomentation; LONG IfWeight: LONG IfItalic; LONG IfUnderline: LONG IfStrikeOut: LONG IfCharSet: LONG IfOutPrecision; LONG ifdipPrecision;
806 LONG LONG LONG } LOGFONT.
Глава 15. Текст
IfQuality; IfPitchAndFamily: 1fFaceName[LF_FACESIZE]: *PLOGFONT:
typedef struct tagENUMLOGFONTEX { LOGFONT elfLogFont: TCHAR elfFullName[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 fdwSthkeOut. DWORD fdwCharSet. DWORD fdwOutputPrecision. DWORD fdwdipPrecision. 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, поэтому мы должны рассмотреть их основные поля. О IfHeight. Желательная высота шрифта в логических единицах. Если значение равно 0, используется стандартная высота шрифта, равная примерно 12 пунктам. Положительные значения определяют требуемую высоту ячеек (то есть сумму надстрочного и подстрочного интервалов). Отрицательные значения определяют высоту символов шрифта (надстрочный интервал + подстрочный интервал - внутренний зазор). О IfWidth. Желательная ширина шрифта в логических единицах. Если значение равно 0, поиск осуществляется сравнением аспектного отношения графического устройства с аспектным отношением физического шрифта. Экраны и принтеры обычно обладают одинаковым разрешением по вертикали и горизонтали — например, 120 х 120 dpi или 600 х 600 dpi. В этом случае нулевое
Логические шрифты
807
значение параметра отдает предпочтение шрифтам с аспектным отношени ем 1:1. О 1 fEscapement. Угол (в десятых долях градуса против часовой стрелки) междч базовой линией текста и осью х устройства. Например, 1 fEscapement = 900 оз начает, что весь текст выводится вдоль базовой линии, параллельной оси у. О IfOrientation. Угол (в десятых долях градуса против часовой стрелки) меж ду базовой линией каждого символа и осью х устройства. Следует помнить что ориентация определяет угол поворота отдельных символов, а накло! (1 fEscapement) — угол поворота всей строки. Если устройство работает в рас ширенном графическом режиме (GM_ADVANCED), доступном лишь в Windows NT 2000, наклон и ориентация задаются независимо друг от друга. В совмести мом графическом режиме (GM_COMPATIBLE), поддерживаемом в Windows 95/98 поле 1 fEscapement определяет IfOrientation, и значения этих полей должш совпадать. О IfWeight. Насыщенность (жирность) шрифта в интервале от 0 до 1000. Значе ние FM_DONTCARE (0) позволяет GDI выбрать шрифт с произвольной насыщен ностью, FM_NORMAL (400) соответствует средней насыщенности, a FWJHEAVY (900 обычно определяет самый жирный шрифт. О If Italic. Если значение этого поля равно TRUE, предпочтение отдается курсив ным шрифтам. О 1 fUnder! i ne. Если значение этого поля равно TRUE, текст выводится с подчер киванием. О IfStrikeOut. Если значение этого поля равно TRUE, текст выводится перечерь нутым (посреди строки проводится линия). О IfCharSet. Набор символов шрифта; также определяет кодировку, в которо строки передаются функциям вывода текста GDI. Наборы символов и код!ровки описаны в разделе «Что такое шрифт?» главы 14. У поля IfCharSe имеется специальное значение DEFAULT_CHARSET. В Windows 95/98 шрифт ог ределяется значениями других полей, а в Windows NT/2000 будет задействс ван набор символов, используемый по умолчанию для текущего системно! локального контекста. Например, если системный локальный контекст соо~ ветствует английскому языку, используется значение ANSI_CHARSET. При рабе те с экзотическими языками поле IfCharSet играет очень важную роль дл выбора правильного шрифта, поскольку глифы нужного набора могут по; держиваться лишь небольшим количеством физических шрифтов. О IfOutPrecision. Желательные параметры подбора физических шрифтов. Зн; чение OUT_DEFAULT_PRECIS указывает на стандартный способ подбора шрифте При значении OUT_DEVICE_PRECIS предпочтение отдается шрифтам устройст а при значении OUT_RASTER_PRECIS — растровым шрифтам. Значение OUT_OUTLINI PRECIS (только в Windows NT/2000) отдает предпочтение контурным шрис] там, в том числе и шрифтам TrueType. При значении OUT_TT_PRECIS предпо1 тение отдается шрифтам TrueType, а при значении OUT_TT_ONLY_PRECIS подбс осуществляется только среди шрифтов TrueType. О IfClipPrecision. Способ отсечения шрифтов. Для этого поля определено н сколько флагов, но похоже, что к отсечению имеет отношение только фл;
806 LONG LONG LONG } LOGFONT. '
Глава 15. Текст
IfQuality: IfPitchAndFamily: IfFaceNameCLFJACESIZE]: *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 CreateFontlndirecUCONST LOGFONT * Iplf): HFONT OeateFontIndirectEx(const ENUMLOGFONTEXDV * penumlfex);
Параметры этих трех функций описывают требования пользователя к создаваемому логическому шрифту. Функция CreateFont использует для описания логического шрифта 14 параметров — рекорд для функций GDI. Функция CreateFont Indirect получает указатель на структуру LOGFONT, в которой упакованы все 14 параметров. Новая функция CreateFontlndirectEx, появившаяся только в Windows 2000, получает указатель на структуру ENUMLOGFONTEXDV. В структуру ENUMLOGFONTEXDV добавляется поле DESIGNVECTOR, в котором содержится уникальное имя шрифта, имена начертания и модификации. Таким образом, базовые требования к логическому шрифту описываются структурой LOGFONT, передаваемой при вызове CreateFontlndirect; функция CreateFont просто получает развернутую версию структуры LOGFONT, тогда как функция CreateFontlndirectEx всего лишь использует расширенный вариант этой структуры. LOGFONT и другие шрифтовые структуры играют очень важную роль для понимания шрифтов GDI, поэтому мы должны рассмотреть их основные поля. О 1 fHei ght. Желательная высота шрифта в логических единицах. Если значение равно 0, используется стандартная высота шрифта, равная примерно 12 пунктам. Положительные значения определяют требуемую высоту ячеек (то есть сумму надстрочного и подстрочного интервалов). Отрицательные значения определяют высоту символов шрифта (надстрочный интервал + подстрочный интервал - внутренний зазор). О IfWidth. Желательная ширина шрифта в логических единицах. Если значение равно 0, поиск осуществляется сравнением аспектного отношения графического устройства с аспектным отношением физического шрифта. Экраны и принтеры обычно обладают одинаковым разрешением по вертикали и горизонтали — например, 120 х 120 dpi или 600 х 600 dpi. В этом случае нулевое
Логические шрифты
807
значение параметра отдает предпочтение шрифтам с аспектным отношением 1:1. О 1 fEscapement. Угол (в десятых долях градуса против часовой стрелки) между базовой линией текста и осью х устройства. Например, 1 fEscapement = 900 означает, что весь текст выводится вдоль базовой линии, параллельной оси у. О If Orientation. Угол (в десятых долях градуса против часовой стрелки) между базовой линией каждого символа и осью х устройства. Следует помнить, что ориентация определяет угол поворота отдельных символов, а наклон (IfEscapement) — угол поворота всей строки. Если устройство работает в расширенном графическом режиме (GM_ADVANCED), доступном лишь в Windows NT/ 2000, наклон и ориентация задаются независимо друг от друга. В совместимом графическом режиме (GM_COMPATIBLE), поддерживаемом в Windows 95/98, поле IfEscapement определяет IfOrientation, и значения этих полей должны совпадать. О IfWeight. Насыщенность (жирность) шрифта в интервале от 0 до 1000. Значение FM_DONTCARE (0) позволяет GDI выбрать шрифт с произвольной насыщенностью, FMJJORMAL (400) соответствует средней насыщенности, a FW_HEAVY (900) обычно определяет самый жирный шрифт. О If Italic. Если значение этого поля равно TRUE, предпочтение отдается курсивным шрифтам. О If Underline. Если значение этого поля равно TRUE, текст выводится с подчеркиванием. О IfStrikeOut. Если значение этого поля равно TRUE, текст выводится перечеркнутым (посреди строки проводится линия). О IfCharSet. Набор символов шрифта; также определяет кодировку, в которой строки передаются функциям вывода текста GDI. Наборы символов и кодировки описаны в разделе «Что такое шрифт?» главы 14. У поля IfCharSet имеется специальное значение DEFAULT_CHARSET. В Windows 95/98 шрифт определяется значениями других полей, а в Windows NT/2000 будет задействован набор символов, используемый по умолчанию для текущего системного локального контекста. Например, если системный локальный контекст соответствует английскому языку, используется значение ANSI_CHARSET. При работе с экзотическими языками поле IfCharSet играет очень важную роль для выбора правильного шрифта, поскольку глифы нужного набора могут поддерживаться лишь небольшим количеством физических шрифтов. О IfOutPrecision. Желательные параметры подбора физических шрифтов. Значение OUT_DEFAULT_PRECIS указывает на стандартный способ подбора шрифтов. При значении OUT_DEVICE_PRECIS предпочтение отдается шрифтам устройств, а при значении OUT_RASTER_PRECIS — растровым шрифтам. Значение OUT_OUTLINE_ PRECIS (только в Windows NT/2000) отдает предпочтение контурным шрифтам, в том числе и шрифтам TrueType. При значении OUT_TT_PRECIS предпочтение отдается шрифтам TrueType, а при значении OUT_TT_ONLY_PRECIS подбор осуществляется только среди шрифтов TrueType. О IfClipPrecision. Способ отсечения шрифтов. Для этого поля определено несколько флагов, но похоже, что к отсечению имеет отношение только флаг
808
Глава 15. Текст
CLIP_DEFAULT_PRECIS (стандартная процедура отсечения). Если поле IfClipPrecision равно CLIP_EMBEDDED, допускается использование внедренных шрифтов, доступных только для чтения. При указании флага CLIP_LH_ANGLES направление поворота глифов шрифтов устройств зависит от того, является ли логическая система координат левосторонней или правосторонней; в противном случае шрифты устройств всегда поворачиваются против часовой стрелки. О IfQuality. Качество вывода глифов. Значение DEFAULT_QUALIYY сообщает GDI, что внешний вид символов несущественен. Значение DRAFT_QUALITY говорит о том, что размер шрифта важнее качества глифа, что позволяет GDI масштабировать растровые шрифты по нужным размерам с возможными искажениями. Значение PROOF_QUALITY указывает на то, что качество глифа важнее размера шрифта, поэтому масштабирование растровых шрифтов запрещается. Для шрифтов TrueType константы DRAFT_QUALITY и PROOF_QUALITY несущественны, поскольку контуры глифов свободно масштабируются до нужной величины. Значение ANTIALIASED_QUALITY заставляет GDI выполнять сглаживание текста, если оно поддерживается шрифтом, а сам шрифт не слишком велик и не слишком мал. Значение NONANTIALIASED_QUALITY запрещает сглаживание. О IfPitchAndFamily. Шаг и семейство шрифта определяются в одном поле. Младшие 2 бита могут быть равны DEFAULT_PITCH (шаг по умолчанию,) FIXED_PITCH (моноширинный шрифт) или VARIABLEJ4TCH (пропорциональный шрифт). Биты 4-7 определяют семейство шрифта в виде константы FF_DECORATIVE, FF_DONTCARE, FFJMODERN, FF_ROMAN, FF_SCRIPT и FFJSWISS. Семейства шрифтов описаны в разделе «Что такое шрифт?» главы 14. О IfFaceName. Имя гарнитуры шрифта. Имена гарнитур шрифтов, установленных в настоящий момент, перечисляются функцией EnumFontFamilies. О elf Full Name. Уникальное имя шрифта, включающее название компании, имя гарнитуры, начертания и т. д. О el fStyl e. Начертание шрифта — например, Bold Italic. О el fScri pt. Название языковой модификации шрифта — например, Cyrillic. О elfDesignVector. Оси шрифтов Multiple Master OpenType. Функции CreateFont, CreateFontlndirect и CreateFontlndirectEx создают объект логического шрифта и возвращают его манипулятор вызывающей стороне. Вызывая по манипулятору объекта GDI функцию GetObject, можно получить структуру LOGFONT или ENUMLOGFONTEX с описанием логического шрифта. Когда объекты логических шрифтов становятся ненужными, их, как и остальные объекты GDI, следует удалить функцией DeleteObject. При создании логического шрифта следует прежде всего рассчитать высоту шрифта в логических координатах. Если вы знаете размер шрифта в пунктах, следует преобразовать его в логические координаты по эталонному контексту устройства. Ниже приведена функция, которая преобразует размер шрифта в пунктах в размер, заданный в логических координатах. // Преобразование пунктов в логические координаты int PointSizetoLogicaKHDC hDC. int points, int divisor)
809
Логические шрифты POINT P[2] = // Две точки (POINT) в координатах устройства, // расстояние между которыми равно высоте шрифта
{ 0. О }. { 0. : :GetDeviceCaps(hDC. LOGPIXELSY) DPtoLP(hDC, P. 2);
points I 12 I divisor }
// Преобразовать координаты устройства // в логические координаты
return abs(P[l].y - Р[0].у); Функция PointSizeToLogical получает манипулятор эталонного контекста устройства, размер в пунктах и необязательный делитель, повышающий точность вычислений. Сначала пункты преобразуются в пикселы на основании вертикального разрешения устройства, после чего значение преобразуется в высоту, заданную в логической системе координат. Например, высота шрифта с кеглем 12 пунктов вычисляется вызовом PointSizetoLogical(hDC,12), а для шрифта с кеглем 12,25 используется вызов PointSizetoLoglcaKhDC, 1225, 100). На устройствах высокого разрешения (скажем, на принтере с разрешением 1200 dpi) каждый пиксел равен 0,06 пункта, поэтому дробная часть кегля может влиять на форматирование текста. Заполнение 14 полей структуры LOGFONT или передача 14 параметров функции CreateFont — утомительная процедура, которая нередко чревата ошибками. В листинге 15.1 приведен простой класс KLogFont, инкапсулирующий структуру логического шрифта LOGFONT. Листинг 15.1. Класс KLogFont: инкапсуляция структуры LOGFONT class KLogFont { public: LOGFONT mjf: KLogFontOnt height, const TCHAR * typeface=NULL) mjf .If Height mJf.lfWidth m_lf.IfEscapement mjf .1fOrientation mJf.lfWeight mjf.lfltalic m_lf.IfUnderline mjf .IfStrikeOut mJf.lfCharSet m_lf.1fOutPreci si on mjf .IfClipPrecision mjf. IfQuality
m If.IfPitchAndFamily
height: 0: 0; 0: FW_NORMAL: FALSE: FALSE: FALSE: ANSI_CHARSET; OUT_TT_PRECIS; CLIP_DEFAULT_PRECIS: DEFAULT_QUALITY: DEFAULT PITCH FF DONTCARE:
if ( typeface ) Jxsncpy(mjf.IfFaceName. typeface. LF_FACESIZE-1): else
Продолжение
810
Глава 15. Текст
Листинг 15.1. Продолжение m_lf.lfFaceName[0] = 0:
} HFONT CreateFont(void) {
return ::CreateFontIndirect(& m_lf); } int GetObject(HFONT hFont) { return : :GetObject(hFont. sizeof(mjf). &m_lf); Класс KLogFont сокращает количество параметров с 14 до 2. Остальным параметрам присваиваются разумные значения по умолчанию, которые можно изменить через поля открытой переменной m_l f. В следующем фрагменте создается логический курсивный шрифт с кеглем 36 пунктов для шрифта Times New Roman: KLogFont lf(- PointSizetoLogical (hDC. 36). "Times New Roman"; If.mjf.lfltalic = TRUE: HFONT hFont = If.CreateFontO:
Подстановка шрифта Новый логический шрифт, созданный функцией CreateFont, CreateFontlndirect или CreateFont IndirectEx, не ассоциируется ни с каким физическим шрифтом, поскольку он еще не ассоциирован с контекстом устройства. При выборе логического шрифта в контексте устройства перед GDI встает задача — подобрать физический шрифт, соответствующий заданному описанию. Этот процесс называется подстановкой шрифта (font matching). В процессе подстановки GDI сверяет требования логического шрифта с данными всех шрифтов, доступных для графического устройства. Помимо шрифтов, постоянно установленных в системе, в подстановке также могут участвовать внедренные шрифты и шрифты устройств. В главе 14 было показано, как внедрить шрифт в документ, установить его при открытии документа и получить список установленных шрифтов. Шрифты устройств поддерживаются драйвером графического устройства и реализуются графическим устройством на аппаратном уровне. Например, принтер PostScript обычно поддерживает несколько десятков шрифтов PostScript и передает информацию о них GDI, чтобы эти шрифты использовались при выводе текста. Обычно пользовательское приложение форматирует текст на основании метрических данных, запрашиваемых у контекста устройства. При непосредственном получении команд вывода драйвер принтера может генерировать команды, использующие шрифты устройства, вместо загрузки шрифтов TrueType. В классических растровых и векторных шрифтах Windows структура заголовка шрифта очень похожа на структуру TEXTMETRIC, используемую в GDI. Структура TEXTMETRIC содержит практически те же данные, что и LOGFONT, — высоту, среднюю ширину, насыщенность, семейство и тип, курсив, подчеркивание, перечеркивание и т. д. GDI без особых усилий подбирает нужный шрифт, сопостав-
Логические шрифты
811
'ляя содержимое LOGFONT с заголовком шрифта. Иначе говоря, создание логических шрифтов по структуре LOGFONT ориентировано на работу с растровыми и векторными шрифтами. Шрифты TrueType и ОрепТуре содержат гораздо более подробную информацию о характеристиках физического шрифта. Метрические данные шрифтов TrueType/OpenType хранятся в таблице метрик OS/2 и Windows, похожей на структуру GDI OUTLINETEXTMETRIC. Самым важным фактором при подборе физического шрифта является набор символов. Хотя большинство шрифтов поддерживает набор ANSI, символы других языков иногда поддерживаются лишь незначительной долей шрифтов, установленных в системе. Например, шрифты очень редко поддерживают декоративные знаки. Когда приложение запрашивает конкретный набор символов, GDI прикладывает все усилия к тому, чтобы найти шрифт с поддержкой именно этого набора; в противном случае символы могут выводиться совершенно неверными глифами. Растровые и векторные шрифты поддерживают лишь один набор символов; шрифт TrueType может поддерживать десятки наборов. В каждом шрифте TrueType/OpenType хранится 64-разрядное поле флагов с определением кодировок, поддерживаемых шрифтом. Очень большое внимание также уделяется точности вывода. Этот показатель ограничивает кандидатов определенными типами шрифтов. Например, OUT_ OUTLINE_PRECIS отдает предпочтение контурным шрифтам. Моноширинные шрифты по внешнему виду сильно отличаются от пропорциональных, поэтому тип шрифта также является важным фактором при подстановке. Первостепенное внимание уделяется и имени гарнитуры. Обнаружив физический шрифт с точным совпадением имени гарнитуры, у которого совпадают другие важные факторы (набор символов, высота, курсивное начертание и насыщенность), подсистема подстановки шрифтов GDI прекращает дальнейшие поиски. В системном реестре хранится список синонимов для имен гарнитур, заданных пользователем. Например, этот список может сообщить системе подстановке шрифтов, что «Helv» является синонимом «MS Sans Serif», «MS Shell Dig» — синонимом «Microsoft Sans Serif», a «Times» — синонимом «Times New Roman». Другими важными факторами, учитываемыми в процессе подстановки для растровых шрифтов, является семейство шрифта, высота, ширина и аспектное отношение. Для контурных шрифтов насыщенность шрифта, подчеркивание, перечеркивание, высота, ширина и аспектное отношение уже не столь существенны.
Система подстановки шрифтов PANOSE Как видите, процесс подбора шрифтов по данным LOGFONT выглядит вполне логично. Однако в нем учитываются лишь те данные, которые передаются при вызовах CreateFont/CreateFontlndirect и хранятся в заголовках растровых и векторных шрифтов. Если документ пересылается на компьютер с другим набором шрифтов, ситуация значительно усложняется. Допустим, документ хранит в структуре LOGFONT информацию о шрифте с гарнитурой Antique Olive Compact. Как следует действовать GDI, чтобы подобрать правильный физический шрифт?
812
Глава 15. Текст
si;
Логические шрифты
Хотя структура OUTLINETEXTMETRIC шрифтов TrueType/OpenType содержит копию простой структуры TEXTMETRIC, основным средством классификации шрифтов является структура PANOSE. Система подстановки шрифтов PANOSE предназначена для классификации и подбора шрифтов в соответствии с их внешним видом. В настоящее время шрифты TrueType используют технологию PANOSE 1.0, которая описывает шрифт 10 однобайтовыми характеристиками: typedef struct tagPANOSE BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE PANOSE,
bFamilyType: bSerifStyle; bWeight; bProportion; bContrast; bStrokeVariation; bArmStyle; bLetterForm; bMidline; bxHeight; * LPPANOSE:
В отличие от структуры LOGFONT, в которой к внешнему виду шрифта относятся всего два поля (IfWeight и IfPitchAndFamily), структура PANOSE закладывает основу для более точной подстановки шрифтов. В частности, в PANOSE определяются 14 разных стилей засечек - квадратные, треугольные, закругленные и т. д. Структура PANOSE обеспечивает компактный и эффективный способ классификации и подстановки шрифтов в системе. Для каждого шрифта TrueType/ ОрепТуре заполняется структура PANOSE, и степень сходства двух шрифтов оценивается по «расстоянию» между соответствующими точками 10-мерного пространства характеристик шрифтов. Технология PANOSE 2.0 идет еще дальше для описания шрифтов в ней используется 36 значений. За дополнительной . информацией о системе подстановки шрифтов PANOSE обращайтесь по адресу www.fonts.com/hp.panose/index.htm. Хотя технология PANOSE обеспечивает значительно лучший результат, чем подстановка шрифтов на основании структур LOGFONT и TEXTMETRIC, в GDI не существует функций для ее непосредственной поддержки. Функции CreateFont, CreateFontlndirect и CreateFontlndirectEx не используют структуру PANOSE при определении логического шрифта. На самом деле работа алгоритма подстановки шрифтов PANOSE основана на СОМ-интерфейсе IPANOSEMapper, реализованном в одной из малоизвестных системных библиотек panmap.dll. В частности, этот интерфейс используется приложением Fonts (Шрифты) панели управления, когда пользователь запрашивает груп£Ил^КоУ схожих шрифтов. На рис. 15.3 показана система подстановки шрифтов PANOSE в действии. На рисунке показано, как выглядит окно приложения при выборе команды View » List Fonts by Similarity (Вид > Группировать схожие шрифты). Если выбрать в качестве эталона шрифт Tahoma, то шрифт Verdana будет обозначен как «очень похожий», шрифт Arial - «весьма похожий», а шрифт Courier - «не похожий». Для некоторых шрифтов в списке имеет место запись о недоступности сведений (то есть данные PANOSE отсутствуют).
*'Ыт» '•
- ' - - - '
J-SI^^Me&nfcL -,:-•-;-'
ЁЗ A'ial ^ Arial Black § Arial Narrow
Very similar Very similar Fairly similar Fairly similar Fairly similar
РЙ Ronkmsn ПИ SJtulp
Fsirlu fimiUr
|d] Tahoma jd] Verdana
'' :';I'
>!
- 'Ч-: - -2
<£|
Рис. 15.3. Механизм PANOSE в приложении панели управления
В листинге 15.2 приведен простой класс для работы с интерфейсом IPANOSE Mapper. Листинг 15.2. Класс KFontMapper: использование интерфейса IPANOSEMapper class KFontMapper IPANOSEMapper const PANOSE int
* m_pMapper; * m_pFontList: mjiFontNo:
public: KFontMapper(void) m_pMapper = NULL: m_pFontList = NULL: mjiFontNo = 0:
CoInitialize(NULL): CoCreateInstance(CLSID_PANOSEMapper. NULL, CLSCTX_INPROC_SERVER. IID_IPANOSEMapper,
(void **) & mjpMapper);
void SetFontList(const PANOSE * pFontList. int nFontNo) { m_pFontList = pFontList; m nFontNo = nFontNo:
int PickFontstconst 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, // Количество сравниваемых шрифтов sizeoftPANOSE).
pTarget->bFamilyType):
m_pMapper->bPANRestoreThresho1d(); return rslt;
-KFontMapperO { if ( m_pMapper ) m_pMapper->Release(): CoLlninitializeO;
Помимо конструктора и деструктора, класс KFontMapper содержит две функции. Функция SetFontList заполняет массив структур PANOSE для доступных шрифтов. Функция PickFonts получает метрику PANOSE и пытается найти для нее хорошие совпадения. Результаты возвращаются в двух массивах — шрифтов и расстояний между исходной структурой PANOSE и подобранными вариантами. Чтобы использовать класс KFontMapper, необходимо решить две проблемы. Первая — определение метрики PANOSE для шрифта, которому вы подбираете замену. Вторая — построение базы данных с метриками PANOSE для всех доступных шрифтов в системе. В одном из возможных решений метрика PANOSE сохраняется вместе со структурой LOGFONT в документе. При создании логического шрифта и его выборе в контексте устройства 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 является расширением LOGFONT с полными именами гарнитуры и стиля, идентификатором разработчика, метрикой PANOSE и т. д. Таким образом, структура
815
Логические шрифты
Содержит информацию как о логическом, так и о физическом шрифтах. Как ни странно, ни одна документированная функция GDI не получает и не возвращает структуру EXTLOGFONT. Существует лишь одно документированное применение EXTLOGFONT - в структуре 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 ToolsVPanose Список установленных физических шрифтов хранится в реестре по ключу SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts
В результате перебора списка физических шрифтов можно получить имена гарнитур шрифтов и отфильтровать их, оставив только шрифты TrueType/ ОрепТуре. По имени гарнитуры вы создаете логический шрифт, выбираете его в контексте устройства и отображаете на физический шрифт. Если найденный физический шрифт является шрифтом TrueType/OpenType, его метрику PANOSE можно получить функцией GetOutlineTextMetrics. Функция, приведенная в листинге 15.3, по имени гарнитуры возвращает метрику PANOSE и имя подставленной гарнитуры. Методика перечисления шрифтов рассматривается в главе 14. Листинг 15.3. Получение метрики PANOSE по имени гарнитуры
// 'Arial Bold Italic' -> PANOSE bool GetPANOSEtHDC hDC. const TCHAR * full name, PANOSE * panose. TCHAR facename[]) { TCHAR name[MAX_PATH]:
// Удалить начальные пробелы while (fullname[0]==' ') full name ++:
Продолжение
816
Глава 15. Текст
Листинг 15.3. Продолжение _tcscpy(name. fullname):
// Удалить завершающие пробелы for (int i-_tcslen(name)-l: (i>-0) && (named]-=' '): 1--) named] = 0; LOGFONT If; memset(&lf. 0. • If. If Height = If.lfCharSet If.IfWeight -
sizeof(lf)); 100: DEFAULT_CHARSET; FW_REGULAR;
if ( strstr(name, "Italic") ) If.lfltalic - TRUE; if ( strstrCname. "Bold") ) If.IfWeight = FW_BOLD:
817
Получение информации о логическом шрифте
сунка видно, что шрифт Courier New близок к шрифтам Andale Mono, Lucida Console, Georgia и Palatino Linotype. .«.idix}!
Г«£ ; ,T?spr :r ". , ,^рЫЗ£<*
1 t ЙШЫ
-
Courier New Courier New Bold Courier New Bold Italic Courier New Italic Lucida Console Lucida Sans Unicode Times New Roman Times New Roman B... Times New Roman B... Times New Roman It... Wingdings Symbol Verdana
.
Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Text and Display Tent and Display Text and Display Pictorial Pictorial Text and Display
:
Ssi*
f We»
t йдахб«XI 1 Cer*«±il
Thin Thin TU' 1 nin Thin Normal Sans Normal Sans Cove Cove Cove Cove
Light Medium Medium Thin Medium Medium Medium Bold Demi Book
Monospaced Mono=|
Any
Any
Any
Obtuse Sq.. Normal Sans
No Fit Medium
OldSt Even*
None
j]
^1Шщ!||1Ж&!:3|1
Mono.-' МОПО:
OldSt Mode! ModelModef Modet
5
Q
'
г „v "as ' i
as ie ., -.72-7г;
& -•; 6' .? . 8 ,, S "•,, ]#}
,ш
''JSiv ''
_tcscpy(lf.lfFaceName. name); HFONT hFont = CreateFontIndirect(& If); if ( hFont==NULL ) return false; HGOIOBJ hOld - SelectObjecUhDC, 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: SelectObject(hDC. hOld); DeleteObject(hFont);
}
return facename[0] !- 0-
На рис. 15.4 показано окно PANOSE Font Matching демонстрационной программы Font. Основное место в нем занимает список всех доступных шрифтов и их атрибутов. Если щелкнуть правой кнопкой мыши на одной из строк списка, метрика PANOSE этого шрифта сравнивается с метриками PANOSE всех остальных шрифтов. В окне, расположенном в правой части рисунка, приведены результаты сопоставления для шрифта Courier New. Обратите внимание: у шрифта MmgLiU нет метрики PANOSE, что приводит к некоторому искажению результатов. Этот шрифт следовало бы исключить из массива чисел PANOSE. Из ри-
Рис. 15.4. Иллюстрация системы подстановки шрифтов PANOSE
Качество замены отсутствующих шрифтов можно повысить несколькими способами. Например, в процессе построения документа наряду со структурой LOGFONT можно сохранить метрику PANOSE вместе с другими данными физического шрифта. За образец можно взять структуру EXTLOGFONT, используемую в EMF. При открытии документа на другом компьютере приложение проверяет сходство физического шрифта, предложенного GDI, с физическим шрифтом, использовавшимся на исходном компьютере. Степень сходства оценивается сравнением полных имен шрифтов или их чисел PANOSE. Если приложение решает, что исходный шрифт в системе отсутствует, а предложенная замена неприемлема, оно выполняет подстановку самостоятельно. Для этого оно может построить собственную базу данных чисел PANOSE для всех доступных шрифтов, найти оптимальную замену при помощи класса KFontMapper или другого средства и затем заново создать логические шрифты для лучших кандидатов, найденных на локальном компьютере.
Получение информации о логическом шрифте После того как созданный объект логического шрифта выбран в контексте устройства, GDI подбирает для него физический шрифт из числа установленных в системе. Когда это происходит, логический объект ассоциируется с реализованным шрифтом. Реализованный шрифт не стоит путать с физическим; это всего лишь конкретный экземпляр физического шрифта с конкретным размером, аспектным отношением, углом поворота, имитацией особых возможностей и т. д. Например, физический шрифт Times New Roman в конкретный момент времени может существовать в нескольких воплощениях; одно будет соответствовать ло-
818
Глг
гическому шрифту с кеглем 12 для экрана с разрешением 96 dpi, другое — логическому шрифту с кеглем 24 для принтера с разрешением 300 dpi, повернутому на 30 градусов. Реализация физического шрифта — сложная и длительная операция, требующая больших затрат памяти. Как было показано в главе 14, шрифты TrueType имеют весьма сложную структуру, и анализ и поиск в их исходной форме весьма затруднены. Чтобы найти глиф для символа конкретной кодировки, символ приходится преобразовывать в Unicode и затем находить индекс глифа по специальной таблице. В процессе построения глифа его контуры масштабируются по заданным размерам с учетом инструкций, хранящихся в шрифте, а затем преобразуются в растровую форму. Примитивная реализация, при которой вывод каждого символа начинается с отображения кода символа на индекс глифа, окажется непомерной нагрузкой для быстродействия системы. На практике графический механизм поддерживает сложные структуры данных, описывающие соответствие между объектами логических шрифтов и физическими шрифтовыми файлами. В Windows NT/2000 для каждого используемого физического шрифта в адресном пространстве ядра создается структура данных (PFF), содержащая связный список реализаций шрифта (FONTOBJ). В каждой структуре FONTOBJ имеется кэш построенных глифов для их повторного использования. В каждом объекте логического шрифта имеется указатель на недокументированный объект GDI — объект PFE, содержащий ссылки на соответствующие объекты PFF. Эта сложная организация данных обеспечивает возможность эффективного кэширования реализованных шрифтов и их многократного использования на системном уровне, одновременно позволяя GDI свободно создавать, использовать и удалять логические шрифты. Внутренние структуры данных графического механизма, относящиеся к работе со шрифтами, подробно описаны в главе 3. После того как логический шрифт выбран в контексте устройства, вы можете получить дополнительную информацию о подобранном физическом шрифте и о метриках текущей реализации физического шрифта. Для этого используются следующие функции: int DWORD int int
GetTextFace(HDC hDC. int nCount. LPSTR IpFaceName): GetFontLanguageInfo(HDC hDC): GetTextCharSet(HDC hDC); GetTextCharSetlnfoCHDC hDC. LPFONTSIGNATURE IpSig, DWORD dwFlags):
BOOL GetTextMetrics(HDC hDC. LPTEXTMETRIC Iptm): UINT GetOutlineTextMetrics(HDC hDC. UINT cData. LPOUTLINETEXTMETRIC IpOtm); Функция GetTextFace возвращает имя гарнитуры физического шрифта, соответствующего логическому шрифту в контексте устройства (может отличаться от имени гарнитуры, использованного при создании логического шрифта). Функция GetFontLanguagelnfo возвращает информацию о текущем шрифте, выбранном в контексте устройства, в том числе о поддержке двухбайтовых глифов, о присутствии диакритических знаков, о наличии таблицы кернинга и т. д. Информация, полученная при помощи этой функции, часто применяется при нетривиальном форматировании текста (например, при непосредственной работе с глифами с использованием функций GetCharacterPlacement или ExtTextOut).
Получение информации о логическом шрифте
819
Функция GetFontLanguagelnfo возвращает комбинацию нескольких флагов, самыми распространенными из которых являются FLI_GLYPHS (0x40000) и GCP_ USERKERNING (0x0008). Флаг FLI_GLYPHS означает, что шрифт содержит дополнительные глифы, к которым невозможно обратиться через текущую кодировку. Флаг GCPJJSERKERNING означает, что в шрифте присутствует таблица кернинга. Если на компьютере реализована поддержка арабского языка или иврита, для некоторых шрифтов TrueType функция GetFontLanguagelnfo возвращает флаги GCP_REORDER, GCP_GLYPHSHARE, GCP_LIGATE, GCPJIACRITIC и GCP_KASHIDA. Например, шрифты Arial, Lucida Sans Unicode, Tahoma и Andalus поддерживают GCP_ REORDER, a «Times New Roman» не поддерживает. Функция GetTextCharset возвращает идентификатор набора символов для текущего шрифта, выбранного в контексте устройства. По полученному значению можно проверить, поддерживает ли физический шрифт набор символов, необходимый для логического шрифта. Например, если при создании логического шрифта был запрошен набор HANGUL_CHARSET, но в вашей системе нет ни одного корейского шрифта, GDI может выбрать набор символов по умолчанию; в этом случае функция GetTextCharset вернет ANSI_CHARSET. Функция GetTextCharsetlnfo возвращает структуру FONTSIGNATURE с информацией о поддиапазонах Unicode и кодировках, поддерживаемых шрифтом TrueТуре/OpenType. Первые четыре двойных слова FONTSIGNATURE образуют 128-разрядное поле USB (Unicode subset bitfield), а два оставшихся двойных слова образуют 64-разрядное поле СРВ (code page bitfield). Например, бит 9 USB показывает, поддерживаются ли глифы кириллицы, а бит 16 СРВ является признаком поддержки кодировки 874 (тайская). Для шрифта Tahoma в СРВ устанавливаются 12 бит, поскольку этот шрифт содержит глифы 12 разных кодировок. Функции GetTextMetrics и GetOutlineTextMetrics возвращают важные метрические данные о текущей реализации физического шрифта. Функция GetTextMetrics возвращает структуру TEXTMETRIC. Функция GetOutlineTextMetrics возвращает для шрифтов TrueType/OpenType структуру OUTLINETEXTMETRIC — расширенную версию TEXTMETRIC, содержащую дополнительную информацию.
Метрики растровых и векторных шрифтов Структура TEXTMETRIC изначально разрабатывалась для заголовков растровых и векторных шрифтов Windows, но она подходит и для шрифтов TrueType/OpenType. Чтобы получить заполненную структуру TEXTMETRIC для текущего шрифта, выбранного в контексте устройства, вызовите функцию GetTextMetrics с манипулятором контекста устройства и указателем на TEXTMETRIC. Структура TEXTMETRIC определяется следующим образом: typedef struct tagTEXTMETRIC { LONG LONG LONG LON'J LONG LONG
tmHeight: tmAscent: tmDescent: tmlnternalLeading: tmExternalLeading: tmAveCharWidth:
820 LONG LONG LONG LONG LONG BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE BYTE } TEXTMETRIC;
Глава 15. Текст tmMaxCharWidth; tmWeight: tmOverhang: tmDigitizedAspectX; tmDigitizedAspectY: tmFirstChar: tmLastChar; tmDefaultChar; tmBreakChar: tmltalic; tmUnderlined; tmStruckOut: tmPltchAndFamily; 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 в поле IfQuality структуры LOGFONT) GDI иногда не удается точно подобрать шрифт для заданного размера. Например, для 10-пунктового шрифта гарнитуры Terminal поле IfHeight в структуре IfHeight равно -13, но поле tmHeight может быть равно 12 при внутреннем зазоре, равном 0. Если не удается подобрать растровый шрифт заданного размера, обычно используется меньший шрифт. Сумма tmHeight и tmExternal Leading составляет рекомендуемый межстрочный интервал. Высота п строк текста вычисляется по формуле tmHeight х п + tmExternal Leading х (п - 1), поскольку перед первой и за последней строкой дополнительный промежуток не нужен. Допустим, у шрифта с кеглем 36 пунктов в экранном контексте с разрешением 96 dpi и в режиме MMJTEXT поле tmHeight равно 55, а поле tmExternalLeading равно 2. Общая высота строки равна 57 единицам или 42,75 пункта. Следует помнить, что разные шрифты одного кегля могут иметь разные значения полей tmHeight и tmExternal Leading или их суммы. Поле tmAveCharWidth определяет среднюю ширину символов шрифта. В документации Microsoft сказано, что средняя ширина обычно равна ширине строч-
Получение информации о логическом шрифте
821
ной буквы «х». В шрифтах TrueType/OpenType средняя ширина символов более точно вычисляется как взвешенная сумма ширин строчных букв a-z латинского алфавита и пробела. В моноширинных шрифтах все символы имеют одинаковую ширину, и поле tmAveCharWidth может использоваться для вычисления длины символьной строки. Средняя ширина символов также используется при подборе шрифтов. В поле tmMaxCharWidth хранится максимальная ширина символов шрифта. Поле tmWeight определяет насыщенность шрифта и совпадает с полем IfWeight структуры LOGFONT. Следует учитывать, что все ресурсы растровых и векторных шрифтов, а также физические шрифты TrueType обладают фиксированной насыщенностью. Гарнитура TrueType обычно содержит четыре физических начертания — обычное, курсивное, полужирное и полужирное курсивное. Насыщенность первых двух начертаний обычно равна 400 (FW_NORMAL), а двух последних — 700 (FW_BOLD). GDI пытается найти оптимальное соответствие для заданной насыщенности среди доступных шрифтов. Если точное совпадение найти не удается, GDI имитирует нужную насыщенность при помощи простого алгоритма. Поле tmOverhang определяет величину дополнительных промежутков, используемых GDI при синтезе шрифтов. Если требуемая насыщенность превышает доступную, GDI утолщает символы; если запрашивается курсивное начертание, а физический курсивный шрифт недоступен, GDI слегка наклоняет глифы. В любом случае размер строки немного увеличивается. Поле tmOverhang позволяет приложению точно рассчитать горизонтальные размеры строки символов, чтобы она не вышла за пределы отведенного ей места. На практике у растровых и векторных шрифтов это поле отлично от нуля, а у шрифтов TrueType/OpenType оно всегда равно нулю. Например, для курсивного шрифта Wingdings с кеглем 72 пункта, имеющего только обычное физическое начертание, в поле tmOverhang возвращается 0. Поле tmFirstChar определяет первый символ, для которого в шрифте имеется глиф; поле tmLastChar определяет последний символ. Оба поля объявляются с типом BCHAR (WCHAR в Unicode, BYTE в остальных кодировках). Растровые и векторные шрифты содержат глифы для всех символов в интервале от tmFi rstChar до tmLastChar. В шрифтах TrueType/OpenType однобайтовых версий полей tmFirstChar и tmLastChar недостаточно для представления истинного интервала символов, для которых в шрифте имеются глифы, а Unicode-версии полей не означают, что шрифт содержит глифы для всех символов между tmFirstChar и tmLastChar. Поле tmDefaultChar определяет символ замены для символов, не имеющих глифа в шрифте (обычно это прямоугольная рамка). В шрифтах TrueType по умолчанию обычно используется первый глиф с индексом 0. Поле tmBreakChar задает символ, по которому определяются границы слов при выравнивании текста. Следующие три поля повторяют поля LOGFONT с аналогичными именами. Поле t m l t a l i c отлично от нуля, если требуется курсивное начертание; поле tmUnderline отлично от нуля, если требуется подчеркивание символов, а поле tmStruckOut отлично от нуля, если требуется перечеркивание символов. Помните, что эти поля отражают лишь требования, указанные в объекте логического шрифта, а не характеристики физического шрифта. Начертания, отсутствующие в физическом шрифте, синтезируются средствами GDI.
822
Глава 15. Текст
Поле tmPitchAndFamily задает тип, технологию и семейство физического шрифта. Старшая половина поля совпадает со старшей половиной поля IfPitchAndFamily структуры LOGFONT. Младшая половина состоит из четырех независимых флагов и отличается от младшей половины IfPitchAndFamily. О TMPF_FIXED_PITCH (0x01) — устанавливается для пропорциональных шрифтов. Все поставлено с ног на голову — почему было не назвать это поле TMPF_PROP_PITCH? О TMPFJ/ECTOR (0x02) — устанавливается для контурных шрифтов (проще говоря, для всех, кроме растровых). О TMPF_TRUETYPE (0x04) — устанавливается для шрифтов TrueType/OpenType. О TMPF_DEVICE (0x08) — устанавливается для шрифтов устройств. Последнее поле tmCharSet определяет набор символов логического шрифта. Его значение совпадает с полем IfCharSet структуры LOGFONT (если это не DEFAULT_ CHARSET) и с возвращаемым значением функции GetTextCharSet. Шрифт TrueType/ ОрепТуре обычно поддерживает несколько наборов символов, информацию о которых можно получить при помощи функции GetTextCharlnfo.
Метрики шрифтов TrueType/OpenType Шрифты TrueType/OpenType обладают значительно большим количеством метрик, для которых была разработана структура OUTLINETEXTMETRIC. Приставка «outline» может вызвать недоразумения, поскольку эта структура не относится к векторным шрифтам — только к шрифтам TrueType/OpenType и шрифтам устройств, для которых шрифтовой драйвер может предоставить структуру OUTLINETEXTMETRIC. Ниже приведено определение структуры OUTLINETEXTMETRIC. typedef struct _OUTLINETEXTMETRIC { UINT otmSize; TEXTMETRIC otmTextMetrics: BYTE otmFiller; PANOSE otmPanoseNumber; UINT otmfsSelection; UINT otmfsType; int otmsCharSlopeRise: int otmsCharSlopeRun: int otmltalicAngle; UINT otmEMSquare; int otmAscent; int otmDescent; UINT otntineGap: UINT otmsCapEmHeight: UINT otmsXHeight; RECT otmrcFontBox; i nt otmMacAscent: int otmMacDescent; UINT otmMacLineGap; UINT otmusMinimumPPEM; POINT otmptSubscriptSize: POINT otmptSubscriptOffset: POINT otmptSuperscriptSize:
Получение информации о логическом шрифте
823
POINT otmptSuperscriptOffset; UINT otmsStrikeoutSize: int otmsStrikeoutPosition: int otmsUnderscoreSize: int otmsllnderscorePosltion; 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: KOutlineTextMetric(HOC hOC): -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 байт. В результате поле otmFi Пег оказалось выровненным по границе двойного слова, а перед ним добавились три байта. Структура PANOSE имеет длину 10 байт; следующее поле otmfsSelection должно начинаться с границы двойного слова, поэтому перед ним добавляются еще два байта. Обычно при специальном выравнивании полей структуры в заголовочные файлы Windows включается директива ^pragma pack, которая обеспечивает нужный тип выравнивания и отменяет тип выравнивания, заданный в пользовательской программе. Это гарантирует, что приложение будет работать с одними и теми же структурами Win32 API независимо от конфигурации проекта. В файле wingdi.h для структуры OUTLINETEXTMETRIC такая директива отсутствует. На момент написания книги выравнивание по границе DWORD в заголовочном файле имело место лишь в случае определения макроса _МАС. Если в вашем проекте требуется выравнивание полей структур по границе 1 или 2 байт и вы хотите использовать структуру OUTLINETEXTMETRIC, обязательно перейдите на выравнивание по границе двойного слова: #pragma pack(push. 4) #include<windows.h> fpragma pack(pop)
Первое поле otmSize определяет размер всей структуры вместе с внедренными строками. За ним следует структура TEXTMETRIC (см. выше), в точности совпадающая с той, которая возвращается при вызове GetTextMetric. Третье поле otmFi 11 ег предназначалось для выравнивания следующей за ним структуры PANOSE по границе слова. Похоже, исходные тексты GDI компилировались с выравниванием полей структур по границе 4 или 8 байт, в результате чего после otmTextMetrics и перед otmFi 11 ег появились три скрытых байта. В поле 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
Поле otmfsType определяет лицензионные права на внедрение шрифта в документы (см. раздел «Установка и внедрение шрифтов» в главе 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 — типографский подстрочный интервал, а поле otmLi neGap — типографский межстрочный интервал. GDI использует собственную интерпретацию надстрочных и подстрочных интервалов, а также внутреннего и внешнего зазора, которая в одних случаях совпадает с типографской интерпретацией, а в других отличается от нее. Еще одно различие состоит в том, что поле otmDescent обычно содержит отрицательную величину, поскольку подстрочный элемент расположен ниже базовой линии, а в Windows всегда используется модуль (абсолютное значение) этой метрики. Группа полей otmMacAscent, otmMacDescent и otmMacLi neGap содержит вертикальные метрики шрифта для Macintosh. В документации Microsoft сказано, что поля otmCapEmHeight и otmXHeight не поддерживаются. Вероятно, поле OtmCapEmHeight должно содержать высоту прописной буквы без подстрочного элемента, а поле otmXHeight — высоту строчной буквы «х». Поле otmrcFontBox определяет ограничивающий прямоугольник всех глифов шрифта относительно базовой точки символа. Поле otmrcFontBox.left обычно имеет отрицательное значение, соответствующее минимальной А-метрике, а поле OtmrcFontBox.bottom имеет отрицательное значение, соответствующее наибольшему подстрочному элементу. Поле otmusMinimumPPEM определяет минимальный допустимый размер в пикселах, до которого можно уменьшить em-квадрат по рекомендации разработчика
Глава 15. Текст
826
шрифта. По значению этого поля можно судить о том, насколько хорошо инструкции привязки подходят для построения при малом размере символов. Обычное значение равно 9 или 12 пикселам. Поля otmptSubscriptSize и otmptSubscriptOffset определяют размер и позицию нижних индексов шрифта от базовой точки символа. Поля otmptSuperscriptSize и otmptSuperscriptOffset содержат аналогичные данные для верхних индексов. Поля otmsStrikeoutSize и otmsStrikeoutPosition определяют толщину горизонтальной черты при перечеркивании символов и ее положение относительно базовой линии. Поля otmsUnderscoreSize и otmsUnderscorePosition определяют толщину и положение относительно базовой линии черты, используемой для подчеркивания. На рис. 15.5 показаны некоторые новые метрики, содержащиеся в структуре OUTLINETEXTMETRIC, — а именно ограничивающий прямоугольник, метрики верхнего/нижнего Индексов, подчеркивания, перечеркивания и наклона символов. Пять пунктирных линий обозначают уровни внешнего зазора, надстрочного элемента, внутреннего зазора, базовой линии и подстрочного элемента. Большой контур соответствует ограничивающему прямоугольнику шрифта и кажется слишком большим. Две серые полоски имитируют подчеркивание и перечеркивание. Два прямоугольника справа обозначают базовую точку и размеры верхних/нижних индексов. Наклонная линия представляет угол наклона символов, используемый при выводе каретки.
827
Получение информации о логическом шрифте
Структура LOGFONT и метрики шрифта Программа Font, прилагаемая к этой главе, поможет вам лучше понять связь между логическими шрифтами, определяемыми структурой LOGFONT, и физическим шрифтом. Эта программа модифицирует стандартное диалоговое окно для выбора шрифта и выводит в нем всю информацию о шрифте. Win32 API содержит функцию ChooseFont для вывода стандартного диалогового окна, в котором пользователь выбирает логический шрифт. Эта функция позволяет приложению переопределить стандартный механизм обработки сообщений в диалоговом окне. Передавая функцию ChooseFont косвенного вызова, мы выводим дополнительную информацию о текущей структуре LOGFONT и метриках текущей реализации физического шрифта. В программе Font на прилагаемом компакт-диске диалоговое окно шрифта расширяется вправо, и в нем выводится иерархическое дерево со всеми атрибутами шрифта. Измененное диалоговое окно выбора шрифта показано на рис. 15.6. В левой части (исходное диалоговое окно выбора шрифта) выбран полужирный шрифт Times New Roman с кеглем 72 пункта. В дочернем иерархическом дереве (TreeView) выводятся результаты вызовов GetTextFace, GetFontLanguagelnfo, GetTextCharset, GetTextCharsetlnfo, GetTextMetrics и GetOutlineTextMetrics для структуры LOGFONT. На рис. 15.6 структура OUTLINETEXTMETRIC развернута, в ней видны вложенные структуры TEXTMETRIC и PANOSE и несколько начальных полей. Если выделить в левой части окна другую строку и щелкнуть на кнопке Apply, содержимое иерархического дерева синхронизируется с выбранной строкой.
Trebuchet MS Verdana Webdings Wide Latin
fir LOGFONT GetTextFace: Times New Roman GetFontlanguagelnfo: 0x40008 GetTextOiarset: 0x0 &• FONTSIGNATURE Ш-TEXTMETRIC olmSize: 330 Ш- otmTextMetrics: TEXTMETRIC i+! otmPanoseNumoer: PANOSE otmlsSelection: 0x21
otmfsType: 0 OtmsCharSlopeRise: 24 otmsChaiSlopeRun: 7 otmltalicAngle:-164 otmEMSquare: 2048 olmAscent: 65 olmDescent: -21
-a
Рис. 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).
Рис. 15.6. Измененное диалоговое окно выбора шрифта с выводом шрифтовых метрик
Точность шрифтовых метрик При создании логического шрифта его размеры определяются полями (или параметрами) ширины/высоты. При подборе физического шрифта его метрики em-квадрата масштабируются по размерам логического шрифта и возвращаются
828
Глава 15. Текст
в структуре TEXTMETRIC или OUTLINETEXTMETRIC. Когда логический шрифт выбирается в контексте устройства, его ширина и высота интерпретируются в логических координатах данного контекста, поэтому все метрики TEXTMETRIC и OUTLINETEXTMETRIC задаются в логической системе координат данного контекста устройства. Метрические данные используются при разбиении длинного текста на строки в абзацах и при размещении текста на страницах. Если приложение поддерживает печать документа, очень важно, чтобы разрывы строк и страниц на экране точно соответствовали разрывам строк и страниц при печати документа на разных принтерах. Кроме того, разрывы строк и страниц должны сохраняться при выводе экрана в другом масштабе. Это одно из основных требований концепции WYSIWYG (What You See Is What You Get — «что видите, то и получаете»). Допустим, вы пишете простейший текстовый редактор, в котором весь текст выводится одним шрифтом. Возникает вопрос: как по заданному кеглю и высоте страницы вычислить количество строк, помещающихся на странице? Ниже приведен один из возможных вариантов. int LinesPerPage(HDC hDC. int nPolntSize int nPageHeight) { KLogFont lf(-PointSizetoLogical(hDC. nPointSize). "Times New Roman"): HFONT hFont = If.CreateFontO: HGDIOBJ hOld = SelectObjecUhDC, hFont); TEXTMETRIC tm: GetTextMetrics(hDC. & tm); int linespace = tm.tmHeight SelectObjectthDC. hOld): DeleteObject(hFont):
tm.tmExternalLeading;
POINT P[2] = { 0, 0, 0. nPageHeight DPtotP(hDC. P. 2); nPageHeight = abs(P[l].y-P[0].y): ejinespace e_pageheight e_externalleading
// Координаты устройства // Логические координаты
= linespace;
= nPageHeight: = tm.tmExternalLeading;
return (nPageHeight + tm.tmExternalLeading) / linespace: Функция LinesPerPage получает манипулятор контекста устройства, кегль шрифта и высоту страницы в системе координат устройства (в пикселах). Она преобразует кегль в логические координаты, создает логический шрифт, выбирает его в контексте устройства и запрашивает вертикальные метрики шрифта. Расстояние между двумя строками вычисляется как сумма высоты и внешнего зазора. После преобразования высоты страницы (за вычетом полей) в логические координаты количество строк на странице вычисляется по формуле (высота страницы + внешний зазор)/расстояние между строками. Внешний зазор прибавляется к высоте страницы, поскольку N строк разделяются всего (N — 1) внешними зазорами.
829
Получение информации о логическом шрифте
Следующий вопрос: насколько точны подобные вычисления? В табл. 15.1 приведены примеры данных для высоты страницы, равной 10 дюймам (11-дюймовый лист формата Letter с полями по 0,5 дюйма сверху и снизу). Таблица 15.1. Разрывы строк Устройство
Режим отображения
Логическая высота страницы
Внешний зазор
Логический Строк на межстрочный •страницу интервал
Экран (96 dpi)
MMJEXT
960
1
16
60,0625 (60)
MM_LOENGLISH
1050
1
17
61,8235 (61)
MM TWIPS
15118
9
245
61,74286 (61)
MMJEXT
1200
1
20
60,05 (60)
MM_LOENGLISH
1312
1
22
59,6818 (59)
MM_TWIPS
18897
И
310
60,9935 (60)
Принтер (360 dpi)
MMJEXT
3600
2
59
61,0508 (61)
Принтер (600 dpi)
MMJEXT
6000
4
98
61,2653 (61)
Экран (120 dpi)
Как видно из таблицы, в зависимости от логического разрешения экрана, режима отображения и устройства (экран или принтер) по шрифтовым метрикам, возвращаемым в структуре TEXTMETRIC, на простой вопрос о количестве строк в области высотой 10 дюймов можно получить три разных ответа: 59, 60 и 61. Обратите внимание: функция LinesPerPage усекает дробный результат до целой части, поскольку неполные строки не выводятся на экране. Даже если бы мы воспользовались округлением до ближайшего целого, все равно получилось бы три разных ответа: 60, 61 и 62. Также следует учитывать, что даже в одном стандартном режиме отображения (MMJ.OENGLISH или MMJTWIPS) на одном и том же компьютере можно получить разные результаты при переходе от режима мелких шрифтов (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 поле высоты в структуре LOGFONT будет равно -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 и OUTLINETEXTMETRIC задаются в логических координатах. Например, если переключить экранный контекст с разрешением 96 dpi в режим MM_ANISOTROPIC, задать габариты окна (100,100) и габариты области просмотра (1,1), логическая система координат будет иметь разрешение 96 dpi. Иначе говоря, перемещение на 9600 единиц в логических координатах будет соответствовать 1 логическому дюйму на экране. Казалось бы, такая система координат должна обеспечивать значительно большую точность, чем контексты принтеров с разрешением 600 dpi, получившие столь широкое распространение. Как ни странно, на практике все получается совсем не так. Увеличение разрешения в логической системе координат не приводит к повышению точности шрифтовых метрик. Похоже, графический механизм допускает громадную ошибку, сначала масштабируя метрики в координатах устройства, а затем — в логических координатах. Хотя не исключено, что результаты масштабирования на первом этапе сохраняются в формате с фиксированной точкой, при масштабировании в логическую систему координат с высоким разрешением особых улучшений не наблюдается. Ниже приведена небольшая функция, которая проверяет, как разрешение логической системы координат влияет на точность текстовых метрик, void Test_LC(void) { HOC hDC = GetDC(NULL): SetMapMode(hDC. MM_ANISOTROPIC); SetViewportExtEx(hDC. 1. 1. NULL); TCHAR mess[MAX PATH];
Получение информации о логическом шрифте
831
mess[0] = 0: for (int i=l: i<=64; i*=2) SetWindowExtEx(hDC. i. i. NULL); KLogFont lf(-PointSizetoLogical(hDC. 24). "Times New Roman"): HFONT hFont = If.CreateFontO: SelectObject(hDC, hFont); TEXTMETRIC tm; GetTextMetrics(hDC. & tm): wsprintf(mess + _tcslen(mess). "%u:l lfHeight=Ud. tmHeight=Ud\n". i. If.mJf.lfHeight. tm.tmHeight); SelectObject(hDC. GetStockObject(ANSI_VAR_FONT)); DeleteObject(hFont); ReleaseDC(NULL. hDC); MessageBox(NULL. mess. "LCS vs. TEXTMETRIC". MBJDK):
При каждой итерации функция последовательно увеличивает логическое разрешение, создает шрифт с кеглем 24 пункта, выбирает его в контексте устройства и запрашивает высоту шрифта. На экране с логическим разрешением 96 dpi поле If Height принимает значения -32, -64 и т. д. до -2048, а поле tmHeight изменяется от 36, 72 до 2304. Значение tmHeight тоже каждый раз удваивается. Из чего же следует, что этот результат неверен? Попробуйте создать шрифт с кеглем 24 х 64 = 1536 пунктов в измененном диалоговом окне выбора шрифта, показанном на рис. 15.6. Поле If Height сохраняет то же значение 2048, но поле tmHeight равно 2268, а не 2304. Итак, увеличение разрешения логической системы координат не повышает точности шрифтовых метрик — по крайней мере, в текущей реализации GDI.
Кегль и точность Второй способ увеличения логического размера шрифта для повышения точности шрифтовых метрик предельно прост — нужно увеличить кегль шрифта. Ниже приведена аналогичная функция для проверки связи между кеглем и точностью шрифтовых метрик. void Test_Point(void) HDC hDC = GetDC(NULL);
TCHAR mess[MAX_PATH*2]; mess[0] = 0: for (int 1=1: i<=64: i*=2) KLogFont lf(-PointSizetoLogical(hDC. 24*1). "Times New Roman"); HFONT hFont = If.CreateFontO: SelectObject(hDC. hFont):
832
Глава 15. Текст
TEXTMETRIC tm: GetTextMetrics(hDC. & tm): wsprintfCmess + _tcslen(mess). "%d point lfHeight=2d, tmHeight=Xd\n". 24*i. If.mjf.IfHeight. tm.tmHeight): SelectObject(hDC, GetStockObject(ANSI_VARJONT)); DeleteObject(hFont): ReleaseDC(NULL, hDC): MessageBox(№JLL, mess. "Point Size vs. TEXTMETRIC", MB_OK): При увеличении кегля до 1536 пунктов поле IfHeight равно -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) LOGFONT
If:
OUTLINETEXTMETRIC otra[3]; // С учетом строк HDC hDC
= GetDC(NULL):
HGDIOBJ hOld = SelectObject(hDC. hFont): int size = GetOutlineTextMetrics(hDC. sizeof(otm). otm); SelectObjecUhDC, hOld); ReleaseDC(NULL. hDC): if ( size )
// TrueType
GetObject(hFont. sizeof(lf). & If): emsquare = otm[0].otmEMSquare: // Получить размер ЕМ-квадрата If.IfHeight = - emsquare: // Соответствие 1:1 If.IfWidth = 0: // Исходные пропорции return CreateFontlndirect(Slf);
} else return NULL:
Простой вывод текста
833
Помимо функций, упоминавшихся в этом разделе, существуют и другие функции получения метрических данных шрифтов. Они будут рассмотрены ниже, при обсуждении вывода и форматирования текста средствами GDI.
Простой вывод текста Контекст устройства обладает рядом атрибутов, используемых всеми функциями вывода текста. К числу этих атрибутов относится цвет текста и цвет фона, режим заполнения фона, тип выравнивания текста и т. д. COLORREF SetTextColor(HDC hDC. COLORREF crColor); COLORREF GetTextColor(HDC hDC): COLORREF SetBkColor(HDC hDC. COLORREF crColor); COLORREF GetBkColor(HDC hDC): int SetBkMode(HDC hDC. int iBkMode); int GetBkModeCHDC hDC): Функция SetTextCol or задает цветовую ссылку (COLORREF), которая используется для вывода основных пикселов текстовой строки. Основными считаются пикселы, которые образуют внутренние области глифов в строке. Функция SetTextCol or возвращает предыдущий цвет текста для заданного контекста устройства. На всякий случай напоминаем, что цветовые ссылки задаются тремя способами: Р6В(красный, зеленый, синий), РА1_ЕТТЕ1МиЕХ(индекс) и 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 GetTextAligntHDC hDC):
834
Глава 15. Текст
Функция TextOut выводит текстовую строку в заданной позиции, используя текущие значения управляющих атрибутов — шрифт, выбранный в контексте устройства, цвета текста и фона, режим заполнения фона и т. д. Выводимая строка задается указателем на первый символ и количеством символов. Таким образом, выводимый текст не обязательно завершать нуль-символом. Символы строки должны входить в набор символов текущего шрифта. Например, при использовании набора ANSI все управляющие символы выводятся глифом по умолчанию (глифом отсутствующего символа). Точная интерпретация параметров nXStart и nYStart определяется специальным атрибутом контекста устройства — типом выравнивания текста. Тип выравнивания задается в виде комбинации нескольких флагов (табл. 15.2). Таблица 15.2. Выравнивание текста
Группа
По вертикали
Флаг
Описание
ТА_ТОР
Верхний край (надстрочная линия) текста совмещается с nYStart
TA_BASELINE ТА BOTTOM
Базовая линия текста совмещается с nYStart Нижний край (подстрочная линия) текста совмещается с nYStart
По горизонтали
const TCHAR * mess - "Align"; for (int 1=0: i<3; i++. x+=250) { const UINT Align[] = { TAJOP | TAJ.EFT. TAJASELINE | TA_CENTER, TAJOTTOM TA_RIGHT }; SetTextAlign(hDC. A11gn[i] | TAJJPDATECP); MoveToEx(hDC. x. y. NULL); // Установка текущей позиции TextOutChDC. x. у, mess. _tcslen(mess)): POINT cp: MoveToEx(hDC. 0, 0, & cp); // Получение текущей позиции LineChOC, 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);
Левый край текста совмещается с nXStart Горизонтальный центр текста совмещается с nXStart
LineChDC. x. y-75. x. y+75); // Вертикальный ориентир
Правый край текста совмещается с nXStart
SIZE size;
Использовать nXStart, nYStart; текущая позиция не обновляется при выводе текста
GetTextExtentPoint32(hDC, mess. _tcslen(mess). & size);
ТА UPDATECP
Игнорировать nXStart, nYStart; использовать текущую позицию. Текущая позиция обновляется при каждом выводе текста Справа налево
int x = 50; int у = 110;
TA_CENTER
TA_NOUPDATECP
TA_RTLREADIN6
Текст выводится справа налево. В Windows 2000 этот порядок возможен лишь при реализованной поддержке арабского языка или иврита. Ранее этот флаг поддерживался лишь в версиях ОС для арабского языка и иврита
Флаги выравнивания текста делятся на четыре группы и управляют выравниванием по вертикали и горизонтали, обновлением текущей позиции и возможностью вывода текста справа налево в иврите/арабском языке. Флаги разных групп объединяются логической операцией OR. По умолчанию используется значение TAJTOP | TA_LEFT | TAJOUPDATECP. При установке флага TAJJPDATECP GDI игнорирует начальную позицию, заданную координатами (nXStart, nYStart). Текст выводится с текущей позиции контекста устройства, причем каждая операция вывода обновляет горизонтальную координату текущей позиции.
835
В следующем фрагменте демонстрируются некоторые интересные комбинации флагов выравнивания текста. SetTextColor(hDC. RGBCO. О, 0)}; // Черный SetBkColor(hOC. RGBCOxDO. OxDO. OxDO)); // Серый SetBkMode(hDC. OPAQUE); // Непрозрачный
TA_LEFT
ТА RIGHT
Обновление
Простой вывод текста
BoxChDC. x, y, x + size.ex. у + size.су): } x -= 250 * 3; LineChDC. x-20. у. х+520, у); // Горизонтальный ориентир В этом фрагменте назначается черный цвет текста и светло-серый цвет фона при непрозрачном режиме заполнения фона; текст выводится в светло-сером прямоугольнике, обозначающем границы области вывода. Затем программа трижды выполняет тело цикла, демонстрируя разные комбинации горизонтального и вертикального выравнивания. Начальная точка каждого вызова отмечена перекрестием, а конечная точка обозначается наклонным крестиком. Пример приведен на рис. 15.7. При установке флагов ТА_ТОР | TAJ.EFT с начальной точкой совмещается левый верхний угол текстовой области. Если установлен флаг TAJJPDATECP, начальная точка находится в текущей позиции контекста и обновляется координатами правого верхнего угла текстовой области. При установке флагов TA_BASELINE | TA_CENTER с начальной точкой совмещается точка пересечения базовой линии и горизонтального центра текстовой области. Если установлен флаг TAJJPDATECP, начальная точка находится в текущей позиции контекста, которая при выводе не обновляется.
836
Глава 15. Текст
TAJTOP | TA_LEFT
TA_BASELINE | TA_CENTER
TA_BOTTOM | TA_RIGHT
Шё&
n
Рис. 15.7. Выравнивание текста
рецком, вьетнамском, на языках Западной Европы и американском диалекте английского.
-.• Jjxj
fews, and dates. Set tefasal English (United States)
При установке флагов ТА_ВОТТОМ | TA_RIGHT с начальной точкой совмещается правый нижний угол текстовой области. Если установлен флаг TA_UPDATECP, начальная точка находится в текущей позиции контекста и обновляется координатами левого нижнего угла текстовой области.
Вывод текста справа налево Мы привыкли, что текст выводится слева направо, а страница заполняется горизонтальными строками сверху вниз. Однако это лишь одна из многих систем письма, существующих в мире. В арабском языке и иврите большая часть текста пишется справа налево, хотя страница также заполняется горизонтальными строками сверху вниз. В традиционной японской и китайской письменности иероглифы пишутся сверху вниз, а вертикальные столбцы текста заполняют страницу справа налево. И в наши дни традиционная письменность используется во многих печатных изданиях - например, в газетах, журналах, художественной литературе. А в монгольском языке символы также записываются в вертикальные столбцы, но страница заполняется слева направо. Направление письма, стандартное для латиницы и кириллицы, хорошо поддерживается в GDI на уровне базовых средств вывода текста. Как будет показано ниже в этой главе, вертикальную письменность можно имитировать путем поворота и при помощи вертикальных шрифтов. С поддержкой вывода справа налево дело обстоит сложнее. Не существует специализированных версий Windows 2000 для других стран; для всего мира используется один и тот же двоичный код, а это означает, что в системе должна присутствовать встроенная поддержка других языков, включая языки с письменностью справа налево. При разработке Windows 2000 обеспечивалась поддержка чтения и создания документов на разных языках. Впрочем, поддержка других языков относится к числу дополнительных возможностей и реализуется отдельно с помощью панели управления (рис. 15.8). После установки системы поддержки других языков, включающей шрифты, файлы преобразования кодировок, методы ввода и т. д., Windows 2000 позволит читать и создавать документы на арабском и армянском языке, на языках стран Центральной Европы и кириллице, на грузинском, греческом, иврите, санскрите, китайском (в упрощенной и традиционной письменности), на тайском, ту-
837
Простой вывод текста
010001 (MAC-Japanese) El Ю002 (MAC • Traditional Chinese BigS) 010003 (MAC-Korean) 010004 (MAC-Arabic) 010005 (MAC-Hebrew) П Ю006(MAC-Greek I)
languages.
0 Hebrew 0 Indie 0 Japanese 0 Korean
J
0 Simplified Chinese
Ж,
Caned
Рис. 15.8. Установка системы поддержки других языков
На уровне приложения при создании окна функцией CreateWindowEx флаг WS_EX_RTLREADING указывает, что текст должен выводиться справа налево. Флаг WS_EX_RIGHT присваивает окну общий набор атрибутов выравнивания по правому краю. В Windows 2000 появился новый флаг WS_EX_LAYOUTRTL, при котором базовая точка окна перемещается к правому краю, а горизонтальные координаты увеличиваются справа налево. На уровне GDI в Windows 98 и Windows 2000 появился новый атрибут контекста устройства — раскладка (layout), для работы с которым используются две функции: DWORD Setl_ayout(HDC hDC. DWORD dwLayout): DWORD GetLayouttHDC hDC);
Для атрибута раскладки определены всего два флага. Флаг LAYOUT_BITMAPORIENTATIONPRESERVED запрещает зеркальное отражение растров, выводимых функциями BitBH, StretchBlt и т. д.; флаг LAYOUT_RTL задает общее направление горизонтальной раскладки справа налево. В тернарные растровые операции был добавлен новый бит NOMIRRORBITMAP (0x80000000), запрещающий горизонтальное отражение растров. При выводе текста флаг TA_RTLREADING сообщает, что GDI следует расположить текст в соответствии с правилами чтения справа налево. Практическая реализа-
838
Глава 15. Текст
ция вывода справа налево в Windows 2000 сосредоточена в новом компоненте GDI — UniScribe, API которого позволяет точно задать основные характеристики текста при выводе. Рассмотрим простой пример использования флагов LAYOUT_RTL и TA_RTLREADING. void Demo_RTL(HDC hDC, const RECT * rcPaint) { KLogFont lf(-PointSizetoLogical(hDC. 36), "Lucida Sans Unicode"): If.mJf.lfCharSet = ARABIC_CHARSET; . If.mJf.lfQuality = 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 \xDO\xDl\xD2":
for (int i=0; i<4; i++) { if ( i & 1 )
Set Text Align (hDC. TAJOP | TA_LEFT | TA_RTLREADING): else Set Text Align (HOC. TAJOP | TA_LEFT):
if ( i & 2 ) SetLayout(hDC. LAYOUT_RTL): else SetLayout(hDC. 0); TextOut(hDC, 10. 10 + linespace * i. mess. _tcslen(mess)); Функция Demo_RTL создает логический шрифт для арабского набора символов, используя гарнитуру Lucida Sans Unicode. На экран выводятся четыре строки с одним и тем же текстом, но с разными комбинациями флагов. Результаты показаны на рис. 15.9.
1-360-212-0000 JP jp 0000-212-360-1 jiP 0000-212-360-1 1-360-212-0000 jp Рис. 15.9. RTL_LAYOUT и TA_RTLREADING
Первая строка выводится слева направо и выравнивается по левому краю — ничего необычного. Вторая строка выводится справа налево со стандартной ле-
Простой вывод текста
839
восторонней раскладкой (флаг TA_RTLREADING). Обратите внимание: GDI при помощи UniScribe разбивает текстовую строку на слова и располагает их в порядке, соответствующем чтению справа налево. Строка выводится в той же позиции, но с обратным порядком следования символов. Третья и четвертая строки выглядят похоже, но они выводятся после установки флага RTL_LAYOUT в раскладке контекста устройства. Без флага TA_RTLREADING текст выравнивается по правому краю клиентской области и выводится справа налево. При выравнивании с флагом TA_RTLRADING текст возвращается к стандартному порядку вывода. Таким образом, флаг TA_RTLREADING меняет направление чтения на противоположное.
Дополнительные интервалы Функция TextOut обеспечивает выравнивание текста по левому/правому краю и по центру, но не поддерживает выравнивание по ширине (выключку). Выравнивание по ширине обычно означает вывод текста в горизонтальной области таким образом, что левый край текста выравнивается по левой стороне области, а правый край — по правой стороне. В процессе выравнивания по ширине пробелы в строке могут увеличиваться, обеспечивая совмещение правого края с правой стороной области. В GDI выравнивание по ширине состоит из нескольких шагов. Сначала GDI вычисляет точную ширину текстовой строки и сравнивает ее с шириной области вывода, чтобы узнать, сколько места необходимо компенсировать. Затем подсчитывается количество пробелов в строке, передаваемое при вызове функции SetTextJustification GDI. На последнем шаге функция TextOut выводит строку, выровненную по ширине. У контекста устройства имеется специальный атрибут, управляющий расстоянием между символами, — дополнительный межсимвольный интервал (character extra). Дополнительный межсимвольный интервал определяет целочисленную величину в логической системе координат — размер дополнительного промежутка, добавляемого после каждого символа при выводе текста. Ниже перечислены функции GDI, предназначенные для вычисления размеров текста, настройки межсимвольных интервалов и выравнивания. int SetTextCharacterExtratHDC hDC. int nCharExtra); int GetTextCharacterExtra(HDC hDC); BOOL GetTextExtentPoint32(HDC hDC. LPCTSTR IpString. int cbString. LPSIZE IpSize); BOOL SetTextJustification(HDC hDC, int nBreakExtra, int nBreakCount): По умолчанию дополнительный межсимвольный интервал в контексте устройства равен 0. Функция SetTextCharacterExtra присваивает ему новое целочисленное значение в логических координатах, возвращая предыдущее значение. Функция GetTextCharacterExtra просто возвращает текущее значение межсимвольного интервала. Межсимвольные интервалы могут использоваться как для разрядки, так и для уплотнения текста. Функция GetTextExtentPoint32 возвращает размеры символьной строки в логической системе координат. Высота равна высоте текущего шрифта, а ширина
840
Глава 15. Текст
определяется шириной отдельных символов, межсимвольными интервалами и параметрами выравнивания по ширине. Функция SetTextJustification задает величину дополнительного интервала, распределяемого между N ограничителями слов. Обычно ограничителем слова является пробел, но любой шрифт может переопределить этот символ. Ограничитель слова хранится в поле tmBreakChar структуры TEXTMETRIC. Ниже приведен небольшой пример использования функций SetTextCharacterExtra и GetTextExtentPoint32.
Простой вывод текста
считывает количество символов-ограничителей, настраивает выравнивание текста по ширине в контексте устройства, после чего выводит выровненную строку. BOOL TextOutJustfHDC hDC. int left, int right, int y. LPCTSTR IpStr. int nCount. bool bAllowNegative, TCHAR cBreakChar) { SIZE size; SetTextJustificat ion(hDC. 0. 0); GetTextExtentPoint32(hDC. IpStr. nCount. & size);
const TCHAR * mess = "Extra": SetTextAlignChDC. TAJ.EFT | TAJOP);
int nBreak = 0: for (int i=0; i
for (int i-0; i<3; i++) { SetTextCharacterExtraChDC. i*10-10): TextOut(hDC. x. y. mess. _tcslen(mess));
int breakextra = right - left - size.cx; if ( (breakextra<0) && ! bAllowNegative ) breakextra =0;
SIZE size; GetTextExtentPoint32(hDC. mess. _tcslen(mess). & size): BoxfhDC. x. y. x + size.ex. у + size.су); у += size.су + 10;
SetTextJustificationhDC. breakextra, nBreak): return TextOuUhDC, left. y. IpStr. nCount):
} SetTextCharacterExtra(hDC. 0): // Сбросить межсимвольный интервал
}
В этом фрагменте одна и та же строка выводится три раза с межсимвольными интервалами -10, 0 и 10. Функция GetTextExtentPoint32 возвращает размеры текстовой области, которая слева на рис. 15.10 обведена рамкой. Из рисунка видно, что GDI поддерживает как положительные, так и отрицательные межсимвольные интервалы. Межсимвольный интервал применяется после вывода символа. Если межсимвольный интервал отрицателен, между значением ширины текста, возвращаемым функцией GetTextExtentPoint32, и фактическим размером ограничивающего текст прямоугольника возникает расхождение в размере одного отрицательного интервала.
"Пге The The
Extra Extra
The The
841
quick
quick brown
quick brown fox quick brown fox jumps
The quick brown fox jumps over The quick brown fox jumps over the Thequickbrownfoxjumpsoverthelazy
Thequickbrownfoxjumpsoverthelazydog, The quick brown fox jumps over the lazy dog.
Рис. 15.10. Дополнительные межсимвольные интервалы и расширение промежутков между словами
Ниже все перечисленные этапы выравнивания текста по ширине объединены в одну удобную функцию TextOutJust. Функция вычисляет размеры текста, под-
Правая часть рис. 15.10 иллюстрирует пример использования функции TextOutJust. По ширине выравниваются части предложения — сначала одно слово, потом два, три слова и т. д. до полного текста. Как видно из рисунка, при выводе одного слова символы-ограничители в строке отсутствуют. При выводе двух слов расширяется только промежуток между словами, чтобы второе слово выровнялось по правой границе блока. С увеличением количества слов промежутки увеличиваются в равной степени. При некотором количестве слов промежутки между словами начинают сокращаться. Когда величина этих промежутков уменьшается до 0, текст начинает «перетекать» через правую границу, и тогда строку приходится разбивать на абзацы. Для сравнения последняя строка выведена со стандартными промежутками между словами и символами.
Ширина символа При использовании крупного шрифта можно заметить, что «левый край» текстовой области, совмещаемый с параметром nXStart функции TextOut при установке флага TA_LEFT, в действительности несколько отходит от левого края глифа. Точное расположение символов по горизонтали определяется их шириной v. метриками ABC шрифтов TrueType/OpenType. Ниже перечислены функции GDI, возвращающие информацию о ширине символов и метриках ABC шрифта, выбранного в контексте устройства. typedef struct _ABC { int abcA; UINT abcB: int abcC;
} ABC;
typedef struct _ABCFLOAT {
842
Глава 15. Текст
FLOAT abcfA; FLOAT abcfB; FLOAT abcfC; } ABC; BOOL GetCharABCWidths(HDC hDC. UINT uFlrstChat. UINT uLastChar. LPABC)BOOL GetCharABCWidthsFloat(HDC hDC. UINT uFirstChat, UINT uLastChar LPABCFLOAT IpABCF); BOOL GetCharWidth32(HDC hDC. UINT iFirstChar. UINT iLastChar LPINT IpBuffer): ' BOOL GetCharWidthFloat(HDC hDC. UINT iFirstChar. UINT iLastChar PFLOAT pxBuffer):
Функция GetCharABCWidths заполняет массив структурами ABC для всех символов в заданном интервале. Структура ABC определяет ширину символа в виде трех составляющих. Метрика А (левый отступ) определяет смещение начала глифа от текущей позиции курсора. Метрика В (ширина глифа) определяет ширину самого глифа. Метрика С (правый отступ) определяет смещение от конца глифа до следующей позиции курсора. Метрика В является целым без знака, поэтому ее значение должно быть положительным. Метрики А и С относятся к знаковому целому типу и могут принимать отрицательные значения. Функция GetCharABCWidthsFloat аналогична GetCharABCWTdths за одним исключением: в возвращаемой ею структуре ABCFLOAT вместо целых чисел используются вещественные числа одинарной точности. Значения, возвращаемые GetCharABCWidths и GetCharWidthsRoat, задаются в логической системе координат. Как ни печально, структура ABCFLOAT не обеспечивает повышения точности по сравнению со структурой ABC, кроме случаев, когда отображение из системы координат устройства в логическую систему координат осуществляется с дробными коэффициентами. Иначе говоря, обе функции берут значения ширины из внутренней таблицы, содержимое которой масштабируется по размерам текущего шрифта в системе координат устройства; для • структуры ABC - по целым, а для структуры ABCFLOAT - по вещественным числам в логической системе координат. Например, в режиме отображения MMJEXT обе структуры будут содержать одинаковые значения в разных форматах. Функция GetCharABCWidths работает только со шрифтами ТгаеТуре/ОрепТуре а функция GetCharABCWidthsFloat также поддерживает растровые и векторные шрифты. Для растровых и векторных шрифтов, глифы которых не имеют метрик А и С, функция GetCharABCWidthsFloat просто заполняет соответствующие поля нулями. Для любого шрифта, поддерживаемого системой Windows, всегда можно вызвать функцию GetCharWidth32 и заполнить массив значениями полной ширины символов из заданного интервала. Для шрифтов TrueType/OpenType полная ширина равна сумме метрик А, В и С, а для растровых и векторных шрифтов она равна ширине символа. Функция GetCharWidthFloat представляет собой вещественную версию GetCharWidth32. Похоже, реализация GetCharABCWidthsFloat в Windows 2000 содержит ошибку - возвращаемые значения составляют 1/16 от фактических. Впрочем, приложения все равно не используют эту функцию.
843
Простой вывод текста
Роль метрик ABC при выводе текста, а также их связь с размерами текстовой области, возвращаемыми функцией GetTextExtentPoint32, иллюстрирует рис. 15.11.
Т(0х66): -34 129 -32 'o'(OxBf): 6 91 Б 'п'(Охбе): 7 101 5 •Г[0х74): 9 63 -5 ABC widths sum: 346 GetTextExtent 346 218 GetTextABCExtent-34 385 -5
Рис. 15.11. Метрики ABC при выводе текста
На рисунке слово «font» выведено курсивным шрифтом с кеглем 144 пункта. Курсивное начертание выбрано из-за того, что в нем глифы обладают более заметными отрицательными метриками А и С, особенно для букв «f», «j», «t» и т. д. Длинная полоса в верхней части рисунка обозначает начальную точку вывода текста и его горизонтальный размер, возвращаемый функцией GetTextExtentPoint32. Справа приведены метрики ABC для каждого символа, их сумма и размеры текста. Длинные вертикальные линии обозначают начальную и конечную точку каждого символа. Расстояние между двумя соседними линиями равно полной ширине символа (сумме метрик ABC). Как видите, ширина символьной строки равна сумме полных значений ширины всех символов, возможно — с добавлением дополнительных межсимвольных интервалов и промежутков между словами. Отрезки, расположенные над символами, обозначают метрики А каждого символа; затушеванный прямоугольник соответствует отрицательному значению. Отрезки, расположенные под символами, обозначают метрики С. Для первой буквы «f», обладающей значительной отрицательной метрикой А, левый край глифа смещается на 34 пиксела влево. Хотя метрика В символа «f» достаточно велика, отрицательная метрика С заметно приближает начальную точку буквы «о». Буквы «о» и «п» обладают положительными метриками А и С. У буквы «t» метрика А положительна, а метрика С отрицательна. К счастью, при выводе строки средствами GDI значения метрик ABC учитываются автоматически и обеспечивают правильное позиционирование символов в строке, не требуя дополнительных усилий со стороны приложения. Впрочем, не обошлось и без проблем: если используемый шрифт имеет отрицательные метрики А или С, текстовая строка не начинается с точки, заданной приложением, и не завершается в позиции, возвращаемой функцией GetTextExtentPoint32.
844
Глава 15. Текст
Метрика А первого символа и метрика С последнего символа строки могут выходить за пределы области, в которой выводиться текстовая строка. Погрешности при обработке отрицательных метрик А и С вызывают ряд проблем. Прежде всего, это может привести к случайному отсечению частей глифов. Запустите WordPad, выберите курсивный шрифт Times New Roman с кеглем 72 пункта и введите букву «f» — ее часть, соответствующая отрицательной метрике А, отсекается. Ограничивающий прямоугольник строки вычисляется неверно. Подобное отсечение часто встречается и в профессиональных приложениях. Ширину строки, как и ширину одного символа шрифта TrueType, можно разделить на три метрики А, В и С. Метрика А строки соответствует метрике А первого символа, метрика С соответствует метрике С последнего символа, а метрика В равна сумме всех остальных метрик строки. Следующая функция вычисляет метрики ABC для целой строки. // ( АО. ВО. СО ) + ( А1. В1, С1 ) = ( АО. ВО+СО+А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 abc: GetCharABCWidthsthDC. 1pString[0], lpString[0]. & abc): // Первый символ pABC->abcB -= abc.abcA; pABC->abcA = abc.abcA;
GetCharABCWidths(hDC. lpString[cbString-l]. lpString[cbStnng-l], & abc); // Последний символ pABC->abcB -= abc.abcC: pABC->abcC = abc.abcC: return TRUE:
BOOL PreciseTextOutCHDC hDC. int x. int y. LPCTSTR IpString. int cbString) { long height: ABC abc: if ( GetTextABCExtent(hDC. IpString. cbString. & height. & abc) ) switch ( GetTextAlign(hDC) & (TA_LEFT | TA_RIGHT | TA_CENTER) ) case TA_LEFT : x -= abc.abcA; break; case TA_RIGHT : x +- abc.abcC; break; case TA_CENTER: x -- (abc.abcA-abc.abcC)/2; break; return TextOut(hDC. x, y. IpString, cbString):
}
Функция Preci seTextOut вычисляет метрики ABC для всей строки и изменяет координату х вызова TextOut в зависимости от текущего флага горизонтального выравнивания текста. Рис. 15.12 наглядно показывает различия между стандартной реализацией выравнивания в GDI и корректировкой, вносимой функцией Preci seTextOut. В первом столбце приведены результаты обычного вызова TextOut для выравнивания строки по левому краю, по центру и по правому краю. Части глифов, выходящие за пределы двух пограничных линий, обычно отсекаются — как, например, в ячейках таблиц Word или Excel. Во втором столбце те же строки выводятся с небольшой поправкой, вычисленной функцией Preci seTextOut; выравнивание обеспечивает точность на уровне пикселов.
fluff fluff -CI
-£С
{fluff JW-JJ fluff fium %/ «/*/
I *" v:*"f I «fc/fc/
/Т
(tfc/
Для строки, показанной на рис. 15.11, GetTextABCExtent возвращает структуру ABC {-34,385,-5}; это означает, что фактическая длина строки равна 385 единицам, причем строка смещена на -34 единицы от начальной точки. Когда функция TextOut выравнивает строку в соответствии с атрибутами выравнивания текста, метрики А и С не учитываются. Если метрика А первого символа отрицательна, часть глифа может исчезнуть. Если метрика А положительна, текст неточно выравнивается по правому краю (особенно при выравнивании текста, оформленного разными шрифтами или одним шрифтом с разным кеглем). В следующем фрагменте показано, как точно выровнять текст на уровне пикселов при помощи функции GetTextABCExtent.
845
Простой вывод текста
J~£*
(fc/.".
*/%/
/3
j?i
J?J*
*/*/
/Vf*
Рис. 15.12. Выравнивание текста: TextOut и PreciseTextOut
Во внутренних операциях графического механизма используется система координат устройства, из которой и берутся все масштабированные текстовые
846
Глава 15. Текст-
метрики. Если отображение из системы координат устройства в логическую систему координат не сводится к масштабированию с целым коэффициентом, получение метрик функцией GetTextABCWidths может привести к возникновению погрешности и последующему накоплению ошибок при вычислениях. Функция . GetTextABCWidthsFloat помогает избавиться от погрешности, возникающей при преобразовании координат. Если ваше приложение выводит текст как на экран, так и на принтер, необходимо специально позаботиться о том, чтобы экранные данные совпадали с печатными. Например, для получения точных текстовых метрик можно воспользоваться эталонным логическим шрифтом (см. раздел «Получение информации о логическом шрифте»).
Нетривиальный вывод текста Функция TextOut обладает простейшими возможностями и предоставляет удобный интерфейс к средствам вывода текста GDI. При использовании этой функции приложение задает значения нескольких атрибутов контекста устройства и передает строку. Функция ограничивается простейшим выводом и полностью маскирует от приложения все сложные операции, выполняемые GDI при выводе текста. За удобство и простоту приходится расплачиваться тем, что приложение не может управлять преобразованием символов в глифы, упорядочением глифов, лигатурами и кернингом, позициями отдельных глифов и т. д. Для решения нетривиальных задач вывода текста в GDI предусмотрены специальные функции, используемые в современных текстовых редакторах и других графических пакетах.
Преобразование символов в глифы В шрифте TrueType центральное место занимает набор описаний глифов, масштабируемых для любого разрешения и преобразуемых в глифы. Символы разных кодировок обычно сначала преобразуются в Unicode, а затем — в индексы глифов по таблице «стар», хранящейся в шрифте TrueType. Некоторые возможности обработки текстов, не поддерживаемые GDI напрямую, легко реализуются на уровне глифов. В GDI Windows 2000 появилась новая функция GetGlyphlndices, позволяющая приложению получить массив индексов глифов для символов строки. В предыдущих версиях Windows это преобразование выполнялось при помощи функции GetCharacterPl acement, описанной ниже. В GDI Windows 2000 также поддерживаются функции для получения информации о метриках глифов: DWORD GetGlyphlndlcesCHDC hDC. LPCTSTR Ipstr. int c. LPWORD pgi. DWORD f1): BOOL GetCharWidth(HDC hDC. UINT giFirst. UINT cgi. LPWORD pgi. LPINT IpBuffer); BOOL GetCharABCWidthsKHDC hDC. UINT giFirst. UINT cgi. LPWORD pgi. LPABC Ipabc); BOOL
GetTextExtentPointKHDC hDC. LPWORD pgiln. int cpi. LPSIZE IpSize):
Нетривиальный вывод текста
847
Индексы глифов хранятся в виде 16-разрядных целых чисел (WORD). Символы Unicode тоже хранятся в виде 16-разрядных целых чисел, поэтому индексы глифов позволяют представить весь интервал Unicode. Для получения информации об интервалах Unicode, поддерживаемых шрифтом, используется функция GetFontUnicodeRanges. Функция GetGlyphlndices отображает текстовую строку на массив индексов глифов. Параметр pgi указывает на массив WORD, размеры которого достаточны для хранения всех индексов. Последний параметр Л сообщает GDI, как следует поступать с отсутствующими символами. Если он равен CGI_MASK_NONEXISTING_ GLYPHS, отсутствующие символы заменяются маркером OxFFFF. По умолчанию отсутствующие глифы в шрифтах TrueType представляются первым глифом шрифта. Функции получения текстовых метрик GetCharWidthI, GetCharABCWidthsI и GetTextExtentPointl очень похожи на аналогичные функции без суффикса I с одним исключением — при вызове им передаются интервалы или массивы индексов глифов. Функции API уровня глифов позволяют выполнять специфические операции, не поддерживаемые непосредственно в GDI. Если приложение хочет реализовать лигатуры или контекстную замену глифов, оно может получить соответствующую таблицу шрифта TrueType функцией GetFontData и произвести поиск в таблице. Внутреннее строение шрифтов TrueType подробно рассматривается в главе 14.
Кернинг
При простом выводе текста функцией TextOut символы позиционируются в строке только по межсимвольным расстояниям, хранящимся в шрифте TrueType, а именно по метрикам ABC глифов. Дополнительные межсимвольные интервалы просто применяются к каждому выводимому символу без учета их специфики. Шрифты TrueType обычно содержат данные кернинга, обеспечивающие контекстную регулировку расстояний. Данные кернинга кодируются в таблице кернинга, каждый элемент которой (пара кернинга) определяет параметры регулировки расстояния для двух конкретных символов, расположенных по соседству. Для получения пар кернинга приложение использует функцию GetKerningPairs: typedef struct tagKERNINGPAIR { WORD wFi rst; WORD wSecond; int iKernAmount; } KERNINGPAIR;
DWORD GetKerningPairs(HDC hDC. DWORD nNumPairs, LPKERNINGPAIR Ipkrnpair); Каждая пара содержит коды двух символов, wFirst и wSecond, и величину кернинга. Фактически она означает следующее: если за символом wFirst следует символ wSecond, относящийся к тому же шрифту, расстояние между этими двумя символами корректируется на величину iKernAmount. Обычно в парах кернинга задается отрицательное расстояние, чтобы символы приближались друг к другу.
848
Глава 15. Текст
однако расстояние может быть и положительным. Величина кернинга, возвращаемая функцией GetKerningPairs, задается в логической системе координат. Шрифт TrueType может содержать сотни пар кернинга. Чтобы получить информацию обо всех парах, следует сначала вызвать функцию GetKerningPairs для получения общего числа пар. После выделения блока памяти достаточного размера функция вызывается повторно для получения данных кернинга. Ниже приведено объявление простого класса для работы с парами кернинга (полная реализация находится на прилагаемом компакт-диске). class KKerningPair { public: KERNINGPAIR * m_pKerningPairs: int rnjiPairs: KKerningPair(HDC hDC): -KKerningPair(void): int GetKerningCTCHAR first. TCHAR second)
}:
Класс KKerningPair содержит две переменные. Переменная m_pKerningPairs указывает на динамически выделенный массив структур KERNINGPAIR, а в переменной m_nPairs хранится количество пар кернинга для текущего шрифта. Конструктор класса запрашивает количество пар кернинга, а деструктор освобождает выделенные ресурсы. Дополнительный метод GetKerm'ng возвращает величину кернинга по кодам двух символов. Примеры кернинга показаны на рис. 15.13. В верхнем ряду текст выводится без кернинга. На левом верхнем рисунке изображены четыре пары символов, чрезмерно удаленных друг от друга, а на правом верхнем — три пары символов, расположенных слишком близко. В результате кернинга символы на левом рисунке сближаются (отрицательная корректировка), а на правом рисунке они удаляются (положительная корректировка).
Рис. 15.13. Примеры пар кернинга
Расположение символов работать с
ни я индексами глифов и данными кернинга в приложении достаточно трудно. В GDI существует удобная функция GetCharacterPlacement,
Нетривиальный вывод текста
849
возвращающая информацию о расположении символов в строке. Приложение может проанализировать полученные данные, изменить их или воспользоваться ими для других целей. Соответствующие определения выглядят так: typedef struct tabGCP_RESULTS { IStructSize: DWORD LPTSTR IpOutString; UINT * IpOrder; INT * INT *
IpDx; IpCaretPos: IpClass;
LPTSTR LPWSTR * IpGlyphs; UINT nGlyphs; UINT mMaxFit; } GCP_RESULTS;
DWORD GetCharacterPlacementCHDC hDC. LPCTSTR IpString. int nCount, int nMaxExtent. LPGCP_RESULTS IpResults. DWORD dwFlags): Функция GetCharacterPlacement получает манипулятор контекста устройства, указатель на текстовую строку и ее длину, необязательную ширину текстовой области и набор флагов. На выходе она заполняет структуру GCP_RESULT сведениями о расположении символов в строке: порядке следования символов, расстояниях между ними, позиции каретки, классификации символов, индексах глифов и количестве символов, помещающихся в заданной области. Сама структура GCP_RESULT не содержит всей информации, поскольку эти данные представляются массивами переменного размера; в структуре хранятся указатели на массивы. Приложение должно соответствующим образом подготовить GCP_RESULT перед вызовом GetCharacterPlacement. Если приложение запрашивает некоторую информацию, оно устанавливает требуемый флаг в последнем параметре, а соответствующее поле GCP_RESULTS должно содержать действительный указатель; в противном случае значение поля может быть равно NULL. Функция GetCharacterPlacement обладает очень широкими возможностями, проектировавшимися в расчете на поддержку разных аспектов обработки текста — выравнивания по ширине, кернинга, диакритических знаков, упорядочения глифов, лигатур и т. д. Конкретные возможности зависят от шрифта и текущей конфигурации системы. В частности, не все шрифты TrueType содержат таблицы кернинга и поддерживают лигатуры. Возможности конкретного шрифта можно проверить при помощи функции GetFontLanguagelnfo. Полное описание возможностей GetCnaracterPl acement займет слишком много места. За подробностями обращайтесь к MSDN. Здесь же будут рассмотрены некоторые простые случаи — использование флагов GCPJJSERKING, GCP_MAXENTENT и GCP_JUSTIFY. В структуре GCP_RESULTS поле IpOutputString содержит указатель на выходную строку, которая будет выведена для заданной входной строки. Как правило, выходная строка совпадает с входной, однако строки могут отличаться — например, при установке флагов GCP_REORDER (изменение порядка следования символов) и GCP_MAXEXTENT (превышение предельной длины входной строки). Поле IpOrder содержит указатель на массив, заполняемый данными отображения входной строки на выходную. В иврите и в арабском языке текст выводится справа
850
Глава 15. Текст
налево, что может привести к изменению порядка следования символов входной строки. Поле 1 pDx содержит указатель на массив, заполняемый сведениями о ширине каждого символа в строке (то есть разности расстояний между позициями текущего и следующего символа в строке). Это расстояние определяется полной шириной символа со всеми поправками — дополнительными межсимвольными интервалами, увеличенными промежутками между словами и кернингом. Порядок следования элементов в массиве IpDx соответствует порядку следования символов в выходной строке. Поле IpCaretPos указывает на массив, заполняемый позициями каретки для всех символов в порядке их следования во входной строке. Данные могут использоваться текстовым редактором для перемещения каретки в процессе работы. Информация об угле наклона символов хранится в структуре OUTLINETEXTMETRIC. Поле IpClass содержит массив классификационных признаков для всех символов строки. Например, флаг GCPCLASS_ARABIC обозначает арабский символ. Поле IpGlyphs содержит указатель на массив, заполняемый индексами глифов всех символов строки. При помощи этого массива можно получить индексы глифов без помощи функции GetGlyphlndices, поддерживаемой только в Windows 2000. Если одна текстовая строка используется при нескольких вызовах функций, следует получить индексы один раз и задействовать их повторно — это сэкономит время на многократные преобразования. Поле nGlyphs изначально содержит максимальное количество элементов в разных массивах структуры GCP_RESULTS. При выходе из GetCharacterPlacement в него записывается количество фактически используемых элементов в массивах. Поле nMaxFit содержит количество символов, укладывающихся в области, размеры которой задаются параметром GetCharacterPlacement. Обратите внимание: в поле nMaxFit хранится количество символов, в отличие от поля nGlyph, содержащего количество глифов. На компакт-диске находится простой класс КР1 acement, предназначенный для работы с функцией GetCharacterPlacement.
Функция ExtTextOut Получение индексов глифов, данных кернинга и сведений о расположении символов — не более чем подготовка к выводу текста. Чтобы применить полученную информацию при выводе текста, следует воспользоваться функцией ExtTextOut. BOOL ExtTextOut(HOC hDC. int x. int ym UINT fuOptions. CONST RECT * Iprc, LPCTSTR IpString. UINT cbCount. CONST INT * IpDx);
Функция ExtTextOut представляет собой расширенную версию TextOut. Вызов TextOut можно заменить эквивалентным вызовом ExtTextOut: ExtTextOut(hDC. x. у. 0. NULL. IpString, cbCount. NULL);
Остается лишь разобраться с тремя новыми параметрами. Параметр fuOptions содержит набор флагов, управляющий интерпретацией других параметров. Необязательный параметр 1 ргс указывает на прямоугольник, который может определять границы непрозрачной области, использоваться для отсечения или совмещать эти функции. Необязательный параметр IpDx содержит указатель на массив расстояний. В табл. 15.3 перечислены допустимые значения параметра fuOptions.
851
Нетривиальный вывод текста Таблица 15.3. Флаги функции ExtTextOut Значение
Описание
ETO_OPAQUE (0x0012)
Перед выводом текста прямоугольник, заданный параметром 1 ргс, закрашивается цветом фона
ETO_CLIPPED (0x0004)
Текст отсекается по прямоугольнику, заданному параметром Iprc
ETO_GLYPHJNDEX (0x0080)
Параметр IpString указывает на массив индексов глифов (вместо кодов символов). Индексы глифов всегда являются 16-разрядными величинами
ETO_RTLREADING (0x0080)
То же, что и флаг TA_RTLREADING при выравнивании текста. В шрифтах арабского языка и иврита текст выводится справа налево
ETO_NUMERICSLOCAL (0x0400)
Числа выводятся символами национальных алфавитов
ETOJUMERICSLATIN (0x0800)
Числа выводятся стандартными европейскими цифрами
ETOJGNORELANGUAGE (0x1000)
Не выполнять дополнительную языковую обработку текста
ETO_PDY (0x2000)
Параметр IpDx указывает на массив пар, в которых первое число определяет горизонтальное расстояние, а второе — вертикальное
Параметр 1 ргс функции TextOut не влияет на обычную прорисовку фона; скорее он упрощает вывод текста в ограниченной области — например, в ячейке таблицы. Если параметр 1 ргс отличен от NULL, он интерпретируется как указатель на прямоугольник в логической системе координат. При установленном флаге ETO_OPAQUE прямоугольник закрашивается текущим цветом фона вместе со стандартным фоном текста. При установленном флаге ETO_CLIPPED прямоугольник задает дополнительный уровень отсечения в логической системе координат (не в координатах устройства!). Чтобы вывести текстовую строку в ячейке таблицы, приложение может передать прямоугольник ячейки ExtTextOut и установить оба флага, ETO_OPAQUE и ETO_CLIPPED. При этом текущим цветом фона закрашивается весь прямоугольник ячейки, а не только текстовая область, и текст заведомо не нарушит границ ячейки. Параметр IpDx позволяет приложению точно определять позицию каждого глифа, не полагаясь на помощь GDI. При такой архитектуре приложение может дополнительно обработать структуру расположения символов, сгенерированную функцией GetCharacterPlacement. Вспомните: текстовые метрики, возвращаемые GDI приложению, задаются в логической системе координат, а вывод текста происходит в системе координат устройства. При печати документа, созданного на экране монитора, на принтере с высоким разрешением, обладающим существенно более точными данными метрик, раскладка текста не будет точно соответствовать экранной. Для решения этих двух проблем несоответствия (между логическими координатами/координатами устройства и экраном/принтером) приложение может получить точные данные метрик шрифта по эталонному логическому шрифту, размер которого совпадает с размером em-квадрата физиче-
852
Глава 15. Текст
ского шрифта. Все позиционные вычисления производятся по точным данным em-квадрата и масштабируются в логической системе координат, сохраняются в массиве и передаются ExtTextOut. Функция ExtTextOut также может использоваться для вывода текстовой строки по индексам глифов, полученным при помощи функции GetGlyph Indices или GetCharacterPl acement. Следующий метод выводит текстовую строку по содержимому структуры GCP_RESULTS, возвращаемой функцией GetCharacterPl acement. BOOL KP 1 acement : :GlyphTextOut( HOC HOC. int x. int y) •{
AVOWAL gi •A' )gi •v )gi •0' >• gi •B 5gi •A' )1
S8
36
extent 34S.
gi 'A' )gi •v )gi •0' )=
36 57 50 91(4')• 58 gi •A'1 )= 36 gi •L )= 47 extent 346.
}
A VO WA
gi 'A' )= gi •v )• gi '0' )gi •B )gi 'A' )gi 'L' )1
36 57
50 58 36 47
dx dx dx dx dx dx
0 1
49
49 58 66 49 4 44 5 sum dx= 15 45, dx 0 dx 1 47. dx 2 58, dx 3 60, dx 4 49, dx 5 44 sum dx= 03 dx 0 54 dx 1 56
dx dx dx dx
2 3
2 3 4
5
kern( 'A' , 'V )= -4 kern( 'V , '0' )= -2 kern( '0' , 'W )= 0 1 kern('W. 'A )- -6 kern( ' A' , 'L' )= 0
67 68 57 44
extent 346, sun dx=346
Рис. 15.14. Пример ExtTextOut: индексы глифов и кернинг
KPlacement<MAX_PATH> placement: TEXTMETRIC tm;
GetTextMetrics(hDC. & tm); int linespace = tm.tmHeight + tm.tmExternal Leading: KKerningPair kerningChDC); SIZE size;
GetTextExtentPoint32(hDC. mess, _tcslen(mess). & size): for (int test=0: test<3; test++) { у += linespace;
int opt: switch ( test ) {
case 0: opt = 0: break;
case 1: opt = GCPJJSEKERNING: break; case 2: opt = GCPJJSEKERNING | GCP_JUSTIFY; break; pi acement. GetPlacement( hDC. mess, opt size.cx*ll/10): pi acement. GlyphTextOuU hDC. x. y):
36 57 SO
gi 'L' )- 47
return ExtTextOut (hDC. x. y, ETO_GLYPH_INDEX. NULL. (LPCTSTR) m_glyphs. mjcp.nGlyphs. m_dx)-
Следующий фрагмент иллюстрирует пример использования индексов глифов и кернинга при работе с функциями GetCharacterPl acement и ExtTextOut. void Test_Kerning(HDC hDC. int x. int y. const TCHAR * mess) { TextOut(hDC, x. y. mess. Jxslen(mess));
853
Нетривиальный вывод текста
GCPMAXEXTENT.
Первая строка выведена обычной функцией TextOut. Вторая строка выведена функцией ExtTextOut по индексам глифов и расстояниям, сгенерированным функцией GetCharacterPl acement, но без установки флага GCPJJSEKERNING. Результат выглядит точно так же, как и в первом случае. Третья строка выведена функцией ExtTextOut для данных, возвращенных функцией GetCharacterPl acement при установленном флаге GCPJJSEKERNING. Наконец, нижняя строка демонстрирует результат выравнивания по ширине с флагом GCP_JUSTIFY. Справа выводятся индексы глифов, расстояния между символами и пары кернинга. Видно, что различия в положении символов второй и третьей строк обусловлены применением кернинга, в результате чего ширина текстовой области сократилась на 12 единиц. Различия между третьей и четвертой строками обусловлены выравниванием по ширине: 43 единицы дополнительного пространства почти равномерно распределяются между 5 парами символов (9,9,9,8,8). Если при выравнивании текста установлен флаг TA_RTLREADING и при вызове функции GetCharacterPl acement был передан флаг GCP_REORDER, GDI записывает глифы в соответствии с направлением чтения и правилами замены глифов, если они поддерживаются текущим шрифтом. Пример: KLogFont lf(-PointSizetoLogical(hDC. 48). "Andalus"): If.mJf.lfCharSet = ARABIC_CHARSET; If.mJf.lfQuality - ANTIALIASED_QUALITY:
KGDIObject font (hDC. If .CreateFontO): assert ( GetFontLanguagelnfo(hDC) & GCP_REORDER );
В программе, находящейся на компакт-диске, расстояние между глифами отмечается линиями; также выводятся индексы глифов и интервалы кернинга. Пример приведен на рис. 15.14.
KPlacement<MAX_PATH> placement: const TCHAR * mess = "abc \xC7\xC8\xC9\xCA": SetTextAlignChDC. TAJEFT);
854
Глава 15. Текст
placement.GetPlacementChDC. mess. 0): placement.61yphTextdutthDC. x. yd): SetTextAlign СhDC. TA_LEFT | TA_RTLREADING); placement.GetPlacementChDC, mess. GCP_REORDER): placement.GlyphTextOut(hDC, x. yl): На рис. 15.15 показан очень интересный результат.
o;
order =0, order 1 = 1, order 2 = 2, order '3 = 3, order 4; = 4, order 5 =5, order 6 = 6, order 7, = 7, order 0] = 5, order 1, = 6, order 2 = 7, order 3 = 4, order 4 = 3, order '5 = 2, order :X = 1, order 7 = 0,
gi(0x61)= 68, gi(0x62)= 69, gi(0x63)= 70, gi(0x20)= 3, gi(0xc7)=346. gi(0xc8)=349. gi(0xc9)=352. gi(Oxca)=3S6, gi(0xca)=353. gi(0xc9)=352. gi(0xc8)=349. gi(0xc7)=345. gi(0x20)= 3, gi(0x61)= 68, gi(0x62)= 69, gi(0x63)= 70,
dx [0]= dx Т = dx '2 : ! dx :з ! dx 4 dx V dx V dx : 7l dx dx dx dx dx 4 1 dx '5 dx ; 6j = dx '?' =
i°;
"2: :з:
30 33
29 16 13 18 23 18 30 33 29 16 19 18
23 48
Рис. 15.15. Изменение порядка следования и замена глифов
Сравните две строки, верхнюю и нижнюю: слова поменялись местами, порядок следования символов изменился, а сами символы отображаются на разные глифы. В первой строке символы выводятся в стандартном порядке — слева направо, от первого до восьмого. Во второй строке изменился порядок следования как слов, так и арабских символов. Арабские символы отображаются на разные глифы в зависимости от их относительной позиции в слове. Скажем, буква «алеф» (ОхС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) были расширены для поддержки сложных текстов.
Нетривиальный вывод текста
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 GetGlyphOutline(HDC hDC. UINT uChar. UINT uFormat.
856
Глава 15. Текст
LPGLYPHMETRICS Ipgra. DWORD cbBuffer. LPVOID IpvBuffer. CONST MAT2 *lpmat2):
Функция GetGlyphOutline за один вызов может вернуть для символа один из трех типов данных: метрики глифа, растр глифа или контур глифа. Первый параметр определяет контекст устройства с выбранным шрифтом TrueType/OpenType. Параметр uChar определяет код символа в однобайтовом формате или в кодировке Unicode (в зависимости от того, используется ли Unicode-версия этой функции). Параметр uFormat в основном управляет форматом данных, запрашиваемых приложением; его основные значения перечислены в табл. 15.4. Следующий параметр, Ipgm, указывает на структуру GLYPHMETRICS, заполняемую метриками глифа. Чтобы получить растр или контур глифа, приложение должно передать функции GetGlyphOutline указатель на буфер IpvBuffer; размер буфера задается параметром cbBuffer. Последний параметр, 1 pmat2, указывает на матрицу аффинного преобразования в формате с фиксированной точкой. Таблица 15.4. Формат результата GetGlyphOutline
Значение
Описание
GGO_METRICS
Заполнить только структуру GLYPHMETRICS. Вернуть 0 в случае успеха, GDI_ERROR при неудаче
GGO_BITMAP
Заполнить GLYPHMETRICS и растр в формате 1 бит/пиксел
GGO_NATIVE
Заполнить GLYPHMETRICS и исходное описание глифа из шрифта TrueType
GGO_BEZIER
Заполнить GLYPHMETRICS и описание глифа в виде кубических кривых Безье
GGO_GRAY2_BITMAP
Заполнить GLYPHMETRICS и растр в формате 8 бит/пиксел с 5 уровнями серого
GGO_GRAY4_BITMAP
Заполнить GLYPHMETRICS и растр в формате 8 бит/пиксел с 17 уровнями серого
GGO_GRAY8_BITMAP
Заполнить GLYPHMETRICS и растр в формате 8 бит/пиксел с 65 уровнями серого
GGO_GLYPH_INDEX
Параметр uChar вместо кода символа содержит индекс глифа
GGOJJNHINTED
Запретить обработку инструкций (хинтов) для контуров глифов (GGO_NATIVE или GGO_BEZIER). Поддерживается только в Windows 2000
Метрики глифа возвращаются в структуре GLYPHMETRICS, которая описывает отдельный глиф в логической системе координат текущего контекста устройства. Поля gmBlackBoxX и gmBlackBoxY определяют ширину и высоту ограничивающего прямоугольника глифа (а также ширину и высоту возвращаемого растра). Следует учитывать, что этот прямоугольник обычно меньше прямоугольника ячейки символа. Поле gmptGlyphOrigin содержит структуру POINT, которая задает координаты левого верхнего угла ограничивающего прямоугольника глифа относительно эта-
Нетривиальный вывод текста
857
лонной точки. Помните, что при описании глифа TrueType в координатах emквадрата вертикальная ось направлена снизу вверх, противоположно направлению вертикальной оси в системе координат устройства или логической системе координат в режиме ММ_ТЕХТ. Поля gmCelllncX и gmCelllncY определяют смещение эталонной точки.
Растр глифа Функция GetGlyphOutline может вернуть растр глифа в одном из четырех форматов. Формат GGO_BITMAP предназначен для простейших монохромных растров, в которых 0 соответствует фоновым пикселам, а 1 — основным пикселам. В трех других форматах растр возвращается в формате 8 бит/пиксел с разным количеством оттенков серого. Эти растры используются GDI для вывода сглаженных символов (вместо обычных резких переходов на границах). Чтобы вывести текст со сглаженными символами, достаточно задать качество ANTIALIASED_QUALITY при создании логического шрифта. Три растровых формата в оттенках серого, GGO_ GRAY2_BITMAP, GGO_GRAY4_BITMAP и GGO_GRAY8_BITMAP, используют 4, 17 и 65 уровней серого соответственно. Вероятно, формат GGO_GRAY8_BITMAP логичнее было бы назвать GGO_GRAY6_BITMAP. Растры глифов либо имеют монохромный формат, либо формат 8 бит/пиксел. Каждая строка развертки всегда выравнивается по границе двойного слова, причем в буфере, возвращаемом GDI, строки развертки следуют в стандартном порядке (от верхней к нижней). Таким образом, формат растра глифа точно совпадает с форматом массива пикселов 1- или 8-разрядного DIB-растра с прямым порядком следования строк. Растр глифа имеет переменный размер, поэтому приложение обычно сначала вызывает функцию GetGlyphOutline для получения размера растра, выделяет блок памяти достаточного объема и вызывает функцию вторично для получения данных растра. Структура МАТ2 определяет ограниченное аффинное преобразование на плоскости. Матрица преобразования содержит числа с фиксированной точкой в формате 16.16. Полное аффинное преобразование описывается структурой XFORM, содержащей шесть вещественных полей: eMll, eM21, eDx, еМ21, еМ22 и eDy, и позволяет выполнять смещение, масштабирование, зеркальное отражение, поворот, сдвиг и их произвольные комбинации. Структура МАТ2 удаляет eDx и eDy из структуры XFORM и преобразует оставшиеся поля в формат с фиксированной точкой. Таким образом, структура МАТ2 позволяет описывать масштабирование, повороты, сдвиги и отражения, но не смещения. Учитывая, что смещение легко реализуется при выводе растра глифа, функция GetGl yphOutl i ne фактически поддерживает полные аффинные преобразования. Обратите внимание: параметр МАТ2 является обязательным. Даже если преобразование является тождественным, вы должны передать правильно заполненную структуру МАТ2. Получив растр глифа, вы можете преобразовать его в аппаратно-зависимыи или аппаратно-независимый растр и воспользоваться растровыми функциями GDI для его вывода. Вариант с DIB предпочтителен, поскольку он не требует создания новых объектов GDI и упрощает управление цветовой таблицей для сглаженных глифов. Функция GetGlyphOutline позволяет приложению самостоятельно имитировать вывод текста, поэтому перед приложением открывается
858
Глава 15. Текст
множество интересных возможностей. Впрочем, получение растров глифов и их вывод — задача не из простых. В листинге 15.4 приведено объявление класса KGlyph для работы с растрами глифов, инкапсулирующего GetGlyphOutline и имитирующего вывод отдельного символа средствами GDI. Полная реализация класса KGlyph находится на компакт-диске. Листинг 15.4. Класс KGlyph: работа с растрами глифов •
class KGlyph { public: GLYPHMETRICS BYTE * DWORD int
mjnetrics: m_pPixels: mjiAllocSize. mjiDataSize: mjjFormat;
KGlyphО; -KGlyph(void);
DWORD GetGlyph(HDC hDC, UINT uChar. UINT uFormat. const MAT2 * pMat2=NULL); BOOL DrawGlyphROP(HDC HDC. int x. int у. DWORD гор, COLORREF crBack. COLORREF crFore); BOOL DrawGlyphtHDC HDC. int x. int y. int & dx. int & dy): Класс KGlyph содержит четыре основные переменные: структуру GLYPHMETRICS, буфер растра глифа, размер буфера и флаг формата. Конструктор устроен достаточно просто; деструктор освобождает всю выделенную память. Метод GetGlyph является оболочкой для вызова функции GetGlyphOutline. По сравнению с GetGlyphOutline ему не передается структура с метриками глифа и буфер, поскольку эти данные находятся под управлением класса, а параметр МАТ2 не обязателен, поскольку тождественная матрица легко строится в приложении. KGlyph::GetGlyph создает матрицу по умолчанию, запрашивает размер данных, выделяет память и запрашивает данные глифа. Одна функция используется для получения метрик, растра и контура глифа. Метод DrawGlyphROP выводит растр глифа, возвращенный методом GetGlyphOutline, с заданными цветами (основным и фоновым) и растровой операцией. Основной и фоновый цвет имитируют атрибуты контекста устройства GDI. Растровая операция имитирует режим заполнения фона (прозрачный или непрозрачный) или любой другой экзотический режим вывода на ваше усмотрение. Функция DrawGlyphROP проверяет формат растра, определяя формат (бит/пиксел) и количество уровней серого цвета. На основании полученных данных строится цветовая таблица, содержащая только основной и фоновый цвета или оттенки серого, расположенные между ними и вычисленные методом линейной интерполяции. В стеке создается структура BITMAPINFO для DIB с прямым порядком строк развертки, после чего растр глифа выводится функцией StretchDIBits с заданной растровой операцией. Учтите, что поле высоты в структуре BITMAPINFOHEADER имеет обратный знак по отношению к высоте ограничивающего прямо-
Нетривиальный вывод текста
859
.угольника глифа; тем самым мы сообщаем GDI, что строки развертки растра следуют в прямом порядке (вместо традиционного обратного порядка). Метод DrawGlyph, основанный на DrawGlyphROP, имитирует вспомогательные операции при выводе растра. Предполагается, что при вызове функции используется режим выравнивания TA_LEFT [ TA_BASELINE. Метод проверяет, не был ли в контексте устройства выбран непрозрачный режим заполнения фона. В этом случае область, в которой выводится глиф, закрашивается цветом фона. После создания кисти, цвет которой определяется текущим цветом текста, растр глифа выводится в прозрачном режиме с использованием тернарной растровой операции ОхЕ20746. Вспомните, что означает код безымянной растровой операции ОхЕ20746: если пиксел источника равен 1, использовать цвет кисти; в противном случае приемник остается без изменений. В данном случае растровая операция выводит основные пикселы текущим цветом текста (при помощи кисти) и не изменяет фоновых пикселов. Поскольку функция DrawGlyph должна быть как можно более универсальной, для вывода отдельных глифов нельзя было воспользоваться простой операцией SRCCOPY — если эта функция последовательно вызывается несколько раз при выводе строки, ограничивающий прямоугольник глифа может перекрываться с ограничивающим прямоугольником предыдущего глифа. Из-за этого функции DrawGlyph приходится задействовать прозрачную растровую операцию при выводе растра глифа. При вызове DrawGlyph не рекомендуется использовать непрозрачный режим заполнения фона, если функция выводит больше одного символа. Фон текста приходится выводить на уровне строки перед выводом самого первого глифа. Функция регулирует экранные координаты в соответствии с координатами базовой точки глифа в структуре GLYPHMETRICS и выводит растр методом DrawGlyphROP. Использовать класс KGlyph для получения информации и вывода растров глифов легко и удобно. В приведенном ниже простом примере строка, выведенная средствами GDI, сравнивается со строками, состоящими из глифов в разных растровых форматах. void Demo_GlyphOutline(HDC hDC) { KLogFont lf(-PointSizetoLogical(hDC. 96). "Times New Roman"): If.m If.lfltalic = TRUE: If.nflf.lfQuality = ANTIALIASED_QUALITY; KGDIObject font(hOC. If.CreateFontO): int x = 20: int у = 160: int dx. dy:
SetTextAlign(hDC. TAJASELINE | TAJ.EFT); SetBkColor(hDC. RGB(OxFF. OxFF. 0)): // желтый SetTextColor(hDC. RGB(0. 0. OxFF)); // голубой // SetBkModethDC. TRANSPARENT); KGlyph glyph;
860
Глава 15. Текст
TextOut(hDC, x. у. "1248". 4): y+= 150: glypli.GetGlyph(hDC. '!', G60_BITMAP); glyph.DrawGlyph(hDC. x. y. dx, dy);x+=dx; glyph.GetGlyph(hDC. ' 2 ' , GGO_GRAY2_8ITMAP); glyph.DrawGlyph(hDC. x. y, dx. dy): x+=dx; glyph.GetGlyphChDC, ' 4 ' , GGO_GRAY4_BITMAP); glyph.DrawGlyph(hDC. x. y. dx. dy): x+=dx: glyph.GetGlyph(hDC. ' 8 ' . GGO_GRAY8_BITMAP): glyph.DrawGlyphChDC. x. y. dx, dy): x+=dx; }
Функция создает сглаженный логический шрифт, выводит строку «1248» функцией TextOut GDI, а затем выводит четыре отдельных глифа «1», «2», «4» и «8» в форматах GGO_BITMAP, GGO_GRAY2_BITMAP, GGOJRAY4JITMAP и GGO_GRAY8_BITMAP. Результат показан на рис. 15.16.
Рис. 15.16. Вывод растров глифов, сгенерированных функцией GetGlyphOutline
Наверху слева изображена строка, выведенная средствами GDI. Похоже, во внутренней работе GDI использует формат GGO_GRAY4_BITMAP с 17 уровнями серого цвета. Слева внизу показан результат, выведенный функцией KG1 yph. Хорошо видны «зазубрины» цифры «1», выведенной в формате GGO_BITMAP, но три других формата практически не отличаются друг от друга. Справа показаны фрагменты растров глифов с 17 и 65 уровнями серого. Внимательный анализ цветного варианта рисунка показывает, что для вычисления промежуточных оттенков GDI не ограничивается линейной интерполяцией в цветовом пространстве RGB. Цвета, используемые GDI, обеспечивают более приятный и красочный результат по сравнению с реализованным в листинге 15.4.
Нетривиальный вывод текста
861
Контур глифа Функция GetGlyphOutline также может вернуть описание контура глифа в виде комбинации прямых и кривых Безье, соответствующих исходному описанию глифа в шрифте TrueType. У приложения появляются новые низкоуровневые возможности для работы с данными, очень близкими к физическому описанию шрифта TrueType. Использование контуров вместо растров находит немало интересных применений. Три флага параметра uFormat определяют формат контура глифа. Общий формат представляет собой последовательность так называемых многоугольников TrueType. Многоугольник TrueType начинается со структуры TTPOLYGONHEADER, за которой следует серия структур TTPOLYCURVE. Если указан формат GGO_NATIVE, многоугольники TrueType состоят только из прямых линий и квадратичных кривых Безье. Квадратичная кривая Безье определяется тремя точками — двумя конечными и одной контрольной, тогда как кубическая кривая Безье определяется четырьмя точками. Как было показано в главе 14, контур глифа в физических шрифтах TrueType описывается закодированным набором линий и квадратичных кривых Безье с инструкциями для подгонки по сетке. Таким образом, было бы неправильно утверждать, что формат GGO_NATIVE полностью соответствует описанию глифа TrueType; тем не менее работать с ним в приложениях гораздо удобнее, чем с физическими таблицами глифов. Если указан выходной формат GGO_BEZIER, все квадратичные кривые Безье в многоугольниках TrueType преобразуются в кубические. По умолчанию полученный контур подвергается дополнительной обработке — к нему применяются специальные инструкции, улучшающие внешний вид глифа и обеспечивающие постоянство графического стиля при малых размерах шрифтов. Но если флаг GGOJATIVE или GGO_BEZIER задан в сочетании с GGOJJNHINTED, инструкции не применяются. Контур глифа, возвращенный GetGlyphOutline, масштабируется по текущему размеру шрифта с применением матрицы преобразования. Что бы ни говорилось в документации, контур глифа не возвращается в единицах, использованных при его конструировании, и матрица преобразования не игнорируется. Координаты точек в контурах глифов возвращаются в виде 32-разрядных чисел повышенной точности с фиксированной точкой (16-разрядная целая часть со знаком и 16разрядная дробная часть). К счастью, эти реальные координаты генерируются непосредственно по описанию глифа в шрифте TrueType и не приводятся к системе координат устройства. К ним даже можно самостоятельно применять преобразования, не беспокоясь о потере точности. Ниже приведены определения многоугольников TrueType. typedef struct tagPOINTFX {
FIXED x:
FIXED y: } POINTFX. FAR* LPPOINTFX:
typedef struct tagTTPOLYCURVE (
WORD «Type: 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_POLYGON_TYPE), а поле pfxStart определяет начальную/конечную точку многоугольника. Поле pfxStart можно рассматривать как своего рода аналог функции MoveTo GDI. Структура TTPOLYCURVE имеет переменный размер и содержит информацию о cpfx точках. Она может относиться к одному из трех типов (поле wType). Если поле wType равно TT_PRIM_LINE, структура описывает ломаную; значение TT_PRIM_QSPLINE соответствует квадратичной кривой Безье, а значение TT_PRIM_CSPLINE — кубической кривой Безье. Ломаную можно рассматривать как последовательность команд LineTo GDI. Кубическая кривая Безье всегда состоит из N х 3 точек, в GDI ее аналогом является команда PolyBeziefTo. В простейшем случае квадратичная кривая Безье определяется двумя точками; вместе с последней точкой многоугольника они образуют сегмент кривой. Вообще говоря, все точки, за исключением последней, в кривых TT_PRIM_ QSPLINE интерпретируются как контрольные. Если в определении кривой несколько контрольных точек следуют подряд, между каждой парой вставляются дополнительные конечные точки. Впрочем, эта тема обсуждалась в главе 14 при описании формата глифов TrueType. Основная проблема с расшифровкой многоугольника TrueType — перебор сегментов и их преобразование в линии или квадратичные кривые Безье, поддерживаемые GDI. В GDI существует функция PolyDraw, которая выводит серию отрезков и кубических кривых Безье, представленных двумя массивами. В массиве POINT хранятся координаты, а в массиве BYTE — флаги. Если бы нам удалось декодировать контур глифа в подобную структуру, то приложение могло бы вывести его одним вызовом функции GDI. В листинге 15.5 приведено объявление класса KGlyphOutline, предназначенного для расшифровки и вывода контуров глифов. Полная реализация класса находится на прилагаемом компакт-диске. Листинг 15.5. Класс KGlyphOutline: работа с контурами глифов template class KGlyphOutline { public: POINT m_Point[MAX_POINTS]: BYTE m_Flag [MAX_POINTS]:
Нетривиальный вывод текста
int private: void void void void void
863
mjiPoints: AddPointCint x. int y, BYTE flag): AddQSpline(int xl, int yl. int x2. int y2); AddCSpline(int xl, int yl. int x2. int y2. int x3. int y3): MarkLast(BYTE flag): Transformtint dx. int dy):
public: int DecodeTTPolygontconst TTPOLYGONHEADER * IpHeader. int size): BOOL DrawCHDC hDC. int x. int y): int DecodeOutline(KGlyph & glyph)
}: Переменные класса KGlyphOutline соответствуют параметрам функции Polyn r a w _ это массив POINT, массив BYTE и количество точек. Метод AddPoint добавляет новую точку с флагом, определяющим ее тип. Метод AddCSpline добавляет кубическую кривую Безье с тремя точками. Метод AddQSpline преобразует квадратичную кривую Безье в кубическую и вызывает AddCSpline. Метод MarkLast используется только для пометки последней точки, замыкающей фигуру. Координаты KGlyphOutline, заданные сначала в виде чисел с фиксированной точкой, сохраняются как 32-разрядные целые, поскольку с исходной структурой FIXED неудобно работать. Метод Transform преобразует число с фиксированной точкой в целое и добавляет начальное смещение. Вы можете легко реализовать дополнительные преобразования — например, для масштабирования или поворота всех точек. Метод DecodeTTPolygon управляет расшифровкой данных, полученных функцией GetGlyphOutline. Он перебирает содержимое структур, вставляет неявные контрольные точки для квадратичных кривых Безье и вызывает три вспомогательные функции для построения кривых. Каждый многоугольник TrueType помечается как замкнутый флагом PT_CLOSEFIGURE. Это гарантирует нормальное завершение линий при использовании утолщенного геометрического пера. Листинг 15.5 выглядит проще кода расшифровки данных TrueType, описанного в главе 14, поскольку самая сложная часть преобразования — расшифровка низкоуровневых данных TrueType, преобразование и подгонка по сетке — выполняется шрифтовыми драйверами, драйверами графических устройств и GDI. В следующем фрагменте текстовая строка выводится в виде контура с использованием класса KGlyphOutline. Результаты вызовов OutlineTextOut и функции TextOut GDI показаны на рис. 15.17. BOOL OutlineTextOuUHDC hDC int x, int y. const TCHAR * str. int count) { if ( count<0 ) count = Jxslen(str): KGlyph glyph; KGlyphOutline<512> outline: while ( count>0 )
864
Глава 15. Текст
if ( glyph.GetGlyphthDC. * str. GGO_NATIVE)>0 if ( outline.DecodeOutline(glyph) ) outline.Draw(hDC. x. y): x += glyph.m_metrics.gmCellIncX; ' у += glyph.mjnetrics.gmCelllncY: str ++: count --:
}
return TRUE;
Outline
Рис. 15.17. Вывод контура текста с использованием GetGlyphOutline
Форматирование текста В этой главе мы рассмотрели простейший вывод текста (функция TextOut), более сложный вывод на уровне индексов глифов и позиционированием отдельных символов (функция ExtTextOut) и даже низкоуровневые операции с растрами и контурами глифов с применением функции GetGlyphOutline. Короче, мы рассмотрели практически все, что необходимо знать о выводе текста в GDI. Пришло время посмотреть, как эти возможности применяются на практике (если, конечно, вы не предпочитаете работать на уровне таблиц TrueType — но об этом уже говорилось в главе 14). Этот раздел посвящен всевозможным проблемам, связанным с форматированием текста, то есть размещением символов в соответствии с заданными требованиями.
Вывод текста с табуляцией Табуляция широко используется в простейших текстовых редакторах для выравнивания текста по столбцам, облегчающего восприятие данных. Впрочем, та-
Форматирование текста
865
булированный вывод текста применяется и во многих современных пакетах. GDI содержит специальные функции для получения метрик и вывода текстовых строк, содержащих внутренние символы табуляции. LONG TabbedTextOut(HOC hDC, int x. int y. LPCTSTR IpString. int nCount, int nTabPosition. LPINT IpnTabStopPositions. int nTabGrigin): 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 { "Group" "Font" "Text"
* lines[] = "\t" "Parameters", "\t" "Result" "\t" "Function" "\t" "DWORD" "\t" "GetFontData" "\t" "(HDC hDC. ...)". "\t" "(HOC hDC, ...)" "\t" "BOOL" "\t" "TextOut"
int x=50. y=50: for (int i=0; i<3: i++)
у += HIWORD(TabbedTextOut(hDC. x. у lines[i]. _tcslen(lines[i]). sizeof(tabstop)/sizeof(tabstop[0]). tabstop. x)):
866
Глава 15. Текст
Программа выводит три строки текста в четыре столбца в позициях х, х + 120, х + 125 и х + 250, причем в позиции х + 120 текст выравнивается по правому краю. Обратите внимание: начальная координата х передается функции GetTabbedTextOut в последнем параметре, поэтому позиции в массиве задаются относительно начала текста. Вы должны проследить за тем, чтобы позиции табуляции были достаточно удалены друг от друга, а текст выводился аккуратно упорядоченным по столбцам.
Open c:\Win\system32\gdi32.c
Open Open
Простое абзацное форматирование В пользовательском интерфейсе 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
Форматирование текста
c:\Win\system32\gdi3; c:\Win\syst...\gdi3 2.dll
&0pen c:\Win\system32\gdi... Open c:\Win\system32\gdi...
The DrawText function draws formatted text in the specified rectangle. It fnrm^t.q thp text anrnrdinn tn The DrawText function draws formatted text in the specified rectangle. It frinrmts thp tpyt annnrHinn tn The DrawText function draws formatted text in the specified rectangle. It
Рис. 15.18. Форматирование текста функциями DrawText/DrawTextEx
В левой части рисунка приведены примеры форматирования однострочногс текста с приведенными далее комбинациями флагов. В частности, рисунок иллюстрирует вертикальное выравнивание, расширение табуляций, маскировку префикса и разные варианты использования многоточий. Флаг DT_MODIFYSTRM даже позволяет изменить исходную строку, привести ее в соответствие с выведенными символами и использовать в другом месте. const TCHAR * mess = "&0pen \tc:\\Win\\system32\\gdi32.dll"; RECT rect = { 20. 120. 20+180. 120 + 32 }: int opt[] = { DT_SINGLELINE | OTJOP. DT_SINGLELINE | DTJCENTER | DTJXPANDTABS. DTJINGLELINE | DTJOTTOM j DTJXPANDTABS | DT_PATH_ELLIPSIS. DTJINGLELINE | DT_NOPREFIX | DTJXPANDTABS DT_WORDJLLIPSIS. DT_SINGLELINE j DTJIDEPREFIX | DTJXPANDTABS | DTJNDJLLIPSIS, }:
Справа на рисунке показано, как DrawText преобразует длинный текст в аб зац, состоящий из нескольких строк. В трех приведенных вариантах использу ются разные флаги горизонтального выравнивания. Хотя функции DrawText/DrawTextEx обладают удобными средствами формати рования многострочного текста, их возможности ограничиваются обслужива нием простых нужд пользовательского интерфейса. Для WYSIWYG этого явш недостаточно. Рис. 15.19 иллюстрирует это утверждение. На рисунке текст, оформленный шрифтом с кеглем 6, 8, 12 и 15 пунктов, вы водится в текстовых областях, размер которых пропорционален кеглю (шири на = 42 х кегль, высота = 5 х кегль). В идеальном случае абзац должен делиться на строки в одних и тех же местах, но на рисунке видно, что положение раз рывов строк изменяется. Для вывода лицензионного соглашения в диалогово> окне сойдет, но в серьезном текстовом редакторе это неприемлемо. Пользовате ли будут сильно огорчены, если текст по-разному форматируется при разны: масштабах изображения или при печати на принтерах с разным разрешением Конечно, проблема связана с тем, что возвращаемые GDI простые целочис
868
Глава 15. Текст
ленные метрики страдают от накопления погрешности. Чтобы форматирование текста соответствовало принципу WYSIWYG, необходимо принять особые меры.
869
Форматирование текста
В листинге 15.6 приведены объявления двух классов, реализующих аппаратно-независимое форматирование текста. Полные реализации находятся на компакт-диске. Листинг 15.6. Классы аппаратно-независимого форматирования текста class KTextFormator
The DrawText function draws formatted text in the specified rectangle. It formats :he 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, justifying characters, breaking lines, and so forth).
{
public: BOOL SetupPixeKHDC hDC. HFONT hFont. float pixelsize): BOOL Setup(HOC hDC. HFONT hFont. float pointsize); BOOL GetTextExtent(HDC ride. LPCTSTR pString. int cbString. float & width, float & height): BOOL TextOut(HDC hDC. int x. int y. LPCTSTR pString, int nCount): DWORD DrawText(HDC hDC. LPCTSTR pString. int nCount. const RECT * pRect. UINT uFormat):
|The DrawText function draws formatted text in the specified rectangle. It pormats the text according to the specified method (expanding tabs, justifying 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 заменяются более точными реализациями.
typedef enum { MaxCharNo = 256 }; float m_fCharWidth[MaxCharNo]; float m_fHeight:
class KLineBreaker
{
LPCTSTR mjDText: int mjlength: i nt m_nPos; BOOL SkipWhite(void): void GetWord(void); BOOL BreakableCint pos);
// Пропуск пробелов // Чтение следующего слова // Попытка разбиения слова
public:
float textwidth. textheight: KLineBreaker(LPCTSTR pText. int nCount): BOOL GetLine(KTextFormator & 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: SetBkModeChDC, TRANSPARENT): for (int size=6; size<=21; size+=3) // 6. 9. 12. 15, 18. 21 { int width = size * 42: int height = size * 5; KLogFont Ш-PointSizetoLogicaUhDC, size), "MS Shell Dig"): Tf.mJf.lfQuality = ANTIALIASEDJJUAUTY; 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 | DTJ.EFT):
Box(hDC. rect.left-1. rect.top-1, rect.right+1. rect.bottom+1); у - rect.bottom + 10: Функция Demo_Paragraph получает логический параметр bPreci se. Если этот параметр равен TRUE, для вывода многострочного текста используется класс KTextFormator; в противном случае используется функция DrawText GDI. Результаты применения стандартной реализации GDI вы видели на рис. 15.19. WYSIWYGверсия, реализованная методом KTextFormator::DrawText, изображена на рис. 15.20.
he DrawText function draws formatted text in the specified rectangle. : formats the text according to the specified method (expanding tabs, uslifying characters, breaking lines, and so forth).
.'he DrawText function draws formatted text in the specified rectangle. It formats the text according to the specilied method (expanding tabs, justifying characters, breaking lines, and so forth).
[The DrawText function draws formatted text in the specified rectangle, t formats the text according to the specified method (expanding tabs, ustifying characters, breaking lines, and so forth). Рис. 15.20. Форматирование текста с использованием класса KTextFormator
Рисунок наглядно показывает, что при использовании вещественных метрик смена разрешения устройства и масштаба никак не сказывается на форматировании абзацев и выводе текста.
Эффекты при выводе текста В предыдущем разделе было показано, как управлять различными аспектами вывода текста на уровне GDI. Следующий вопрос — как воспользоваться этими возможностями для создания эффектов при выводе текста?
872
Глава 15. Текст
В простейших текстовых редакторах текст выглядит тривиально, черные буквы на белом фоне. Вам остается лишь рассчитать, как правильно отформатировать этот текст. Впрочем, существуют многочисленные эффекты, позволяющие оформлять заголовки или выделять фрагменты текста. В этом разделе рассматривается изменение цвета и формы текста, а также использование текста в виде растров и кривых.
Цвет текста В контекстах устройств GDI предусмотрены атрибуты, определяющие цвет текста, цвет фона и режим заполнения фона при выводе текста. Например, чтобы текст выводился синим цветом, достаточно вызвать функцию SetTextColor(hDC, RGB(OxFF.O.O)). В GDI также предусмотрена возможность вывода сглаженного текста с использованием промежуточных цветов между цветами текста и фона, устраняющих неровности на границах глифов. Чтобы вывести сглаженный текст, при создании логического шрифта следует указать параметр качества ANTIALIASED_ QUALITY. В GDI цвета текста и фона могут быть только однородными. В режиме с 256 цветами GDI перед выводом всегда заменяет указанный текст однородным цветом из палитры. Кроме того, в GDI не предусмотрена возможность закраски текста произвольной кистью. Конечно, это вызывает определенные проблемы при выводе текста в пользовательском интерфейсе, поэтому была предусмотрена специальная функция для вывода «затушеванных» текстовых строк. Функция GrayString, реализованная системой управления окнами (user32.dll), позволяет раскрашивать текст кистями и удалять из глифов некоторые пикселы (предполагается, что затушеванный текст будет выводиться на недоступных элементах). Поскольку GDI не поддерживает закраски текста кистью, функция GrayString создает совместимый контекст устройства, преобразует текст в растр и работает с полученным растром. Как и другие графические функции, не относящиеся к GDI, функция GrayString обладает достаточно простыми возможностями и предназначается в основном для вывода текста на экран. В частности, GrayString не поддерживает отрицательные значения метрик А и С. Несмотря на отсутствие прямой поддержки, цветной текст можно вывести и другими средствами GDI. В одном из простых решений используется растровая операция, а раскраска выполняется в три этапа. Если вы хотите вывести текст кистью цвета Р, выполните следующие действия.
с вычислением ограничивающего прямоугольника, поскольку сколько-нибудь общее решение должно учитывать отрицательные значения метрик А и С. В листинге 15.7 приведена одна из возможных реализаций этой идеи. Функция GetOpaqueBox вычисляет ограничивающий прямоугольник выводимой строки. Функция Col orText показывает, как закрасить текст кистью. Аналогичная функция для заполнения текста растровым изображением в книге не приводится. Листинг 15.7. Закраска текста кистью BOOL GetOpaqueBox(HOC hDC. LPCTSTR IpString. int cbString. RECT * pRect. int x. int y) { long height: ABC abe; if ( ! GetTextABCExtent(hOC, 1 pString. cbString, & height. & abc) ) return FALSE; switch ( GetTextAlign(hDC) & (TAJ.EFT | TA_RIGHT | TA_CENTER) ) case TA_LEFT case TA_RIGHT case TA_CENTER default:
3. Снова воспользуйтесь кистью Р с растровой операцией PATINVERT. Фон восстанавливает цвет D, а текст окрашивается в цвет Р. Аналогичный способ применяется для вывода непрозрачного текста с применением кистей для закраски фона и текста, а также для заполнения текста растровыми изображениями или градиентными заливками. Главные трудности связаны
break: x -= abc.abcB: break; x -= abc.abcB/2; break; assert(false);
switch ( GetText Align (hDC) & (TAJOP | TA_BASELINE | TA_BOTTOM) ) case TA_TOP : break: case TA_BOTTOM : у = - height: break; case TA_BASELINE: {
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:
1. Закрасьте нужную область кистью Р с растровой операцией PATINVERT. Приемник переходит в состояние ОЛР. 2. Выведите текстовую строку в прозрачном режиме с черным цветом текста. Фон сохраняет цвет ОЛР, а текст окрашен в черный цвет (0).
873
Эффекты при выводе текста
return TRUE; BOOL ColorText(HDC hDC. int x, int y. LPCTSTR pString. int nCount. HBRUSH hFore)
{
HGDIOBJ hOld
= SelectObjectthDC. 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 = SetBkMode(hDC, TRANSPARENT): COLORREF oldColor = SetTextColor(hDC, RGB(0. 0. 0 ) ) ;
•
TextOut(hDC, x. y. pString, nCount): SetBkMode(hDC. oldBk); SetTextColorthDC. oldColor); BOOL rslt = PatBlt(hDC. rect.left. rect.top, rect.right-rect.left. rect.bottom - rect.top, PATINVERT); SelectObjectChDC, hOld): return rslt;
}
Функция GetOpaqueBox проверяет метрику А первого символа и метрику С последнего символа и смотрит, не отрицательны ли они. Для проверки используется функция GetTextABCExtent, описанная в этой главе. Кроме того, мы учитываем разные комбинации флагов вертикального и горизонтального выравнивания и вносим соответствующие поправки в параметры прямоугольника. Функция ColorText дважды закрашивает прямоугольник при помощи функции PatBIt с растровой операцией PATINVERT. Перед выводом текста необходимо правильно задать режим заполнения фона и цвет текста и восстановить исходные значения после его завершения. На рис. 15.21 приведены примеры использования функций GrayString, ColorText и BitmapText.
Рис. 15.21. Закраска текста функциями GrayString, ColorText и BitmapText
В функциях ColorText и BitmapText задействованы три операции графического вывода, и при непосредственном выводе в экранный контекст устройства возникает неприятное мерцание. Проблема решается кэшированием вывода в промежуточном контексте устройства или использованием других приемов, не требующих многократной прорисовки. Учтите, что вследствие применения рас-
Эффекты при выводе текста
875
тровых операций сглаживание шрифтов не рекомендуется, поскольку на рисунке появятся пикселы довольно странных цветов.
Начертания При создании логического шрифта приложение может выбрать различные варианты начертания (насыщенность, курсив, сглаживание, подчеркивание и перечеркивание) при помощи атрибутов структуры LOGFONT. Семейство шрифтов TrueType обычно состоит из четырех физических файлов для поддержки четырех начертаний: обычного, полужирного, курсивного и полужирного курсивного. Система сопоставляет требования пользователя с атрибутами установленных шрифтов и находит оптимальное соответствие. Если шрифт с указанной насыщенностью недоступен, GDI пытается синтезировать его простейшим способом (только если доступный шрифт имеет обычную насыщенность, а запрашиваемый является полужирным). Имитация настолько проста, что различия заметны лишь при малом размере шрифта — GDI просто выводит строку дважды с горизонтальным смещением в один пиксел. Если у вас имеется только полужирный шрифт, GDI не сможет синтезировать по нему шрифт с обычным начертанием. Курсивные шрифты синтезируются гораздо проще. Как говорилось выше, последний параметр функции GetGlyphOutline определяет матрицу преобразования 2 x 2 , что позволяет подвергнуть глиф преобразованию сдвига. Имитация курсивного начертания сводится к небольшому сдвигу с учетом изменения в метриках шрифта. При описании функции GetGlyphOutline упоминалось и о сглаживании, поддерживаемом шрифтовыми драйверами. В GDI для сглаживания текста применяются растры глифов с 17 уровнями серого. Чтобы запросить сглаживание для шрифта TrueType, передайте флаг ANTIALIASED_QUALITY в поле качества; контекст устройства при этом должен находиться в режиме High Color или True Color. Подчеркивание и перечеркивание реализуются GDI на основании данных, содержащихся в структуре OUTLINETEXTMETRIC шрифта TrueType. В некоторых приложениях поддерживаются разные стили подчеркивания и перечеркивания, но все они синтезируются по одним и тем же данным. Кроме эффектов, поддерживаемых на уровне логического шрифта, в приложениях часто реализуются и другие текстовые эффекты — негативный вывод, тени, рельеф (приподнятый и утопленный), обводка контуров и т. д. Негативный вывод реализуется легко — достаточно поменять местами цвет текста с цветом фона и вывести текст в непрозрачном режиме. Для точного воспроизведения эффектов теней и рельефа приходится работать на уровне растров и моделировать источники света. В текстовых редакторах обычно используется упрощенный подход — одна и та же строка выводится несколько раз с разными смещениями и цветами. Функция OffsetTextOut выводит текстовую строку до трех раз. Первые пять параметров аналогичны параметрам функции TextOut. Следующие две группы из трех параметров задают смещение и цвет строки при повторном выводе. Сначала функция выводит первую смещенную строку с параметрами (x + dxl,y + dyl,crtl), затем вторую смещенную строку с параметрами (х + dx2,y + dy2,crt2), после чего
876
Глава 15. Текст
выводит исходную строку от точки (х,у) с исходным цветом текста. Программа рисует увеличенный прямоугольник, а две смещенные строки рисуются в прозрачном режиме. BOOL OffsetTextOut(HDC hDC, int x. int y. LPCTSTR pStr. int nCount. int dxl. int dyl. COLORREF crl. int dx2, int dy2, COLORREF cr2) { COLORREF cr = GetTextColor(hDC): iflt bk = GetBkMode(hDC); if ( bk==OPAQUE ) { RECT rect; GetOpaqueBox(hDC. pStr. nCount, & rect. x, y); rect.left +=• mi n( mint dxl. rect.right += max(max(dxl. rect.top += min(min(dyl. rect.bottom+= maxtmaxtdyl.
dx2), dx2). dy2). dy2).
0): 0); 0): 0);
ExtTextOutthDC, x, y. ETQ_OPAQUE. & rect, NULL. 0, NULL); SetBkModethDC. TRANSPARENT): if ( (dxl!-0) || (dyl!=0) ) SetTextColor(hDC. crl): TextOutthDC. x + dxl. у + dyl. pStr. nCount);
}
if ( (dxl!-0) || (dyl!=0) ) SetTextColorthDC. cr2); TextOutthDC. x + dx2. у + dy2, pStr. nCount);
} SetTextColorthDC. cr): BOOL rslt = TextOutthDC. x. y, pStr. nCount); SetBkModethDC. bk): return rslt; В следующем фрагменте показано, как при помощи функции OffsetTextOut создаются эффекты тени, приподнятого и утопленного рельефа. // Тень
SetBkColorthDC. RGBtOxFF. OxFF. 0)): // Желтый фон SetTextColorthDC, RGBtO. 0. OxFF)); // Синий текст OffsetTextOut(hDC. x. у, "Shadow". 6. 4. 4. GetSysColor(COLOR_3DSHADOW). // Тень 0. 0. 0);
877
Эффекты при вы воде текста
// Приподнятый рельеф SetBkColorthDC. RGBtOxDO. OxDO. 0)); // Темно-желтый фон SetTextColorthDC, RGBtOxFF, OxFF, 0)); // Желтый текст OffsetTextOut(hDC. x. y. "Emboss". 6, -1, -1. GetSysColor(COLOR_3DLIGHT). 1, 1. GetSysColor(COLOR_3DDKSHADOW)); // Углубленный рельеф SetBkColor(hDC. RGB(OxFF. OxFF, 0)); // Желтый фон SetTextColor(hDC, RGB(OxDO. OxDO, 0)); // Темно-желтый текст OffsetTextOut(hDC. x, y. "Engrave". 7, -1. -1. GetSysColor(COLOR_3DDKSHADOW). 1. 1. GetSysColortCOLORJDLIGHT)); Тень имитируется предварительным выводом серого текста со смещением (4,4). Для создания эффекта приподнятого рельефа сначала выводится более светлый текст со смещением (-1, -1), а затем — более темный текст со смещением (1,1). Эффект утопленного рельефа имитируется аналогично, просто цвета меняются местами. Смещения, использованные в этом примере, подходят только для обычного вывода на экран. При выводе с увеличением или печати на принтере в них необходимо внести соответствующие изменения. На рис. 15.22 изображены различные варианты стилевого оформления текста: обычный, полужирный, сглаженный, курсив, полужирный курсив, подчеркнутый, перечеркнутый, негативный, тень, приподнятый и утопленный рельеф.
Style Style Sh Style Style Style Style Style Style Рис. 15.22. Начертания текста
Геометрические эффекты До настоящего момента мы рассматривали только пропорциональный текст, выводимый вертикально. Настало время познакомиться с выводом текста с разной ориентацией и углом поворота символов, с искажением исходных пропорций и т. д.
878
Глава 15. Текст
Структура LOGFONT содержит два атрибута, связанных с углом вывода текста. В поле 1 fEscapement задается угол наклона базовой линии всей строки, а в поле IfOrientation — угол наклона базовой линии отдельных символов. В архитектуре GDI эти углы могут различаться. Например, строка может выводиться горизонтально (1 fEscapement = 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; yl -= 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 ( (xl<0.01) && (xl>-d.dl) ) if ( yl>0 ) escapement = 2700;
Эффекты при выводе текста
879
else escapement = 900; else
double angle = atan(-yl/xl): escapement = tint) ( 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 PathTextOuUHDC hDC. LPCTSTR pString. POINT point[], int no) { double xO = point[0].x: double yO = pointed].y: for (int i=l: i<no: i++) { double xl - point[i].x; double yl = pointeiLy; double curlen = dis(xO. 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-yfl) * length / curlen: DrawCharChDC. x O O . 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, а также вертикальное азиатское письмо и шрифты с искаженными пропорциями.
to
т
ц;
6 p., CD ? &ь *
**•* да
да ,
> „<г
%
#«& 4ГЛ ^ *
nФ Ф
j| ft
Эффекты при выводе текста
881
клоном базовой линии строки. Все символы расположены вертикально, но после каждого символа позиция вывода смещается вправо и вверх в соответствии с заданным углом наклона базовой линии. Также обратите внимание на увеличивающийся прямоугольник фона. При различающихся углах наклона строки и отдельных символов текст не рекомендуется выводить в непрозрачном режиме. В диапазоне от 50 до 90° строки выводятся с одинаковыми углами наклона. Кривая, вдоль которой размещаются символы длинной строки в центре рисунка, демонстрирует работу функции PathTextOut. Фоновые прямоугольники показывают, что при отсутствии крутых изгибов текст достаточно плавно размещается вдоль кривой. Текст, выводимый под углом 270°, направлен сверху вниз, как в традиционной китайской и японской письменности. Впрочем, каждый символ дополнительно разворачивается на 90° против часовой стрелки. Для шрифтов, поддерживающих двухбайтовые кодировки (в частности, китайскую и японскую), предусмотрены специальные имена, при которых символы разворачиваются на 90° и принимают вертикальное положение. Все, что для этого требуется, — поставить символ «@» перед именем гарнитуры. Например, вместо «SimSun» используется имя «©SimSun». На рис. 15.23 приведены две строки из стихотворения времен династии Тан в китайской традиционной письменности (символы следуют сверху вниз, строки заполняют лист справа налево). Код следующего фрагмента выводит вертикально ориентированный текст. KLogFont Ш-PointSizetoLogicaKhDC. 24). "@SimSun"): If.mJf.lfQuality = ANTIALIASED_QUALITY; If.mJf.lfCharSet = GB2312_CHARSET: If.mjf. If Escapement = 2700; If.mJf.lfOrientation = 2700; KGDIObject font(hDC. If.CreateFontO):
SI
№'
75% W *'(№ 100% W ^ ill 125% W
00° deg Рис. 15.23. Геометрические эффекты
Слева внизу десять строк выводятся под углами от 0 до 90°. В диапазоне от О до 40° текст выводится с нулевым наклоном символов и с изменяющимся на-
SetBkColor(hDC, RGB(OxFF. OxFF. 0 ) ) : WCHAR linel[] = { Ox59Dl. Ox82CF. Ox57CE. 0x5916. Ox5B02, Ox5C71. OxSBFA }; WCHAR 11ne2[] = { Ox591C. Ox534A. Ox949F. Ox58FO. 0x5230, Ox5BA2, 0x8239 }; TextOutW(hOC. xO. yO. linel. 7); xO -= 45; TextOutW(hDC. xO. y O . Iine2. 7); xO -- 50; TextOut СhDC. xO. yO. "270 degree". 10);
При создании логического шрифта поле IfWidth структуры LOGFONT обычно равно 0; это означает, что GDI следует подобрать шрифт с таким же аспектным отношением, как у графического устройства. Аспектным отношением называется отношение ширины пиксела к его высоте. Аспектное отношение текущего контекста устройства можно получить при помощи функции GetAspectRatioFilterEx. Практически все современные графические устройства обладают аспектным отношением 1:1, поэтому шрифт, созданный с нулевым полем IfWidth, обладает правильным соотношением ширины и высоты. При передаче ненулевой величины в поле IfWidth GDI сопоставляет ее со средней шириной шрифта и имитирует шрифт с искажением пропорций. Чтобы созданный шрифт был шире или уже исходного, следует перед заполнением поля IfWidth запросить среднюю ширину шрифта, хранящуюся в поле tmAveCharWidth структуры TEXTMETRIC. В еле-
882
Глава 15. Текст
дующем фрагменте создается логический шрифт, ширина которого составляет заданный процент от ширины текущего шрифта. HFONT ScaleFont(HDC HOC. int percent) { LOGFONT If: GetObject(GetCurrentObject(hDC, OBJ_FONT), sizeof(lf). & If); TEXTMETRIC tm; GetTextMetrics(hDC. & tm): If.lfWidth = (tm.tmAveCharWidth * percent + 50)7100; 'return CreateFontIndirect(&lf);
}
В правой части рис. 15.23 приведен текст, оформленный шрифтами, ширина которых составляет от 25 до 125 % от нормальной. В совместимом графическом режиме настройка анизотропной логической системы координат не обеспечивает масштабирования шрифтов — вам придется самостоятельно создать шрифт с нужным аспектным отношением. В расширенном графическом режиме вывод текста подчиняется настройке логической системы координат, как и все остальные графические элементы. При точном форматировании текста вычисление ширины может сопровождаться ошибками округления; для получения более точных данных воспользуйтесь вещественными метриками (см. раздел «Форматирование текста»).
Работа с текстом в растровом формате Средства вывода текста в GDI подчиняются ограничениям, затрудняющим реализацию некоторых эффектов на «чисто текстовом» уровне. Например, если задать в совместимом графическом режиме логическую систему координат с противоположным направлением осей, все линии и растры выводятся справа налево и снизу вверх, но текстовые строки все равно будут выводиться сверху вниз и слева направо. Это весьма неприятно, особенно если вы хотите реализовать эффект зеркального отражения фрагмента с текстом. Существуют и другие ограничения — текст нельзя закрашивать кистью (см. предыдущий раздел), к нему нельзя применять растровые операции и альфа-наложение. Подобные проблемы решаются преобразованием текста в растровое изображение и выполнением операций на уровне растров. Текст преобразуется в растры двумя способами. Первый способ — получение растров отдельных глифов функцией GetGlyphOutline и имитация вывода текста посредством вывода растров. Второй способ — преобразование всей строки в один растр.
Вывод текста с использованием растров глифов Класс KGlyph, разработанный в этой главе, позволяет легко написать функцию вывода текстовой строки по растрам глифов. Ниже приведена функция BitmapTextOutROP, выводящая растры глифов с применением тернарной растровой операции. На компакт-диске имеется упрощенная версия BitmapTextOut, которая выводит растры глифов в прозрачном режиме при помощи метода Kglyph: :DrawGlyph. BOOL BitmapTextOutROP(HDC hDC. int x, int y. const TCHAR * str. int count. DWORD гор)
Эффекты при выводе текста
883
if ( count<0 ) count = _tcslen(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.mjnetrics.gmptGlyphOrigin.x. у - glyph.mjnetrics.gmptGlyphOrigin.y. гор. crBack. crFore): x += glyph.mjnetrics.gmCelllncX; += У glyph.mjnetrics.gmCelllncY; str ++; count --;
return TRUE:
Функции BitmapTextOut и BitmapTextOutROP переводят задачу из текстовой области в область работы с растровыми изображениями. Тем самым решаются проблемы, связанные с зеркальным отражением в логической системе координат, и появляется возможность применения растровых операций. Впрочем, при использовании растровых операций при выводе растров глифов необходимо действовать осторожно, поскольку при выводе строки фоновые пикселы разных глифов могут перекрываться. Функция BitmapTextOutROP выбирает цвет, на который должны отображаться фоновые пикселы, в зависимости от цвета фона контекста устройства. Например, если вы хотите использовать операцию SRCAND, выберите белый цвет фона (RGB(OxFF.OxFF.OxFF)); в этом случае фоновые пикселы не будут влиять на вывод. Следующий фрагмент убеждает в том, что средства GDI не позволяют выполнить зеркальное отражение текста, а также иллюстрирует методику зеркального отражения текста с использованием функции BitmapTextOut и применение растровых операций при выводе текста. // Зеркальное отражение текста { SaveDC(hDC): SetMapMode(hDC. MM_ANISOTROPIC): 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. Текст
SetTextAlign(hDC. TA_LEFT | TA_BASELINE): SetTextColorthDC. RGBtO, 0, OxFF)); // Синий (темный) BitmapTextOut(hDC, x. y. mess. _tcslen(mess). GGO_GRAY4_BITMAP): SetTextColor(hDC. RGBCOxFF. OxFF, 0)): // Желтый (светлый) TextOut(hDC. x. y. mess. _tcslen(mess)):
Эффекты при выводе текста
885
Код второй половины фрагмента выводит красную строку, используя растровую операцию SRCAND, а затем повторяет тот же текст в зеленом цвете со смещением (5,5) и растровой операцией SRCINVERT. Чтобы фоновые пикселы не влияли на вывод, при операции SRCAND используется белый цвет фона, а при операции SRCINVERT — черный цвет фона.
RestoreDC(hDC. -1);
Преобразование текста в растр
// Растровые операции при выводе текста
Если приложение хочет обработать растровые данные перед выводом в контексте устройства или сохранить их для последующего использования, вместо операций с отдельными глифами лучше преобразовать в растровый формат сразу всю строку. К полученному растру можно применить всевозможные графические алгоритмы — например, описанные в главах 10-13 этой книги. В листинге 15.9 приведено объявление класса KTextBitmap, преобразующего текстовую строку в DIB-секцию, с простым фильтром размытия. В классе KTextBitmap объединяются различные приемы работы с растровыми изображениями и текстом, рассмотренные в книге. Полная реализация имеется на прилагаемом компакт-диске.
int x = 10: int у = 300;
KLogFont lf(-PointSizetoLogical(hDC. 48), "Times New Roman"); If.mJf.lfWeight = FWJOLD: 1 f .mjf. 1 fQual ity= ANTIALIASEDJUALITY: KGDIObject font (hDC. If.CreateFontO): const TCHAR * mess = "Raster": SetBkCo1or(hDC. RGB(OxFF, OxFF. OxFF)): SetTextColor(hDC, RGB(OxFF. 0. 0)): B1tmapTextOutROP(hDC. x. y, mess. _tcslen(mess). SRCAND); SetBkColor(hDC. RGBtO, 0. 0)): SetTextCo1or(hDC. RGB(0. OxFF, 0)): BitmapTextOutROP(hDC. x+5. y+5. mess. _tcslen(mess), SRCINVERT): Фрагмент начинается с настройки анизотропного режима отображения, в котором ось х направлена справа налево, а ось у — снизу вверх. Сначала мы выводим синий (темный) текст функцией BitmapTextOut, а затем та же строка выводится в желтом (светлом) цвете функцией TextOut GDI. Строки выводятся в одинаковых логических координатах, но как видно из рис. 15.24, на экране они появляются в разных местах и с разной ориентацией.
Листинг 15.9. Класс KTextBitmap: применение растровых эффектов к тексту // Преобразование текстовой строки в растровое изображение class KTextBitmap public: HBITMAP НОС HGDIOBJ int int int int BYTE *
га hBitmap; m_hMemDC ; m_h01 dBmp : m_width; mjieight: m_dx: m_dy: m pBits;
BOOL ConvertCHDC hDC, LPCTSTR pString. int nCount. int extra); void ReleaseBitmap(void); KTextBitmapO: -KTextBitraapO: void Blur(void): BOOL Draw(HDC hOC. int x. int y. int rop=SRCCOPY);
fuzzy fuzzy fuzzy Soft-Shadow Рис. 15.24. Эффекты с использованием растровых изображений глифов
}: Вся основная работа выполняется классом 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(OxFF. OxFF. OxFF)); // Белый SetTextColorChDC, RGB(Ox80, 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); SetTextCo1or(hDC. RGBCO. 0. OxFF)): // Синий TextOutthDC. 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: // Сгенерировать маску с левым верхним и правым нижнем краем SetBkMode(hOC. OPAQUE): SetBkColor(hDC. RGB(OxFF. OxFF. OxFF)): // Белый фон SetTextColorChDC. RGB(0. 0. 0)); bmp.Convert(hDC. pString, nCount. offset*2): // Черный край // (левый верхний) SetBkMode(hDC. TRANSPARENT): bmp. Render-Text (hDC, offset*2. offset*2. pString. nCount): // Черный край // (правый нижний) SetTextColorthDC, RGB(OxFF. OxFF, OxFF)): // Белый основной текст bmp. Render-Text (hDC. offset, offset. pString, nCount): // Применить маску с левым верхним и правым нижним краем bmp.Draw(hDC. x. у. SRCAND): // Создать цветной растр с левым верхним и правым нижним краем SetBkColorthDC. RGB(0. 0. 0)): // Черный фон SetTextColorthDC. crTL): bmp.Convert(hDC. pString. nCount. offset): // Левый верхний край SetBkMode(hDC, TRANSPARENT): SetTextColorthDC. crBR); bmp.RenderText(hDC, offset*2. offset*2, pString. nCount): // Правый // нижний край SetTextColor(hDC. RGB(0, 0. 0)): bmp. Render-Text (hDC, offset, offset, pString, nCount); // Черный // основной текст // Вывести цветные края (левый верхний и правый нижний) bmp.Draw(hDC. x. у. SRCPAINT):
} Функция TransparentEmboss работает по тому же принципу, который используется при выводе курсоров мыши и значков. Обычно при создании рельефного оформления текстовая строка выводится трижды — сначала в позиции (x - dx, у - dy) цветом левого верхнего края, затем в позиции (x + dx,y + dy) цветом правого нижнего края и, наконец, в позиции (х,у) основным цветом текста. При создании прозрачного рельефа достаточно вывести только два края, левый верхний и правый нижний, не перекрывающиеся с основным текстом. Сначала функция строит маску из черных пикселов на белом фоне. Маска выводится растровой операцией SRCAND на основном изображении и удаляет из него пикселы, расположенные на краях. Затем на черном фоне строится другая маска, в которой пикселы краев окрашены в соответствующие цвета, обеспечивающие создание эффекта рельефа. Маска объединяется с основным изображением растровой операцией SRCPAINT. На рис. 15.25 показан результат применения функции TransparentEmbossing.
888
Глава 15. Текст
889
Эффекты при выводе текста
Текст TrueType легко преобразуется в траекторию, поскольку эта возможность поддерживается на уровне GDI. Приведенный ниже простой пример ограничивается простым выводом контура. BeginPath(hDC): // Начать построение траектории TextOutChDC. x. у. mess, _tcslen(mess)); // Преобразовать один вызов EndPath(hDC); StrokePath(hDC); // Прорисовать контур текста текущим пером // (по умолчанию - черным)
Рис. 15.25. Создание прозрачного рельефа (приподнятого и утопленного)
Текст как совокупность кривых Некоторые задачи, связанные с выводом текста, не удается нормально решить ни в текстовом, ни в растровом формате. К числу таких задач относится прорисовка контура глифа, применение не аффинных преобразований при выводе или имитация объема. Подобные задачи лучше всего решаются преобразованием текста в совокупность отрезков и кривых. Существует три способа преобразования текстовой строки в отрезки и кривые. Первый способ — непосредственная работа с данными шрифта TrueType — рассматривался в главе 14. Второй способ — получение контуров глифов функцией GetGlyphOutline — описан в разделе «Нетривиальный вывод текста». Оба способа дают чрезвычайно точные описания контуров, к которым легко применить преобразования или специальные эффекты, не беспокоясь о потере точности.
Преобразование текста в совокупность отрезков и кривых, представленную траекторией GDI, дает возможность для применения множества специальных эффектов. На рис. 15.26 представлены девять разных вариантов вывода текста. В примере 1 использована функция StrokePath со стандартным черным пером, хорошо подходящим для прорисовки контуров. В примере 2 использована функция Fi 11 Path; результат очень похож на обычный текст, хотя и с некоторой утратой точности и отсутствием сглаживания. В примере 3 функция StrokeAndFillPath прорисовывает контур и заполняет внутреннюю область символов. В примере 4 контур прорисован пунктирным геометрическим пером, вследствие чего очертания глифа состоят из точек среднего размера. Примеры 5 и б демонстрируют разные атрибуты геометрических перьев (закругленные и заостренные соединения). В примере 7 контур сначала обводится толстым черным пером, а затем по толстому контуру рисуется тонкая белая линия, создающая эффект двойной прорисовки.
о
Применение траекторий GDI при выводе текста Третий способ преобразования текстовой строки в совокупность отрезков и кривых гораздо проще — выводимый текст преобразуется в объект траектории GDI. Если заключить функцию вывода текста TrueType/OpenType между функциями BeginPath/EndPath, то контуры глифов (с фоновым прямоугольником, если он присутствует) вместо вывода в контексте устройства будут включены в объект траектории GDI. Завершив построение траектории, приложение может воспользоваться функциями StrokePath, F i l l Path и StrokeAndFi 11 Path для прорисовки контуров и/или заполнения внутренней области глифов. Если данные траектории потребуются для преобразования, переведите внутреннее представление траектории в массив POINT с массивом флагов при помощи функции GetPath. Преобразованный контур можно вывести функцией PolyOraw.
Рис. 15.26. Вывод текста, преобразованного в траекторию
Примеры 8 и 9 очень похожи. В обоих случаях функция StrokePath рисует серию контуров, начиная темными и толстыми и завершая светлыми и тонкими. В результате возникает более реалистичный объемный эффект. В примере 9 поверх полученного рисунка выводится исходный текст, создавая иллюзию углубления.
890
Глава 15. Текст
Преобразование данных траекторий И все же возможности работы с траекториями средствами GDI ограничены. Например, объект траектории создается в системе координат устройства; после того как он создан, смена отображения логической системы координат в систему координат устройства не влияет на вывод траектории. Вам не удастся сместить траекторию даже на один пиксел. К счастью, в GDI предусмотрена функция GetPathData для получения данных, определяющих траекторию. В главе 8 мы создали простой класс KPathData для работы с данными траектории. Один из методов этого класса, MapPoints, применял двумерное преобразование координат ко всем точкам траектории. Данные, полученные в результате преобразования, передаются функции PolyDraw GDI для вывода. Метод MapPoints применяет преобразование только к контрольным точкам траектории, что подходит только для аффинных преобразований, сохраняющих линии и кривые Безье. В результате произвольного двумерного преобразования прямая может превратиться в кривую. Но если мы ограничиваемся преобразованием контрольных точек, то при соединении преобразованных точек все равно получится прямая, а не кривая. Для выполнения произвольных преобразований линии и кривые следует разделить на достаточно мелкие сегменты и добавлять дополнительные точки, обеспечивающие более точное воспроизведение формы преобразованной кривой. В листинге 15.11 приведено определение класса KTransCurve, выполняющего общие двумерные преобразования, а также новый метод KPathData: :Draw. Полная реализация находится на компакт-диске. Листинг 15.11. Родовой класс преобразования траектории class KTransCurve { int m_orgx: // Первая точка фигуры 1 nt m_orgy:
BOOL CloseFigureCHDC hDC): BOOL LineTotHDC hDC. int x3. int y3): BOOL KPathData::Draw(HDC hDC. KTransCurve & trans, bool bPath) if ( m_nCount==0 ) return FALSE: if ( bPath ) BeginPath(hDC): for (int 1=0; 1<m_nCount; i++) switch ( m_pFlag[i] & - PT_CLOSEFIGURE ) case PT_MOVETO: trans.MoveToChDC, m_pPoint[i].x. m_pPoint[i].y); break: case PTJ.INETO: trans.LineTo(hDC. m_pPoint[i].x, m_pPoint[i].y); break; case PTJEZIERTO: trans.BezierTo(hDC. 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;
if ( m_pFlag[i] & PT_CLOSEFIGURE ) trans.CloseFigure(hDC):
Map(float x, float y, float & rx, float & ry); BOOL DrvLineTo(HDC hDC. int x. int y); BOOL DrvMoveTotHDC hDC. int x. int y); BOOL DrvBezierTo(HDC hDC. POINT p[]) :
BOOL BezierTo(HDC hDC, float xl. float yl. float x2. float y2. float x3. float y3): public: KTransCurve(int seglen): BOOL MoveToCHDC hDC. int x. int y): BOOL BezierToCHDC hDC, int xl. int yl. int x3. int y3):
891
default: assert(false);
float xO: // Текущая последняя точка float уО; float mjJstx; float m_dsty; int m_seglen; // Длина сегмента virtual virtual virtual virtual
Эффекты при выводе текста
int x2.
int y2,
if ( bPath ) EndPath(hDC): return TRUE;
Класс KTransCurve решает две задачи: преобразование отдельных точек виртуальным методом Map и вывод, который может изменяться переопределением трех графических примитивов. Метод KPathData:: Draw передает данные траектории, возвращаемые функцией GetPathData, экземпляру класса KTransCurve — как для преобразования, так и для вывода. Ниже приведен простой пример использования классов преобразования траекторий.
892
Глава 15. Текст
893
Эффекты при выводе текста
class KWave : public KTransCurve int m_dx. m_dy:
public: KWavetint 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. x, у. mess. _tcslen(mess)): // Построить траекторию EndPath(hDC); KPathData pd; pd.GetPathData(hDC):
// Запросить данные траектории
StrokeAndFillPath(hDC):
// Вывести исходные данные
KWave wave(8. 360. 0); pd.DrawfhDC. wave, true): StrokeAndFillPath(hDC);
// Преобразование // Применить преобразование. // построить новую траекторию // Вывести новую траекторию
Класс KWave объявляется производным от KTransCurve, и в нем переопределяется ключевой метод Map. Конструктору передаются длины сегмента и смещения, прибавляемые к каждой точке. Метод Map прибавляет заданные смещения, а к вертикальной координате прибавляет дополнительную синусоидальную составляющую с периодом 50 пикселов. Данное преобразование не является аффинным, поскольку оно превращает горизонтальные линии в тригонометрические кривые. На рис. 15.27 показан эффект, созданный классом KWave, а также эффекты других классов, производных от KTransCurve. Исходный контур глифа изображен в левом верхнему углу рисунка. Справа от него показан результат применения синусоидального преобразования класса KWave. Обратите внимание: горизонтальные линии в буквах «V», «U» и «Е» преобразованы в кривые, а не в прямые. В левой нижней части рисунка к каждому кандидату на преобразование применяется небольшое случайное смещение (см. класс KRandom на компакт-диске). Правый нижний рисунок относится к теме следующего подраздела.
Рис. 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 int int int
траекторию в контексте устройства можно преобразовать в объект региона (функция PathToRegion) или воспользоваться ей для отсечения (функция SetClipPath):
m_depth; m_eye_x; m_eye_y; m_eye_z;
HRGN PathToRegion(HDC hDC): BOOL SelectClipPath(HDC hDC. int iMode):
public: KExtrudetint 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);
virtual BOOL DrvLineToCHDC hDC. int xl, int yl) POINT p[5] = { m_xO. m_yO, xl. m_xO. m_yO Map3D(p[0].x, p[0].y. 0): Map3D(p[l].x. p[l].y. 0); Map3D(p[2].x. p[2].y. m_depth); Map3D(p[3].x. p[3].y, mjjepth); Map3DCp[4] .x. p[4].y, 0);
895
Итоги
yl. xl, yl,
m_xO. m_yO.
m_xO = xl: m_yO = yl; return Polygon(hDC. p, 4);
Метод KExtrude: :Ma3D выполняет перспективную проекцию из точки наблюдения с координатами (m_eye_x,m_eye_y,m_eye_z). Метод KExtrude: :DrvLineTo рисует отдельные прямоугольники, образующие поверхность, после отображения трехмерных точек на плоскость. Обратите внимание: класс KExtrude не создает нового объекта траектории; каждый прямоугольник выводится отдельным вызовом Polygon. Дело в том, что режимы заполнения многоугольников GDI не справляются с некоторыми видами экструзионных поверхностей. Как показано на рис. 15.27, класс KExtrude позволяет создавать простейшие объемные эффекты, однако его упрощенная реализация не поддерживает отсечения невидимых поверхностей и вычисления освещенности — для этого лучше воспользоваться каким-нибудь профессиональным пакетом. В книге эта тема не рассматривается.
Текст как регион Преобразование текста в траекторию открывает и другие возможности — к вашим услугам богатый набор функций GDI для работы с регионами. Замкнутую
Функция PathToRegion преобразует текущую траекторию в объект региона, заданный в системе координат устройства. Полученный регион может использоваться для отсечения, проверки принадлежности или для других целей. Второй параметр iMode управляет режимом объединения региона, полученного в результате преобразования, с текущим регионом отсечения. Например, флаг RGN_AND указывает на то, что за новый регион отсечения следует принять пересечение траектории с текущим регионом отсечения. Преобразование текста в регион через траекторию упрощает некоторые операции. Например, в простейшем способе заполнения текста растровым изображением строка преобразуется в регион, после чего строка выводится с назначенным регионом отсечения. Ниже приведено другое решение, которое обходится без раздражающего мерцания. BOOL BitmapText2(HDC hDC, int x. int y. LPCTSTR pString, int nCount. HBITMAP hBmp) { RECT rect; GetOpaqueBoxChDC. pString. nCount. & reel, x. y): HDC hMemDC = CreateCompatibleDC(hDC): HGDIOBJ hOld = SelectObject(hMemOC. hBmp); BeginPath(hDC): SetBkMode(hDC, TRANSPARENT); TextOutChDC. x, y. pString, nCount); // Создать траекторию EndPath(hDC): SelectClipPath(hDC, RGN_COPY); // Преобразовать траекторию в регион BOOL rslt = BitBlt(hDC, rect.left. rect.top. rect.right-rect.left, rect.bottom - rect.top. hMemDC, 0. 0. SRCCOPY-): SelectObject(hMemDC. hOld); DeleteObject(hMemDC): return rslt;
Итоги
Глава, посвященная выводу текста в Win32 GDI, подошла к концу. Мы подробно рассмотрели множество тем, от создания логических шрифтов, получения метрических данных и простейшего вывода до нетривиального форматирования и применения всевозможных эффектов.
896
Глава 15. Текст
В этой главе было убедительно показано, что ограниченные возможности WYSIWYG-форматировании текста в GDI обусловлены целочисленными метриками, используемыми в работе GDI. Чтобы преодолеть эти ограничения, приложение может запросить точные значения метрик по эталонному шрифту, размер которого совпадает с размером em-квадрата. Точные значения метрик обеспечивают форматирование текста, не зависящее от разрешения графических устройств и текущего масштаба. Для создания специальных эффектов текст обычно преобразовывается в растр или в контур. В этой главе были разработаны некоторые классы для решения стандартных задач — получения растров и контуров отдельных глифов средствами GDI, преобразования целых строк в растры и объекты траекторий GDI. Мы рассмотрели родовой класс для применения к траекториям не аффинных преобразований; этот класс даже позволяет создавать простые трехмерные эффекты. С этой главой завершается наше знакомство со всеми основными группами графических примитивов GDI — пикселами, линиями и кривыми, растрами и текстом. В следующей главе мы поговорим о том, как сохранить команды GDI в стандартных и расширенных метафайлах, как происходит обработка и воспроизведение метафайлов. В главе 17 мы вернемся к теме аппаратно-независимого форматирования текста в контексте печати. Глава 18 посвящена интерфейсу DirectDraw.
Пример программы К этой главе прилагается демонстрационная программа Text (табл. 15.5). Впрочем, главное — это даже не программа, а набор родовых функций и классов, созданных в этой главе.
Глава 16 Метафайлы Приложения часто обмениваются друг с другом графическими данными, для чего графические данные требуется сохранять в файлах. Формат BMP, широко используемый в Windows, подходит лишь для обмена растровыми данными. Для поддержки как растровых, так и векторных графических данных используются специальные графические форматы — 16-разрядные метафайлы Windows и 32разрядные расширенные метафайлы Windows. Эти форматы широко применяются в библиотеках графических заготовок, при работе с буфером обмена (clipboard), при обмене данных между сервером и клиентом OLE, а также в работе спулера. Настоящая глава посвящена двум форматам метафайлов Windows, различным способам их создания, использования и расшифровки.
Таблица 15.5. Программа главы 15 Каталог проекта
Описание
Samples\Chapt_15\Text
В меню File содержатся команды для вызова расширенного диалогового окна выбора шрифта, демонстрации системы подстановки шрифтов PANOSE, диалоговых окон для анализа структуры TEXTMETRIC и — самое важное — демонстрационных окон. Выбрав команду File > Demo, вы получаете доступ к 20 с лишним окнам, наглядно демонстрирующим множество тем — от использования стандартных шрифтов до создания специальных эффектов посредством преобразования текста в кривые
Общие сведения о метафайлах У любого графического приложения есть свои сильные и слабые стороны. Но когда опытный пользователь правильно подходит к работе, каждое приложение вносит свой вклад в достижение конечного результата. Например, в 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 возвращает для расширенного метафайла идентификатор типа OBJ_ENHMETAFILE. Расширенный метафайл представляет собой последовательность 32-разрядных команд GDI. Следовательно, основным способом создания расширенных метафайлов является запись серии команд GDI. В GDI предусмотрены две функции, которые начинают и завершают запись расширенного метафайла. HOC CreateEnhMetaFile(HDC hdcRef. LPCTSTR 1pFi1eName. CONST RECT * IpRect. LPCTSTR IpDescription); HENHMETAFILE CloseEnhMEtaFileCHDC hDC);
Функция CreateEnhMetaFile создает специальный контекст устройства, используемый при записи расширенного метафайла. Она всего лишь готовит условия для создания расширенного метафайла — по аналогии с тем, как функция BeginPath начинает построение объекта траектории GDI. Первый параметр, hdcRef, ссылается на эталонный контекст устройства, данные которого потребуются при записи EMF. Если параметр hdcRef равен NULL, GDI принимает в качестве эталона
899
Общие сведения о метафайлах
текущий экран. Во втором параметре, IpFil eName, может передаваться имя файла на диске или NULL. Если передается имя файла, после завершения записи файловый вариант метафайла сохраняется на диске; если передается NULL, метафайл создается в памяти. Третий параметр определяет размеры расширенного метафайла в единицах 0,01 мм. Заданный прямоугольник (кадр) сохраняется в расширенном метафайле и определяет начало координат и размеры области, в которой воспроизводится EMF. Если вместо прямоугольника передается NULL, GDI вычисляет ограничивающий прямоугольник по всем командам вывода; полученный прямоугольник может и не совпадать с тем, который подразумевался при создании метафайла. Следующая функция преобразует прямоугольник в логических координатах контекста устройства в физические единицы 0,01 мм. // Преобразовать прямоугольник из логических координат // в физические единицы 0.01 мм void MaplOum(HDC hDC. RECT & rect) { int wldthmm = int heightmm = int widthpixel = int heightpixel=
GetDev1ceCaps(hDC, HORZSIZE): GetDeviceCaps(hDC. VERTSIZE); GetDeviceCaps(hDC. HORZRES); GetDeviceCaps(hDC. VERTRES);
LPtoDP(hDC. (POINT *) & rect. 2); rect.left rect.right rect.top rect.bottom
= = = =
{ ( ( (
rect.left *widthmm *100+widthpixel/2) rect.right *widthmm *100+widthpixel/2) rect.top *heightmm*100+heightpixel/2) rect.bottom*heightmm*100+heightpixel/2)
/ widthpixel; / widthpixel; / heightpixel; / heightpixel;
}
Обратите внимание на применение индексов HORZSIZE, VERTSIZE, HORZRES и VERTRES функции GetDeviceCaps для преобразования координат в физические единицы, используемые GDI для заполнения полей заголовочной структуры расширенного метафайла. Для экранных устройств разрешение (в пикселах на дюйм) обычно не совпадает с логическим разрешением, возвращаемым для индексов LOGPIXELX и LOGPIXELY. Например, значения LOGPIXELX и LOGPIXELY обычно равны 96 dpi для экранного режима с мелкими шрифтами, а значение HORZRES всегда равно 320 мм; если HORZSIZE = 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(GetDesktopWi ndowt). &rect): MaplOum(hdcRef, rect); HDC hDC = CreateEnhMetaFile(hdcRef. "c:\\test.emf". & rect. "EMF.EXE\OTestEMF\0"); ReleaseDC(NULL. hdcRef): if ( hDC )
GetClientRect(GetDesktopWindow(), Srect); Rectangle(hDC. rect.right/3, rect.bottom/3, rect.right*2/3. rect.bottom*2/3): return CloseEnhMetaFile(hDC):
Общие сведения о метафайлах
Завершив работу с расширенным метафайлом, приложение должно вызвать функцию DeleteEnhMetaFile (по аналогии с функцией DeleteObject, вызываемой для других объектов GDI). Функция DeleteEnhMetaFile удаляет объект расширенного метафайла вместе со всеми ресурсами, выделенными для него GDI. После вызова функции физический файл на диске освобождается, но не удаляется. Чтобы удалить физический файл, следует вызвать функцию OeleteFile файловой системы. Если вместо имени файла при вызове CreateEnhMetaFile передается NULL, внешний файл удалять не нужно. Располагая манипулятором расширенного метафайла, можно воспользоваться функцией PlayEnhMetaFile для воспроизведения любой команды GDI этого метафайла в контексте устройства. Хотя прототип функции PI ayEnhMetaFi I e выглядит просто, внутренний механизм ее работы чрезвычайно сложен. Функция PlayEnhMetaFile получает три параметра: манипулятор приемного контекста устройства, манипулятор исходного расширенного метафайла и прямоугольник, заданный в логических координатах приемного устройства. Прямоугольник соответствует кадру, указанному при создании метафайла функцией CreateEnhMetaFile. Следующий простой пример иллюстрирует связь между построением и воспроизведением EMF. void SampleDrawtHDC hDC, int x, int у)
{
E11ipse(hDC. x+25. y+25. x+75. y+75): SetTextAlign(hDC. TA_CENTER | TA_BASELINE): const TCHAR * mess = "Default Font": TextOut(hDC. x+50, y+50. "Default Font", _tcslen(mess)):
return NULL:
воспроизведение расширенного метафайла Созданный метафайл воспроизводится в контексте устройства по своему манипулятору. Вы также можете открыть расширенный метафайл, хранящийся в файле на диске, и получить манипулятор расширенного метафайла. Ниже перечислены соответствующие функции. HENHMETAFILE GetEnhMetaFile(LPCTSTR IpszMetaFile); BOOL DeleteEnhMetaFile(HENHMETAFILE hemf): BOOL PlayEnhMetaFiletHDC hdc, HENHMETAFILE hemf. CONST RECT * IpRect): Функция GetEnhMetaFile открывает файл с заданным именем, создает объект расширенного метафайла для работы с данными и возвращает его манипулятор. Аналогичный манипулятор возвращается и при завершении построения расширенного метафайла функцией CloseEnhMetaFile. После вызова CloseEnhMetaFile или GetEnhMetaFile заданный файл считается используемым GDI и не может быть удален до удаления объекта функцией DeleteEnhMetaFile.
901
void Demo_EMFScale(HDC 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); SampleDrawfhDC. 10. 10); // Оригинал for (int test=0, x=120: test<3; test++) {
RECT rect = { x. 10. x+(test/2+l)*100. 10+((test+l)/2+l)*100 }:
902
Глава 16. Метафайлы
FillRectChDC. & rect. yellow); PlayEnhMetaFilethDC. hSample, & rect); x = rect.right + 10;
DeleteObject(yellow): DeleteEnhMetaFile(hSample):
}
Содержимое простейшего метафайла1 генерируется функцией SampleDraw. Предполагается, что эта функция рисует круг 50 х 50 единиц в центре квадрата 100 х 100 и выводит текстовую строку в центре круга шрифтом по умолчанию. Функция Demo_EMFScale управляет всем выводом. Сначала она создает EMF в памяти (то есть EMF без файла на диске) при помощи функции SampleDraw. Та же функция SampleDraw рисует то, что было сохранено в метафайле, в левом верхнем углу экрана — просто для сравнения. После этого функция в цикле воспроизводит EMF в трех прямоугольниках разных размеров (100 х 100, 100 х 200 и 200 х 200). Для наглядности соответствующие участки экрана закрашиваются желтым фоном. Результат показан на рис. 16.1.
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 }; // Логический прямоугольник кадра MaplOumChDC. rect); // Перевести в единицы 0.01 мм hDCEMF - CreateEnhMetaFilethDC. NULL. &rect. NULL); // Создать с кадром На рис. 16.2 показаны результаты воспроизведения EMF с правильно заданным прямоугольником кадра.
Default Font
Default Font
Default Font
Default Font
D e f a u l t Font D e f a ult F o n t D
Рис. 16.1. Воспроизведение расширенного метафайла с кадром по умолчанию
Слева показан результат выполнения двух исходных команд рисования в EMF - круг с текстовой строкой в центре квадрата 100 х 100. На втором рисунке показан результат воспроизведения EMF в квадрате 100 х 100. Все отступы куда-то исчезли, а текст и круг масштабируются с искажением пропорций. Третий и четвертый рисунки выглядят примерно так же, хотя в них масштабирование выполняется по прямоугольникам других размеров. Все это произошло из-за того, что EMF был сгенерирован GDI без указания прямоугольника кадра; вернее, прямоугольник кадра был вычислен автоматически по ограничивающему прямоугольнику всех графических команд. В нашем примере ограничивающий прямоугольник определяется координатами {11, 25, 1
D e fa u It F o n t
Здесь и далее под термином «метафайл» подразумевается расширенный метафайл, то есть EMF. — Примеч. перев.
Рис. 16.2. Воспроизведение расширенного метафайла с правильно заданным кадром
Как видно из рисунка, определение кадра 100 х 100 при построении EMF гарантирует, что при воспроизведении EMF в квадрате будет выдержан масштаб и основные пропорции графических элементов. Во всяком случае, круг на рисунке масштабируется превосходно. Впрочем, текстовая строка явно искажается, хотя каждый символ вроде бы расположен в правильной позиции; это связано с тем, что при построении EMF применялся шрифт, выбранный в контексте устройства по умолчанию. Подробности рассматриваются в разделе «Строение расширенных метафайлов».
Получение информации о расширенном метафайле Вы убедились в том, что связь между кадром EMF и прямоугольником, указанным при вызове PlayEnhMetaFile, чрезвычайно важна для правильного размещения и масштабирования расширенного метафайла. Если вы не знаете, как строился метафайл, вам придется каким-то образом получить информацию о нем — например, данные прямоугольника кадра. При построении EMF GDI записывает в него заголовок с основными атрибутами метафайла. Для получения этой информации в GDI определяются специальные функции.
904
Глава 16. Метафайлы
typedef struct tagENHMETAHEADER DWORD iType: DWORD nSIze; RECTL rclBounds: RECTL rclFrame: DWORD dSignature; DWORD nVersion; DWORD nBytes: . DWORD nRecords: WORD nHandles: WORD sReserved: DWORD nDescription: DWORD offDescription; DWORD nPalEntries; SIZEL szlDevice; SIZEL szlMillimeters: DWORD cbPixelFormat: DWORD offPixel Format; DWORD bOpenGL: SIZEL szlMicrometers; } ENHMETAHEADER:
// // // // // // // // //
EMR_HEADER Размер в байтах, включая дополнение Границы в единицах устройства Прямоугольник кадра в единицах 0.01 мм ENHMETA_SIGNATURE Номер версии Размер метафайла в байтах Количество записей в метафайле Количество манипуляторов в таблице
// // // // // // // // //
Длина строки описания Смещение строки описания Количество элементов в палитре метафайла Размер эталонного устройства в пикселах Размер эталонного устройства в миллиметрах 4.0 Размер PIXELFORMATDESCRIPTOR 4.0 Смещение PIXELFORMATDESCRIPTOR 4.0 Флаг наличия команд OpenGL 5.0 Размер эталонного устройства в микрометрах
DINT GetEnhMetaFileHeaderCHENHMETAFILE hemf. UINT cbBuffer, LPENHMETAFILEHEADER): UINT GetEnhMetaFileDescription(HENHMETAFILE hemf. UINT cchBuffer. LPTSTR IpszDescription); Типичный метафайл всегда начинается со структуры ENHMETAHEADER. Первые два поля ENHMETAHEADER соответствуют общей структуре формата EMF, в которой каждая запись должна начинаться с 32-разрядного идентификатора типа записи и 32-разрядного размера. С каждым типом записи EMF связывается уникальный идентификатор типа в интервале от EMR_MIN до EMR_MAX. В настоящее время EMR_MIN = 1, a EMR_MAX = 122. В EMF постоянно добавляются новые типы записей для новых возможностей GDI. Например, идентификатор EMR_ALPHABLEND = 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 должно содержать уникальную сигнатуру расширенного метафайла, Ox464d4520 в шестнадцатеричной записи или «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; в ней поле off Description равно 108. Поле nPal Entries задает количество элементов в накапливаемой палитре, используемой в метафайле. Цветовая таблица хранится не в заголовке, а в последней записи EMF, поскольку при построении EMF размер таблицы еще не известен GDI. Если палитра в EMF не используется, это поле равно нулю. Следующие два поля содержат информацию об эталонном устройстве. Поле szlDevice определяет размеры поверхности устройства в пикселах, а поле szMilli meters — в миллиметрах. Для получения информации о драйвере устройства GDI вызывает функцию GetDeviceCaps с индексами HORZRES, VERTRES, HORZSIZE и VERTSIZE. В метафайлах, записанных с использованием экранного контекста, типичные значения szlDevice равны 1024 х 768 или 1152 х 864. Поле szMillimeters всегда равно 320 х 240 (диагональ 400 мм = 15,75 дюйма, даже если у вас установлен 21-дюймовый монитор). Следующие три поля, cbPixel Format, off Pixel Format и 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]: GetEnhMetaFileHeaderthEmf. sizeof(emfh). emfh): // Размеры изображения в единицах 0.01 мм width - emfh[0].rclFrame.right - emfh[0].rclFrame.1eft; height - emfh[0].rclFrame.bottom - emfh[0].rclFrame.top;
Общие сведения о метафайлах
907
// Преобразовать к пикселам текущего устройства int t = GetDeviceCaps(hDC, HORZSIZE) * 100; width = ( width * GetDeviceCapsthDC. HORZRES) + t/2 ) / t; t = GetDeviceCaps(hDC, VERTSIZE) * 100; height - ( height * GetDeviceCaps(hDC. VERTRES) + t/2 ) / t; RECT rect = { 0. 0. width, height }: // Преобразовать в логические координаты DPtoLP(hDC. (POINT *) & rect. 2): width = abs(rect.right - rect.left); height = abs(rect.bottom - rect.top); Функция GetEMFDimension получает манипулятор контекста устройства, в котором вы собираетесь воспроизвести EMF. Она запрашивает данные заголовка EMF функцией GetEnhMEtaFileHeader и вычисляет по ним размеры кадра. Ширина и высота кадра сначала преобразуются в систему координат устройства, а затем — в логическую систему координат. Результаты, полученные при вызове GetEMFDimension, позволяют воспроизвести EMF с исходными размерами или в заданном масштабе. Ниже приведена общая функция для вывода EMF в заданном масштабе (для исходных размеров оба масштабных коэффициента равны 100). BOOL 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ШОО. у + (height * scaley + 50)7100 }: 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.GetSaveFileNametNULL. "emf". "Enhanced Metafiles") ) if ( szFileName ) _tcscpy(szFi1eName. fd.m_Ti11eName); return CreateEnhMetaFiletNULL. fd.m_TitleName. rcFrame. description);
} else return NULL; }
int KMyView::OnCommand(int and. HWND hWnd) if
(cmd==IDM_FILE_SAVE) hDC = QuerySaveEMFFileC'EMF SampleW. & rect. NULL);
if ( hDC ) { > OnDraw(hDC. NULL); // Функция вывода HENHMETAFILE hEmf = CloseEnhMetaFile(hDC); DeleteEnhMetaFile(hEmf); // Манипулятор не нужен
Общие сведения о метафайлах
909
Загрузка EMF из ресурса Если EMF присоединяется к исполняемому файлу в виде двоичного ресурса, то при помощи функций FindResource, LoadResource и LockResource можно получить указатель на изображение и создать по нему объект EMF вызовом функции SetEnhMetaFileBits. HENHMETAFILE SetEnhMetaFileBitsCUINT cbBuffer. CONST BYTE * IpData): Функция SetEnhMetaFileBits получает два простых параметра — размер метафайла и указатель на метафайл, находящийся в памяти. Функции LoadBitmap и Loadlmage Win32 не позволяют загружать EMF из ресурсов, поэтому ниже приведена простая функция для решения этой задачи. // Загрузить EMF. присоединенный в виде ресурса, с типом данных RCDATA HENHMETAFILE LoadEMF(HMODULE hModule. LPCTSTR pName) HRSRC hRsc
= FindResourcethModule. pName. RT_RCDATA);
if ( hRsc—NULL ) return NULL: HGLOBAL hResData = LoadResource(hModule. hRsc): LPVOID pEmf = LockResource(hResOata); return SetEnhMetaFileBits(SizeofResource(hModule, hRsc). (const BYTE *) pEmf): } Ресурс EMF не принадлежит к числу стандартных типов ресурсов, однако для него можно указать тип ресурса RCDATA (идентификатор типа RT_RCDATA). Функция LoadEMF показывает, как загрузить ресурс EMF и преобразовать его в объект расширенного метафайла GDI.
Вывод EMF в статическом элементе управления В этом фрагменте обрабатывается команда меню IDM_FILE_SAVE. Обработчик вызывает функцию QuerySaveEMFFile, которая запрашивает у пользователя имя создаваемого файла и возвращает метафайловый контекст устройства. Затем вызывается метод OnDraw, выполняющий основной вывод в окне. Программа использует класс для работы с диалоговыми окнами, построенный в одной из предыдущих глав. Зная манипулятор расширенного метафайла, EMF можно сохранить на диске функцией CopyEnhMetaFile: HENHMETAFILE CopyEnhMetaFileCHENHMETAFILE hemfSrc. LPCTSTR IpszFile); Функция CopyEnhMetaFile копирует содержимое EMF в файл на диске, заданный параметром IpszFile, и возвращает манипулятор нового объекта EMF. Если вместо имени файла передается NULL, копия создается в памяти. После завершения работы с копией EMF объект GDI удаляется вызовом Del eteEnhMetaFi 1 е, но файл на диске остается до его удаления вызовом DeleteFile. Объект EMF создается на основе существующего EMF-файла простым вызовом GetEnhMetaFile (см. выше).
Объект EMF можно связать со статическим элементом управления, который затем автоматически выводится в диалоговом окне или странице свойств. У вас появляется возможность вывода векторной графики без применения элементов, прорисовка которых выполняется владельцем, и добавления специального кода графического вывода. По сравнению с растрами и значками, часто используемыми при выводе статических элементов управления и кнопок, EMF обеспечивает большую гибкость при масштабировании в разных разрешениях экрана. Чтобы объект EMF отображался в статическом элементе управления, включите в стиль последнего флаг SS_ENHMETAFILE или присвойте ему в редакторе ресурсов тип «Enhanced Metafile». В процессе инициализации родительского окна элемента управления отправьте ему сообщение STM_SETIMAGE с манипулятором EMF. Следующий фрагмент показывает, как это делается в обработчике сообщений диалогового окна. switch (uMsg) { case WMJNITDIALOG: {
hEmf = LoadEMFC(HMODULE) GetWindowLongChWnd, GWL HINSTANCE). MAKEINTRESOURCEUDR EMFD):
910
Глава 16. Метафайлы
SendDlgltemMessagefhWnd. IDC_EMF. STM SETIMAGE. IMAGEJNHMETAFILE. CLPARAM) hErnf):" return TRUE;
case WMJICDESTROY: if ( hEmf ) DeleteEnhMetaFile(hEmf); return TRUE;
Строение расширенных метафайлов
911
формате WMF или EMF. Операционная система автоматически преобразует данные WMF в формат EMF, поэтому клиентское приложение всегда должно запрашивать из буфера данные в формате EMF. Работать с буфером обмена просто и удобно. Ниже приведены две функции, предназначенные для копирования и вставки данных EMF. void CopyToClipboard(HWND hWnd. HENHMETAFILE hEmf) { if ( OpenClipboard(hWnd) )
{
На рис. 16.3 показано диалоговое окно со статическим элементом управления, в котором выводится объект EMF, использованный в одном из примеров главы 15.
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;
'WMt'tefРис. 16.3. Использование ресурсов EMF в статических элементах
Вывод EMF в статическом элементе управления открывает перед вами возможности, недоступные для обычных растров. При помощи EMF можно рисовать в статических элементах управления линии, кривые, фигуры и текст. Как видно из рисунка, благодаря EMF значительно упрощается применение прозрачности. Имеется и другое преимущество — в статическом элементе EMF автоматически масштабируется с правильными размерами при переходе из экранного режима с мелкими шрифтами в режим с крупными шрифтами, и наоборот. При выводе растров в статических элементах управления это вызывает массу проблем.
Обмен данными через буфер На удивление простой и эффективный способ передачи графических данных в формате EMF основан на использовании буфера обмена (clipboard). Большинство графических приложений поддерживает старый формат метафайлов Windows; некоторые приложения поддерживают расширенные метафайлы. При копировании данных из графического приложения копия сохраняется в буфере в
Функция СоруТоСЛ ipboard копирует EMF в буфер. Она открывает буфер обмена функцией OpenCl ipboard, удаляет его текущее содержимое функцией EmptyClipboard, копирует данные функцией 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 DWORD } EMR:
iType; nSize:
// Тип записи EMRJtXX // Размер записи в байтах
Первое поле iType определяет тип записи EMF. Каждому типу записи присваивается символическое имя и числовое значение, определяемые в GDI макросами языка С. Ниже приведена небольшая часть этих макросов; полный список приведен в файле WINGDI.H. // Типы записей расширенного метафайла #define EMRJEADER 1 #define EMR_POLYBEZIER 2 #define EMR_POLYGON 3 #define EMR_RESERVED120 fdefine EMR_COLORMATCHTOTARGETW #define EMR_CREATECOLORSPACEW
120 121 122
#define EMR_MIN #define EMR_MAX
1 122
Анализ файла WINGDI.H показывает, что во всех новых версиях ОС появляются новые типы записей EMF. Используемые типы записей никогда не изменяются, а список лишь дополняется новыми типами. Например, в Windows NT 3 последним определенным типом записи (EMR_MAX) является EMR_POLYTEXTOUTW, в Windows NT 4.0 список завершается типом EMR_PIXELFORMAT (104), а в Windows 2000 он расширяется до EMR_CREATECOLORSPACEW (122). Существуют даже недокументированные типы записей с именами вроде EMR_RESERVED_120; возможно, они используются спулером при печати. Все незарезервированные типы записей 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. *PEMROEF; Запись EMREOF не только сообщает о завершении метафайла, но и содержит накопленные данные об элементах палитры, использованных в EMF. На первый взгляд может показаться странным, что дополнение с данными палитры вставляется после поля off Pal Entries и перед полем nSizeLast (но об этом речь пойдет ниже). Хотите посмотреть, как выглядит реальный метафайл? Ниже приведен двоичный дамп простого метафайла с единственной командой SetPixelV. // EMRMETAHEADER 00 iType 04 nSize 08 rcl Bounds 18 rcl Frame 28 dSignature 2с nVersion 30 nBytes 34 nRecords 38 nHandles ЗА sReversed ЗС nDescription 40 offDescription 44 nPalEntries 48 szlDevice 50 szlMillimeters 58 cbPixel Format 5С off Pixel Format
1 0x84 { 3. 5. 3. 5} { 0. 0. Ox49BB. 0x2311 1
EMF'
0x10000 OxAC 3 1 0 OxC
Ox6C 0
{ 0x500. 0x400 } { 0x140. OxFO }
0 0
914 60 64 6С
Глава 16. Метафайлы
bOpenGL О szlMicrometers { Ох4Е200, ОхЗА980 Description L 'EMF Sample\0\0'
. // EMRSETPIXELV 84 iType 88 nSize 8C ptlPixel 94 crColor
OxF 0x14 { 3. 5 } RGB(OxFF. 0. 0)
II- EMREOF 98 iType 9C nSize АО nPalEntries A4 offPal Entries A8 nSizeLast
OxOE 0x14 0 0x10 0x14
Когда метафайл сохраняется в файле на диске или присоединяется к программе в виде ресурса, он имеет в точности такую структуру — никаких скрытых или дополнительных данных.
Строение расширенных метафайлов Категория
Типы записей EMF
Траектории
EMR_ABORTPATH, EMR_BEGINPATH, EMR_CLOSEFIGURE, EMR_ENDPATH
ICM
EMR_CREATECOLORSPACE, EMR_CREATECOLORSPACEW, EMR_COLORCORRECTPALETTE, EMR_COLORMATCHTOTARGETW, EMR_OELETECOLORSPACE, EMR_SETCOLORADJUSTMENT, EMR_SETCOLORSPACE, EMR_SETICMMODE, EMR_SETICMPROFILEA, EMR_SETICMPROFILEW
Линии и кривые
EMR_ANGLEARC, EMR_ARC, EMR_ARCTO, EMR_FLATTENPATH, EMR_LINETO, EMR_MOVETOEX, EMR_POLYBEZIER, EMR_POLYBEZIER16, EMR_POLYBEZIERTO, EMR_POLYBEZIERT016, EMR_POLYDRAW, EMR_POLYDRAW16, EMR_POLYLINE, EMR_POLYLINE16, EMR_POLYLINETO, EMR_POLYLINET016, EMR_POLYPOLYLINE, EMR_POLYPOLYLINE16, EMR_STROKEPATH, EMR_WIDENPATH
Замкнутые фигуры
EMR_CHORD, EMRJLLIPSE, EMRJILLPATH, EMR_FILLRGN, EMRJRAMERGN, EMR_GRADIENTFILL, EMR_INVERTRGN, EMR_PAINTRGN, EMR_PIE, EMR_POLYGON, EMR_POLYGON16, EMR_POLYPOLYGON, EMR_POLYPOLYGON16, EMR_RECTANGLE, EMR_ROUNDRECT, EMR_STROKEANDFILLPATH
Растры
EMR_ALPHABLEND, EMR_BITBLT, EMRJXTFLOODFILL, EMR_MASKBLT, EMR_PLGBLT, EMRJETDIBITSTODEVICE, EMRJETPIXELV, EMR_STRETCHBLT, EMRJTRETCHDIBITS, EMR_TRANSPARENTBLT
Текст
EMR EXTTEXTOUTA, EMR EXTTEXTOUTW, EMR POLYTEXTOUTA, EMR POLYTEXTOUTW
Классификация типов записей 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, EMR_SCALEVIEWPORTEXTEX, EMRJCALEWINDOWEXTEX, EMRJELECTOBJECT, EMRJELECTPALETTE, EMR_SETARCDIRECTION, EMR_SETBKCOLOR, EMR_SETBKMODE, EMRJETBRUSHORGEX, EMR_SETLAYOUT, EMR_SETMAPMODE, EMRJETMAPPERFLAGS, EMR_SETMITERLIMIT, EMR_SETPOLYFILLMODE, EMR_SETROP2, EMR_SETSTRETCHBLTMODE, EMRJETTEXTALIGN, EMRJETTEXTCOLOR, EMRJETVIEWPORTEXTEX, EMRJETVIEWPORTORGEX, EMR_SETWINDOWEXTEX, EMR_SETWINDOWORGEX, EMRJETWORLDTRANSFORM
Отсечение _
EMRJXCLUDECLIPRECT, EMR_EXTSELECTCLIPRGN, EMRJNTERSECTCLIPRECT, EMR_OFFSETCLIPRGN, EMR^SELECTCLIPPATH, EMR SETMETARGN
915
OpenGL 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
Прочее
EMRJOF, EMR_GDICOMMENT, EMRJCADER
Сравнивая приведенные в таблице типы записей с функциями 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 LPBYTE IpbBuffer):
UINT cbBuffer
Сначала приложение получает размер 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->1Type<=EMR_MAX) ) { recno ++: wsprintf(mess, '13d: EMRJOSd (Ш bytes)\n". recno. emr->iType, emr->nSize); stream « mess: if ( emr->iType== EMR_EOF ) 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 nRecoids: 47 nHandles: 3 «Reserved: 0 nDescription: 17 offDescription: 10B nPalEntries: 0 szlDevice:{ 1152,864}
szMllimeters: {320,240 ( cbPixelFormat: 0 offPixelFotmat: 0 bOpenGL: 0 • szMictoMeters: (320000, 240000) hO bj[1 ]-Ct eateFonl(-48.0.0,0,400,0,0,0.0.4AOJ SeleclObject(hDC, hObj(1]);12 bytes SttelchDIBits(hDC, 50,50,554,278,0,0,554,278.: SetBkModelhDC, OPAQUE);12 bytes SetBkColotfhDC. RGB(QxFF.OxFF,OxFF)),12 byt< SetTextColot(hDC, RGB(0,0,0)),12 bytes *1
U
UK
Рис. 16.4. Расшифровка и просмотр EMF в программе
918
Глава 16. Метафайлы
На рисунке воспроизведен метафайл с рельефным текстом, созданный одной из программ главы 15. Программа просмотра и расшифровки EMF входит в один из примеров этой главы. Откройте EMF-файл на диске или вставьте EMF из буфера обмена — программа расшифрует его записи и воспроизведет их. В левой части рисунка видна часть заголовка EMF и пять расшифрованных команд GDI. Как показывает заголовок, метафайл состоит из 47 записей, имеет длину 427 460 байт и записан для экрана 1152 х 864 пикселов. Среди записей EMF мы видим функции создания логического шрифта, выбора его в контексте устройства, вывода растров и настройки цвета/режима заполнения фона для последующего вывода. В правой части окна изображен результат воспроизведения метафайла в масштабе 1:1.
Простые объекты GDI в ЕМF Программа просмотра и расшифровки 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 objecthandleQ]: // Переменный размер, определяется nHandles } HANDLETABLE: Стандартные объекты GDI не хранятся в таблице манипуляторов EMF. Для ссылки на стандартный объект его идентификатор указывается с обратным знаком. В настоящее время документируются стандартные объекты GDI с GetStockObject(WHITEJRUSH) до 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-растр представлен двумя указателями — на структуру BITMAPINFO и на массив пикселов. При отсутствии манипулятора сослаться на растр в EMF невозможно; было решено, что данные растров следует передавать вместе с теми командами, в которых они используются. Давайте посмотрим, как функция BitBlt GDI кодируется в записи EMF EMRBITBLT. typedef struct tabEMRBITBLT '
EMR
emr:
920 RECTL rcl Bounds: xDest ; LONG yDest: LONG cxDest: LONG cyDest : LONG DWORD dwRop : xSrc; LONG ySrc; LONG XFORM xformSrc ; . COLORREF crBkColorSrc: iUsageSrc; DWORD
Глава 16. Метафайлы
// Задается в единицах устройства
// // // // // // // //
Преобразование исходного контекста устройства Фоновый цвет исходного контекста устройства в RGB Формат цветовой таблицы растра (DIB_RGB_COLORS) Смещение структуры BITMAPINFO Размер структуры BITMAPINFO Смещение графических данных растра Размер графических данных растра
DWORD offBmlSrc: DWORD cbBmiSrc: DWORD OffBitsSrc: DWORD cbBitsSrc: } EMRBITBLT; Вспомните, что функция BitBlt GDI получает девять параметров: манипулятор приемного контекста устройства, четыре параметра приемного многоугольника, манипулятор исходного контекста устройства, базовую точку в исходном контексте и растровую операцию. В структуре EMRBITBLT приемный контекст устройства не нужен, поскольку он определяется косвенно; приемный прямоугольник представлен четверкой {xDest, yDest, cxDest, cyDest}, базовая точка источника представлена полями {xSrc, ySrc}, а растровая операция определяется полем dwRop. Остается разобраться с манипулятором исходного контекста (и восемью полями структуры EMRBITBLT). Источником при вызове BitBlt может быть совместимый контекст устройства с выбранным DDB-растром или DIB-секцией или же экранный контекст устройства. Маловероятно, чтобы им оказался контекст устройства принтера, поскольку контексты принтеров обычно недоступны для чтения. Совместимый контекст устройства можно представить растром, который в нем содержится, а экранный контекст устройства легко преобразуется в растр, состоящий из его текущих пикселов. В любом случае источник преобразуется в DIB-растр, описываемый последними полями EMRBITBLT. Поле iUsage обеспечивает интерпретацию цветовой таблицы, поля (offBmiSrc, 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 и ExtSelectClipRgn. typedef struct tagEMREXTSELECTCLIPRGN
emr; // Размер данных региона в байтах cbRgnData; i Mode; RgnData[l]: } EMREXTSELECTCLIPRGN; Как видно из определения EMREXTSELECTCLIPRGN, данные региона включаются в запись даже без стандартного поля смещения. Массив переменного размера RgnData в действительности содержит структуру RGNDATA, возвращаемую функцией GetRegionData. EMR DWORD DWORD BYTE
922
Глава 16. Метафайлы
Как и в случае с растрами, при многократном использовании одного региона в EMF сохраняется несколько копий его данных. Если ваше приложение работает со сложными регионами, будьте внимательны.
Траектории в EMF Операции с траекториями в EMF достаточно близки к аналогичным функциям GDI. В EMF предусмотрены типы записей для функций BeginPath, CloseFlgure, EndPath и функций прорисовки траекторий. Таким образом, можно говорить о неявной реализации траектории как объекта GDI. Функции SaveDC и RestoreDC тоже поддерживаются в EMF, что позволяет использовать данные одной траектории несколько раз. Поддержка функции SelectClipPath в записях EMF тоже сокращает необходимость во внедрении данных траекторий. Например, если приложение хочет использовать эллиптический регион отсечения, то вместо функций CreateEllipticRgn и SelectRegion оно может воспользоваться функциями BeginPath, Ellipse, EndPath и SelectClipPath и избежать включения данных региона в EMF. Если вы используете функцию GetPath для получения данных траектории, вызовите PolyDraw для ее прорисовки; данные траектории внедряются в запись EMRPOLYDRAW.
Палитры в EMF В EMF предусмотрены типы записей для основных операций с палитрой в GDI (функции CreatePalette, SelectPalette, ResizePalette и RealizePalette). Логическая палитра интерпретируется в EMF как объект GDI. Однако GDI несколько иначе интерпретирует вызовы SelectPalette в EMF. .Напомню, что при вызове SelectPalette передаются три параметра: манипулятор контекста устройства, манипулятор палитры и флаг. Флаг является признаком форсированного использования фоновой палитры. Если окно, в котором осуществляется вывод, является активным, а параметр bForceBackground функции Sel ect Palette равен FALSE, палитра позднее будет реализована в качестве основной. Все различия между основной и фоновой палитрами состоят в том, что только основная палитра может удалять нестатические цвета из системной палитры, чтобы реализовать больше однородных цветов для повышения точности цветопередачи; фоновая палитра может лишь занимать свободные позиции палитры или подбирать подходящие цвета среди уже существующих. В записи EMF для функции SelectPalette (тип EMRSELECTPALETTE) параметр bForceBackground не фиксируется. При воспроизведении EMF логическая палитра всегда выбирается в качестве фоновой. Смысл такого решения заключается в том, что воспроизведение EMF не должно приводить к изменению текущих цветов экрана, что привело бы к раздражающему мерцанию и искажению цветов. Управление основной палитрой не может осуществляться на уровне EMF, оно относится к более высокому уровню обработки сообщений (то есть выполняется в обработчиках WM_PAINT и WM_PALETTECHANGED). Если приложение действительно хочет согласовать текущую системную палитру с цветами EMF (то есть реализовать палитру EMF в качестве основной),
923
Строение расширенных метафайлов
к его услугам полная цветовая таблица, которую GDI сохраняет в последней записи EMREOF. В процессе записи EMF GDI накапливает элементы палитры и заносит их в «палитру метафайла», входящую в запись EMREOF. Приложение может получить палитру метафайла при помощи функции GetEnhMetaFilePaletteEntries. UINT GetEnhMetaFilePaletteEntries(HENHMETAFILE hemf LPPALETTEENTRY Ippe);
UINT cEntries
А теперь подумайте, как эта функция реализуется в GDI? Конечно, по манипулятору EMF GDI может найти запись EMREOF, но как именно это сделать при отсутствии прямых ссылок? Перебор всех записей EMF займет слишком много времени, если для этого придется загружать EMF в память с диска. Разгадка кроется в nSizeLast, последнем поле записи EMREOF. Вспомните: поле nSizeLast расположено после цветовой таблицы. По общему размеру EMF, хранящемуся в заголовке EMF, GDI может определить адрес поля nSizeLast. В этом поле хранится размер записи EMREOF, по которому легко определяется начало записи EMREOF. Операции с палитрой подробно рассматриваются в главе 13. Следующий фрагмент показывает, как создать логическую палитру по цветовой таблице EMF. HPALETTE GetEMFPalette(HENHMETAFILE hEmf, HOC hDC) {
// Запросить количество элементов
int no - GetEnhMetaFilePaletteEntries(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 участвуют по меньшей мере четыре системы координат:
925
Строение расширенных метафайлов
Система координат Система координат ст йства У Р° эталонного устройства приемного контекста Логическая система контекста координат эталонного координат приемного контекста контекста
О логическая система координат эталонного контекста; О система координат устройства эталонного контекста;
rclFrame
О логическая система координат приемного контекста; О система координат устройства приемного контекста. Я говорю «по меньшей мере четыре», поскольку ничто не гарантирует, что в эталонном контексте используется всего одна логическая система координат. EMF полностью поддерживает настройку и модификацию логических систем координат функциями SetWi ndowExtEx, SetViewportExtEx, SetWorldTransform и т. д. Возникает непростой вопрос: допустим, в EMF записывается команда вывода линии длиной 1 дюйм в режиме отображения MM_LOENGLISH или MM_HIENGLISH. Какую длину будет иметь линия при воспроизведении EMF функцией PlayEnhMetaFile? Еще более сложный вопрос: если мы выбираем регион отсечения, заданный в системе координат устройства, как он будет интерпретироваться при воспроизведении EMF? При воспроизведении EMF GDI может интерпретировать записи EMF, изменяющие системы координат, как отображение логической системы координат эталонного контекста в систему координат устройства. Пусть это отображение определяется матрицей преобразования xformSrc. Все точки логической системы координат приемного устройства также отображаются в систему координат устройства. Пусть матрица этого преобразования называется xformDst. Таким образом, отображение координатных пространств при воспроизведении EMF определяется связью между системой координат устройства эталонного контекста и логической системой координат приемного контекста. Назовем соответствующую матрицу преобразования xformPlay. На рис. 16.5 изображены два контекста устройств, четыре координатных пространства и три преобразования, участвующие в воспроизведении EMF. Матрица преобразования xformPlay вычисляется по прямоугольнику кадра, хранящемуся в заголовке EMF (rcl Frame), и прямоугольнику, переданному при вызове PlayEnhMetaFile (IpRect). Остается лишь преобразовать rcl Frame из единиц 0,01 мм в систему координат устройства эталонного контекста. Следующий фрагмент показывает, как вычисляется матрица преобразования.
в заголовке EMF
ipRect, переданный привыэ
Рис. 16.5. Системы координат и преобразования, участвующие в воспроизведении EMF
// Преобразование из координат устройства эталонного контекста // в логические координы приемного контекста BOOL GetPlayTransformatiorUHENHMETAFILE hEmf. const RECT * rcPic. XFORM & xformPlay) { ENHMETAHEADER emfh; if ( GetEnhMetaFileHeader(hEmf. sizeof(emfh). & emfh)<=0 ) return FALSE:
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. szl Mi Hi meters, ex * emfh. szl Device, ex; double syl = emfh.rclFrame.bottom / 100.0 / emfh.szlMillimeters.cy * erafh.szlDevice.cy: // Отношения размеров источника к приемнику double rx = (rcPic->right - rcPic->left) / ( sxl - sxO ): double ry = (rcPic->bottom - rcPic->top) / ( syl - syO ): / / x ' - x * eMll + у * eM21 + eDx // у 1 = x * eM12 + у * еМ22 + eDy 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-разрядной версией EMRPOLYPOLYGON и 16-разрядной версией EMRPOLYPOLYGON16. Эти две версии различаются только форматом массива точек. Хотя 16-разрядные версии почти вдвое сокращают затраты памяти, если значения логических координат ограничиваются 16 битами, неизвестно, в каких именно системах они используется. Даже Windows 98 при спулинге в формате EMF генерирует записи EMRPOLYPOLYGON вместо EMRPOLYPOLYGON16. Для элементарных графических функций (таких, как LineTo и Rectangle) в записях EMF сохраняются только исходные координаты. Для более сложных функций GDI сохраняет в записях EMF ограничивающий прямоугольник в системе координат устройства. В частности, поле ограничивающего прямоугольника включается в записи EMRPOLYLINE, EMRPOLYGON и 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) и Rectang1e(50,0,100,50) должны соприкасаться всегда, даже при увеличении. Но поскольку в EMF они представлены в виде {0,0,49,49} и {50,50,99,49}, при увеличении между ними возникает крошечный промежуток.
928
Глава 16. Метафайлы
Строение расширенных метафайлов
Три функции заполнения замкнутых фигур — FillRect, FrameRect и InvertRect — не принадлежат к числу функций GDI. Они реализуются модулем управления окнами USER32.DLL, который выполняет вывод при помощи функций GDI. Эти вызовы GDI — создание и выбор кисти, простой блиттинг — и будут сохранены в EMF. Функции прорисовки регионов F i l l R g n , FrameRgn, InvertRgn и PaintRgn поддерживаются полностью. Объект региона GDI преобразуется в структуру данных региона и присоединяется к этим графическим командам в виде дополнения. Функции построения траекторий, в отличие от функций построения регионов, поддерживаются полностью. Также поддерживаются функции прорисовки траекторий F i l l Path, StrokeAndFillPath и StrokePath. Поскольку эти функции ссылаются на неявный объект траектории в контексте устройства, в их записях EMF сохраняется простейший ограничивающий прямоугольник. Для вычисления реального ограничивающего прямоугольника GDI пришлось бы вызывать GetPath в процессе построения EMF. В EMF поддерживаются все функции вывода растров, от простейшей BitBlt до Al phaBl end, кроме PatBl t. Функция PatBl t объединяется с более общей формой BitBlt, и для них используется одна и та же запись EMRBITBLT. Из-за этого объединения PatBl t представляется 100 байтами. Если бы существовала отдельная запись EMRPATBLT, она бы состояла из 66 байт.
Вызовы DrawText, DrawTextEx и TabbedTextOut в EMF преобразуются в серии вызовов ExtTextOut, чередующихся с вызовами SetTextAl i gn, SetBkMode, MoveToEx и даже SelectClipRgn. Вероятно, вас удивит количество записей EMF, представляющих всего один вызов DrawText или TabbedTextOutput.
Аппаратная независимость EMF
Текст в 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 и установленной поддержкой нескольких языков после записей EMRTEXTOUT для функций TextOut и ExtTextOut добавляются вызовы создания, выбора и удаления логических шрифтов. В результате при каждом выводе текста в EMF может появляться десяток ненужных записей. В Windows NT и даже в ранних версиях Windows 2000 ничего похожего нет.
929
I I
Познакомившись поближе с архитектурой EMF, давайте вернемся к главному вопросу — до какой же степени формат EMF является аппаратно-независимым? Аппаратная независимость — важнейший аспект архитектуры расширенных метафайлов. При описании аппаратной независимости EMF Microsoft утверждает, что сохраненный в EMF рисунок 2 x 4 дюйма сохраняет исходные размеры при печати на принтере с разрешением 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 objecthandletU: // Переменный размер } HANDLETABLE; typedef struct tagENHMETARECORD { DWORD IType; DWORD nSize; DWORD dParro[l]; // Переменный размер } ENHMETAFILERECORD; typedef int (CALLBACK* ENHMFENUMPROCHHDC hDC. HANDLETABLE * IpHTable. CONST ENHMETARECORD * IpEMFR. int nObj. LPARAM IpData): BOOL EnumEnhMetaFile(HDC hDC. HENHMETAFILE emf. ENHMFENUMPPRC IpEnhMetaFunc. LPVOID IpData. 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 ( IsBadWritePtr(pObj, 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. Метафайлы
933
Перечисление записей EMF
return :.:EnumEnhMetaFile(hDC. hemf. EMFProc. this. IpRect);
Класс KEnumEMF содержит виртуальную функцию ProcessRecord, которая берет на себя роль функции косвенного вызова. Реализация по умолчанию возвращает 0, завершая перечисление записей EMF. Главная точка входа EnumEMF вызывает функцию EnumEnhMetaFile GDI и передает ей статическую функцию EMFProc, которая передает управление виртуальной функции C++. Подобная инкапсуляция средств Win32 API в классах C++ хороша тем, что все операции с Win32 API выполняются всего в одном месте. Вы можете добавить новые переменные в производных классах, реализовать новые возможности переопределением виртуальных функций, не говоря уже о создании нескольких экземпляров класса.
Замедленное воспроизведение EMF В простейшей реализации виртуальная функция KEnumEMF:: ProcessRecord сводится к простому вызову PI ayEnhMetaFi I eRecord. Фактически вы вручную реализуете PlayEnhMetaFile с небольшой задержкой, связанной с появлением дополнительного кода. Хотя это слово вызывает отрицательные ассоциации, правильно выбранная задержка помогает проследить за воспроизведением метафайлов. Приведенный ниже класс KDel ayEMF делает небольшую паузу перед воспроизведением записи. class KDelayEMF : public KEnumEMF { int m_delay: virtual int ProcessRecorcKHDC 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 del ay(10): del ay.EnumEMF(hDC. hEmf. IpPictureRect): Если вы когда-нибудь интересовались тем, как создаются качественные трехмерные эффекты при выводе текста, скопируйте объемный текст в программу EMF этой главы и проследите за замедленным воспроизведением. Построение трехмерной строки показано на рис. 16.6.
Рис. 16.6. Замедленное воспроизведение EMF
Трассировка воспроизведения EMF От простейшей задержки мы переходим на следующий уровень — трассировке воспроизведения EMF и выводе информации в текстовое окно. Класс KTraceEMF использует класс KEmfDC для расшифровки записей EMF и вывода данных в текстовом окне, реализованном классом KLogWi ndow. class KTraceEMF : public KEnumEMF
int KEmfDC int HGDIOBJ FLOAT
m_nSeq: m_emfdc: m_value[32]: m_object[8]; m float[8]:
virtual int ProcessRecord(HDC hDC. HANOLETABLE const ENHMETARECORD * pEMFR. int nObj) {
pHTable,
CompareDC(hDC): m_pLog->l_og("£4d: Шх Ш % 6d ". m_nSeq++. pEMFR, pEMFR->iType. pEMFR->nSize): m_pLog->l_og(m_emfdc.DecodeRecord((const EMR *) pEMFR)): m_pl_og->Log("\r\n"): return PlayEnhMetaFileRecord(hDC. pHTable. pEMFR. nObj):
public: KLogWindow * m_pLog: void CompareDC(HDC hDC):
934
Глава 16. Метафайлы
KTraceEMF(HINSTANCE hlnst)
{
m_pLog = new KLogWindow: // Выделенная память освобождается // при обработке WMJJCDESTROY m_pLog->Create(hInst. "EMF Trace"): m_nSeq = 1; memset(m_value. OxCD, sizeof(m_value)): memset(m_object. OxCD, sizeof(m_object)); memsetCm float. OxCD. sizeof(m float)):
Одна из дополнительных возможностей, реализованных в классе KTraceEMF, — сравнение атрибутов контекста устройства перед воспроизведением, между записями EMF и после воспроизведения. Атрибуты контекста устройства запрашиваются обычными функциями GDI (такими, как GetBkMode), сохраняются в трех массивах и сравниваются с предыдущими значениями. Наблюдая за изменениями в атрибутах контекста устройства, вы сможете лучше понять, как реализовано воспроизведение EMF. Ниже приведены неполные данные трассировки, полученные с использованием класса KTraceEMF. НИИ/ЦНИИ Перед выводом /////////////// GraphicsMode : 1 WT.eMll : 1.00000 WT,eM12 : 0.00000 WT.eMZl : 0.00000 WT.eM22 : 1.00000 WT.eDx : 0.00000 WT.eDy : 0.00000 Pen : OxOlbQOOl? Brush : 0x01900010 Font : Ox018a0021 Palette : Oxa50805ca 11IlllllllIIlll Начало вывода 111111111111111 GraphicsMode : 2 WT.eDx : 5.00000 WT.eDy : 5.00000 Font : Ox018a0026 Palette : Ox0188000b 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
012eOOOO 012e0084 012e0094 012eOOa4 012eOOb4 012eOOc4 012eOOdc 012eOOf4 012eQ10c 012e0124 012e0188
1 27 54 27 54 43 43 42 42 76 76
132 // Заголовок 16 MoveToExChDC, 300, 50. NULL): 16 LineTo(hDC, 350, 50): 16 MoveToEx(hDC. 300. 51. NULL): 16 LineTo(hDC. 400. 51): 24 Rectangle(hDC. 300. 60. 349. 109); 24 RectanglethDC, 350. 80. 399, 129): 24 Ellipse(hDC. 410. 60. 459. 149); 24 Ellipse(hDC, 460, 60. 509. 149): 100 PatBlt(hDC. 300. 150. 100. 100. BLACKNESS): 100 PatBlt(hDC. 400. 160. 100, 100. PATINVERT):
93!
Перечисление записей EMF 12: 012e01ec 39 24 hObj[l]=CreateSoHdBrush(RGB(Ox59,Ox97, 0x64)): 13: 012e0204 37 12 SelectObject(hDC. Brush : Ox5fl0045e 71: 012e0978 14
20 // EMREOF(0. 16. 20)
/////////////// После вывода 111111111111111 GraphicsMode 1 0.00000 WT.eDx 0.00000 WT.eDy Ox018a0021 Font Oxa50805ca Palette Происходит нечто весьма интересное. Перед вызовом 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. Метафайлы
Перечисление записей EMF
может создать копию этой записи, изменить ее во время выполнения программы и передать GDI для вывода. Иначе говоря, программа может динамически изменить EMF и передать GDI измененный вариант. Ниже приведен простой класс, который преобразует все цвета текста, фона, перьев и кистей в оттенки серого. Если EMF не содержит цветных растров, в результате воспроизведения классом KGrayEMF цветной метафайл преобразуется в серый. Код преобразования цветных растров в оттенки серого приведен в главе 12. . i n l i n e void MaptoGray(COLORREF & сг) {
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 ProcessRecorcKHDC hDC. HANDLETABLE * pHTable. const ENHMETARECORD * pEMFR, int nObj) { int rslt: switch ( pEMFR->iType ) { case EMR_CREATEBRUSHINDIRECT: { EMRCREATEBRUSHINDIRECT cbi; Cbi = * (const EMRCREATEBRUSHINDIRECT *) pEMFR: MaptoGray(cbi.Ib.lbColor): rslt = PlayEnhMetaFileRecord(hDC. pHTable. (const ENHMETARECORD *) & cbi. nObj); } break; case EMR_CREATEPEN: { EMRCREATEPEN cp; cp = * (const EMRCREATEPEN *) pEMFR: MaptoGray(cp.1opn.1opnColor): rslt = PlayEnhMetaFileRecorcKhDC. pHTable. (const ENHMETARECORD *) & cp, nObj); } break; case EMR_SETTEXTCOLOR: case EMR_SETBKCOLOR: { EMRSETTEXTCOLOR stc: stc = * (const EMRSETTEXTCOLOR *) pEMFR:
937
MaptoGray(stc.crColor); rslt = PlayEnhMetaFileRecorcKhDC. pHTable. (const ENHMETARECORD *) & stc. nObj); break; default: rslt - PlayEnhMetaFileRecord(hDC, pHTable. pEMFR. nObj); return rslt;
Класс KGrayEMF является отдельным представителем целой категории классов преобразований, применяемых к EMF во время воспроизведения. Аналогичным способом можно изменить толщину пера в соответствии с параметрами графического устройства, заменить штриховые узоры, слишком мелкие для печати на принтере, исключить из документа все растровые вставки или отрегулировать цвета.
Построение производных метафайлов Приемный контекст устройства функции PI ayEnhMetaFi I e (а следовательно, и метода KEnumEMF: :EnumEMF) может быть любым допустимым контекстом, в том числе и метафайловым. Если вызвать PI ayEnhMetaFi le для контекста устройства 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; // Преобразовать в логическую систему координат DPtoLP(hDC. pPoint. 21:
938
Глава 16. Метафайлы
HENHMETAFILE FilterEMF(HENHMETAFILE hEmf. KEnumEMF & filter) {
ENHMETAHEADER emh: Get£nhMetaFileHeader(hEmf. sizeof(emh), & emh);
RECT rcFrame: memcpy(& rcFrame. & emh.rclFrame. sizeof(RECT)); HOC hDC - QuerySaveEMFFileC'Filtered EMF\0", & rcFrame. NULL); if ( hDC==NULL ) return NULL: MaplOumToLogicaKhDC, rcFrame): filter.EnumEMF(hDC. hEmf. & rcFrame); return CloseEnhMetaFile(hDC); } Функция Fi 1 terEMF строит новый метафайл на основании данных существующего метафайла. Процесс преобразования полностью контролируется экземпляром KEnumEMF или производного класса. Кадр нового метафайла определяется прямоугольником кадра исходного метафайла, преобразованным в логическую систему координат нового метафайла. Это гарантирует, что новый метафайл имеет те же размеры и те же отступы, что и исходный. Преобразование кадра выполняется вспомогательной функцией MeplOumToLogi cal. Например, если в качестве фильтра используется класс KGrayEMF, функция Fi I terEMF преобразует метафайл в оттенки серого цвета (если в нем отсутствуют цветные растры). Новый метафайл состоит из тех же объектов, но окрашивается в другие цвета. Однако в приведенном примере новый метафайл увеличивается на 424 байта и в нем появляется 26 дополнительных записей. Ниже приведена расшифровка новых записей EMF, полученная при помощи класса KTraceEMF. 2: SaveOC(hOC); 3: SetLayout(hDC. 0); 4: SetMetaRgn(hDC): 5: SelectObjectChDC. GetStockObjet(WHITE_BRUSH)): 6: SelectObject(hDC. GetStockObjet(BLACK_PEN)); 7: SelectObjectthDC. GetStOCkObjet(DEVICE_DEFAULT_FONT)); 8: SelectPalettethDC. (HPALETTE)GetStockObjet(DEFAULT_PALETTE). TRUE): 9: SetBkColor(hDC, RGB(OxFF.OxFF.OxFF)): 10: SetTextColor(hDC, R G B ( O . O . O ) ) ; 11: SetBkMode(hDC, OPAQUE); 12: SetPolyFillModeChDC, ALTERNATE); 13: SetROP2(hDC. R2_COPYPEN); 14: SetStretchBltMode(hDC. STRETCH_ANDSCANS): 15: SetText Align (hDC. TAJOUPDATECP | TAJ.EFT TAJOP); 16: SetBrushOrgEx(hDC, 0. 0, NULL); 17: SetMiterLimit(hDC. 0 . 0 0 0 0 0 ) : 18: // Unknown record [120] 19: MoveToExdiDC, 0. 0. NULL): 20: SetWorldTransform(hDC, 1. 0. 0. 1. 0. 0); 21: ModifyWorldTransformChDC. 1. 0. 0. 1. 0. 0. 0x4 /""Unknown*/); 22: SetLayoutChDC. 0);
Перечисление записей EMF
939
23: // GdiComment(52. GDIC. 0x2) 93: 94: 95: 96:
//GdiComment(8, GDIC, 0x3) RestoreDC(hDC. -1): DeleteObject(hObj[l]); De1eteObject(hObj[2]):
В этом листинге приведен точный список действий, выполняемых GDI при воспроизведении EMF. Поскольку воспроизведение происходит в метафайловом контексте, все операции не просто незаметно выполняются, а фиксируются в виде записей EMF.
Подробнее о воспроизведении EMF Давайте проанализируем все операции, выполняемые GDI при воспроизведении EMF. Сначала GDI сохраняет текущее состояние контекста устройства функцией SaveDC. Напоследок прежнее состояние контекста восстанавливается функцией RestoreDC, поэтому сторона, вызвавшая PlayEnhMetaFile или EnumEnhMetaFile, не заметит никаких изменений в контексте устройства. Далее вызывается редко используемая функция SetMetaRgn. Напомню, что метарегион представляет собой атрибут контекста устройства, который в сочетании с регионом отсечения обеспечивает двухуровневый контроль над отсечением. Функция SetMetaRgn преобразует текущий регион отсечения, заданный приложением перед вызовом EnumEnhMetaFile, в метарегион и сбрасывает регион отсечения в NULL. В процессе воспроизведения EMF регион отсечения в EMF может интерпретироваться как регион отсечения, но при фактическом выводе он всегда объединяется с метарегионом. Перед нами пример очень умного использования метарегионов — впрочем, не исключено, что метарегионы были разработаны как раз для воспроизведения EMF. Обратите внимание: ни прямоугольник кадра, ни прямоугольник воспроизведения не имеют ничего общего с отсечением. Десяток записей, следующих за вызовом SetMetaRgn, понять несложно. Все эти записи присваивают атрибутам контекста устройства значения по умолчанию, чтобы отделить воспроизведение EMF от текущих значений атрибутов. Записи EMF, соответствующие вызовам SetWorl dTransform и Modi fyWorl dTransform, иногда бывают очень интересными. Поскольку мы выполняем все необходимые вычисления в Fil terEMF, обе записи содержат матрицы тождественных преобразований. При действующих преобразованиях смещения или масштабированияматрицы изменились бы соответствующим образом. Как ни странно, в 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 с идентификатором EMR_RESERVED_120. Функция EnumEnhMetaFile позволяет легко узнать, что делает эта запись. Для этого следует включить проверку EMR_RESERVED_120 в функцию косвенного вызова, передать запись GDI функцией PlayEnhMetaFileRecord, установить точку прерывания и проанализировать несколько ближайших ассемблерных команд. Оказывается, функция PlayEnhMetaFileRecord проверяет, входит ли тип записи EMF в допустимый интервал (от EMR_MIN до EMR_MAX), и вызывает функцию из таблицы. Для EMR_RESERVED_120 вызывается функция 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 SETL'INKEDUFIS EMR_RESERVED_120 SETTEXTJUSTIFICATION
GDIComment Специальная функция GDIComment предназначена для включения в EMF комментариев (дополнительных данных). Комментарий может содержать любую приватную информацию, известную обеим сторонам (читающей и записывающей). Например, приложение может включить в EMF PostScript-версию представлен-
EMF как средство программирования
941
ного объекта. Осведомленный получатель данных может воспользоваться данными PostScript вместо того, чтобы заниматься расшифровкой команд GDI. Microsoft документирует несколько стандартных комментариев GDI; все они начинаются с 32-разрядного идентификатора GDICOMMENT_IDENTIFIER, который в текстовом виде соответствует цепочке символов «GDIC». Например, GDICOMMENT_ WINDOWS_METAFILE присоединяет к EMF данные в старом формате метафайлов Windows. Комментарий GDICOMMENT_BEGINGROUP сообщает о начале группы записей, а комментарий GDICOMMENT_ENDGROUP — о ее завершении. В нашем примере встречаются комментарии GDICOMMENT_BEGINGROUP (2) и GDI COMMENT_ENDGROUP (3), которые отделяют вспомогательные записи, добавленные GDI, от исходных команд, переданных приложением. Построение новых метафайлов на базе существующих имеет множество нетривиальных практических применений. Например, к записям EMF могут применяться многочисленные оптимизации — исключение команд создания ненужных объектов или присваивания ненужных значений, а также удаление неиспользуемых частей растров. Возможны и другие применения — например, создание специальных эффектов с использованием не аффинных преобразований. В следующем разделе мы рассмотрим еще одно интересное применение — построение кода С по данным EMF.
EMF как средство программирования Итак, мы выяснили, что EMF представляет собой графический объект, который может использоваться для кодировки, передачи и воспроизведения графических данных между разными приложениями, устройствами и операционными системами. Однако возможность записи команд GDI в одном удобном объекте находит и другие интересные применения. В этом разделе рассматриваются возможности применения EMF не только в области компьютерной графики, но и при программировании.
Декомпилятор EMF В одном из разделов этой главы был представлен класс KEmfDC, выводящий расшифрованные записи EMF в виде иерархического дерева или в текстовое окно. На самом деле расшифровка записей EMF — дело второстепенное. Класс KEmfDC предназначен для декомпиляции EMF во фрагменты кода С. Компиляция и выполнение этих фрагментов приводит к тому же результату, что и воспроизведение EMF. Правила преобразования простых записей EMF в программный код С определяются особыми строковыми шаблонами. Рассмотрим несколько примеров.
const Emrlnfo Pattern [] = {
{ EMR_SETWINDOWEXTEX. "SetWindowExtTexChDC. %d. Zd. NULL):" }. { EMR_EXTCREATEFONTINDIRECTW. "#o-Createfont(%d.%d.%d.%d,%d.%b.%b.%b.%b.%b.%b.%b.%b.XS');" ). { EMR_SETPIXELV . "SetPixeWhDC. *d. %d. #c:)" }.
942
Глава 16. Метафайлы
EMR_POLYGON
. "\nstat1c const POINT Points_*n[]-#P:\n" "PolygonChDC. Pointsjin. %4;" }.
Первый шаблон означает, что запись 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 OnDraw(HDC hDC) {
HGDIOBJ hObj[5] = { NULL }; MoveToEx(hDC. 300. 50. NULL);
LineTo(hDC. 350. 50); RectangleChDC, 300. 60. 349. 109); Ellipse(hDC. 410. 60. 459. 149); PatBlt(hDC.300.150,100.100,BLACKNESS); hObj[!]=CreateSo1i dBrush(RGB(0x59.0x97.0x64)): SelectObj ect(hDC.hObj[1]); PatBH(hDC,300.300.100.100.PATCOPY): SelectObject(hDC. GetStockObject(WHITE_BRUSH)); static const POINT Points_l[]= PolylinethDC. Points 1. 4);
10. 200. 50. 200. 90. 200, 130. 200
static KOIB Dib_l; Dib_l.Load(IOB_BITMAPl);// 350x250x8 Dib_l.StretchDIBits(hDC, 10.10.350.250. 0 . 0 . 3 5 0 . 2 5 0 . DIB_RGB_COLORS.SRCCOPY); Dib_l.StretchDIBits(hDC. 10.270.350.250. DIB RGB COLORS.SRCCOPY);
0.0.350.250.
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
Итоги
: - 'ч-V'.-,;
Windows 2000 Printer Test Page Congratulations <
UJL Рис. 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
Samples\Chapt_16\EMFScope
Тестовая программа для преобразования декомпилированного метафайла в автономную Windows-программу Сохранение и вывод 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>E"
PJL_ENTER_PCL3GUI PCL_RESET
"<1B>@PJL ENTER LANGUAGE-PCL3GUI"
QndStartDoc PCLJJSJ.ETTER: PCL_MEDSOURCE_TRAY1 PCL MEDSOURCE PRELOAD
"&u600D*o5WO409000000>" "<1B>&12A" "<1В>ШН" "<1B>&1-2H"
951
Знакомство со спулером
PCL_MEDIA_PLAIN PCL_PQ_NORMAL PCL_CRD_K662_C334 PCL_ORIENT_PORTRAIT
CmdStartPage Raster Data
"<1В>ШМ" "<1В>*оОМ" "*g26W<0204025802580002012C012C0004012C>" "<012C0004012C012C0004>" "<1B>&100"
"&iOE*pOyOX&10l*rlA" "<1B2A62306D32393779326D313776ACOO>" "<0103COEA0003FFFCOOCOEA000103C031>" "<3776AC000103COEA0003FFFCOOCOEAOO>"
QndEndPage
"<1В>*гС<ОО"
PJL_EXIT_LANGUAGE
"<1B>H-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 WTitle: Document ««Creator: Pscript.dll Version 5.0 ««Orientation: Portrait
Знакомство со спулером
953
дает ему данные. Если приложение знает язык управления принтером, оно может сделать то же самое и общаться с принтером напрямую. Следующий фрагмент показывает, как передать файл в порт принтера. BOOL SendFiletHANDLE hOutput. const TCHAR * filename, bool bPrinter) { HANDLE hFile = CreateFile(filename. GENERIC_READ. FILE_SHARE_READ. NULL. OPEN_EXISTING. FILE_ATTRIBUTE_NORMAL. NULL): if ( hFile=-INVALID_HANDLE_VALUE ) return FALSE: char buffer[1024];
HPageOrder: Special «STargetDevice: (HP Color LaserJet 8500) (3010.104) 1 ««LanguageLevel: 3 ««EndComments
for (int size - GetFileSize(hFile. NULL); size; ) { DWORD dwRead = 0. dwWritten = 0: if ( ! ReadFile(hFile. buffer. min(size. sizeof(buffer)). & dwRead. NULL) ) break;
««IncludeResource: font TimesNewRomanPSMT F /FO 0 /256 Т /TimesNewRomanPSMT mF /FOS53 FO [83 0 0 -83 0 0] mFS FOS53 setfont
if ( bPrinter ) WritePrinter(hOutput. buffer. dwRead, & dwWritten); else WhteFile(hOutput, buffer, dwRead, & dwWritten. NULL);
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
size -= dwRead;
showpage
(И[Раде: 1]Ш = ««PageTrailer ««Trailer moundingBox: 12 12 600 780 ««Pages: 1 ««[LastPage]««) = ««EOF <1B>«-12345X@PJL EOJ <1B>«-12345X
return TRUE;
void Demo_WhtePort(void) KFileDialog fd: if ( fd.GetOpenFileName(NULL. "prn", "Raw printer data") ) HANDLE hPort = CreateFileC'lptl:". GENERIC_WRITE. FILE_SHARE_READ. NULL. OPENJXISTING. FILE_ATTRIBUTE_NORMAL. NULL):
После длинного заголовка, макросов и определений начинается последовательное преобразование графических команд GDI в команды PostScript. Оператор setfont выбирает шрифт Times New Roman в качестве текущего, оператор moveto задает позицию вывода, оператор xshow предназначен для вывода текста с точным указанием позиции символов, а оператор showpage начинает печать страницы.
Прямой вывод в порт Как упоминалось выше, низкоуровневые данные, сгенерированные драйвером принтера, передаются монитором порта драйверу порта ввода-вывода и в конечном итоге пересылаются на принтер. Монитор порта стандартными файловыми операциями Win32 открывает манипулятор для драйвера ввода-вывода и пере-
if ( hPort!=INVALID_HANDLE_VALUE ) {
SendFile(hPort. fd.mJitleName. false): CloseHandle(hPort):
i
Функция 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 pDocInfo)• . 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 сообщает спулеру о появлении нового документа, который следует поставить в очередь печати. В последнем параметре этой функции передается указатель на структуру DOC_INFO_1 или DOC_INFO_2, определяющую название документа, имя выходного файла и тип данных для задания печати. DLL спулера создает новое задание печати функцией AddJob и файл спулинга в каталоге спулера. Функция StartDocPrinter используется в паре с функцией
955
Знакомство со спулером
EndDocPrinter, которая завершает задание печати, удаляет его из спулера и освобождает все выделенные ресурсы. Функция StartPagePrinter сообщает спулеру о начале новой страницы. После вызова этой функции приложение может передавать данные спулеру функцией WritePrinter. Каждому вызову StartPagePrinter должен соответствовать парный вызов EndPagePri nter, после которого начинается новая страница или завершается задание печати. Если произошла ошибка, задание печати можно отменить функцией Abort Printer. Функции спулера экспортируются клиентской библиотекой DLL спулера Win32 winspool.drv, чтобы программы Windows могли взаимодействовать со спулером. Однако настоящая реализация спулера находится в отдельном системном процессе (spoolsv.exe). Клиентская библиотека DLL общается со спулером через механизм RPC. Например, функция StartPagePrinter вызывает RpcStartPagePrinter, которая в свою очередь вызывает NdrC1ientCall2. Функциями спулера можно воспользоваться и для отправки низкоуровневых данных на принтер. Впрочем, полная поддержка спулинга позволяет сделать много больше. Выражаясь точнее, вы можете передавать любые данные при условии, что они поддерживаются процессором печати, который отвечает за обработку данных. Для проверки форматов данных, поддерживаемых процессором печати для конкретного драйвера принтера, откройте окно свойств принтера и перейдите на вкладку Advanced (Дополнительно). На рис. 17.1 показано, какие форматы поддерживаются для стандартного процессора печати Windows 2000.
*{** (1
3] ,,-
•'f-JBiiver; )НР DeskJet SFMPSPRT
!•'/ ^,8«p
RAW [FF appended] RAW [FF auto] NT EMF 1.003 NT EMF 1.006 NT EMF 1.007 NT EMF 1.008 TEXT
Cancel
Рис. 17.1. Типы данных, поддерживаемые стандартным процессором печати Windows
956
Глава 17. Печать
Как видно из рисунка, стандартный процессор печати Windows 2000 поддерживает три категории типов данных: низкоуровневые, текстовые и NT EMF Хотя в списке присутствуют четыре версии NT EMF, точные спецификации и различия между ними не документированы. Также следует обратить внимание на то, что имя WinPrint не соответствует физической библиотеке DLL. Стандартный процессор печати Windows 2000 является частью локального процессора печати (localspi.dll), о чем свидетельствуют имена экспортируемых функций — такие, как PrintDocumentOnPrintProcessor. Небольшой эксперимент убедит всех скептиков в том, что документированный интерфейс API спулера благополучно справляется с EMF-файлами. Следующая функция иллюстрирует использование функций спулера с EMF-файлами. void Demo_WritePnnter(void) { PRINTDLG
pd;
memseU&pd. 0, sizeof(PRINTDLG)); pd.IStructSize = sizeof(PRINTDLG):
if ( PrintDlg(Spd)==IDOK ) { HANDLE'hPrinter: DEVMODE * pDevMode = (DEVMODE *) GlobalLock(pd.hDevMode); PRINTER_DEFAULTS prn; prn.pDatatype = "NT EMF 1.008"; prn.pDevMode = pDevMode; prn.DesiredAccess = PRINTER_ACCESS_USE; if ( OpenPrinter((char *) pDevMode->dmDeviceName. & hPrinter. & prn) )
{ KFileDialog fd; if ( fd.GetOpenFileName(NULL. "spl". "Windows 2000 EMF Spool file") ) { DOC_INFO_1 docinfo; docinfo.pDocName - "Testing WritePrinter"; docinfo.pOutputFile = NULL; docinfo.pDatatype = "NT EMF 1.008": \StartDocPrinter(hPrinter, 1, (BYTE *) & docinfo); StartPagePnnter(hPrinter); SendFilethPrinter. fd.mJitleName. true); EndPagePri nter(hPri nter): EndDocPrinter(hPrinter);
Знакомство со спулером
957
ClosePrinter(hPrinter);
if ( pd.hDevMode ) GlobalFree(pd.hDevMode): if ( pd.hDevNames ) GlobalFree(pd.hDevNames):
Функция Demo_WritePrinter создает стандартное диалоговое окно для выбора принтера, на котором будет производиться печать. Если в диалоговом окне был выполнен щелчок на кнопке ОК, в структуру PRINTDLG заносится глобальный манипулятор блока структуры DEVMODE, содержащей все параметры печати. Манипулятор DEVMODE преобразуется в указатель и используется для заполнения полей структуры 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 используется структура DEVMODE с текущими настройками принтера. Правильнее было бы извлечь структуру DEVMODE из SHD-файла, сгенерированного вместе с EMF-файлом. Дело в том, что некоторые поля структуры DEVMODE могут измениться с момента последнего построения EMF-файла. Рассмотрим маленькую схему, которая помогает лучше представить, как реализован спулинг EMF-файлов. Если при создании нового задания GDI решает, что спулинг EMF разрешен, то новое задание спулера в формате EMF создается аналогичным способом. Для каждого вызова функции GDI данные записи EMFфайла передаются спулеру при помощи W r i t e P r i n t e r . Функция StartDoc GDI вызывает StartDocPrinter, функция StartPage вызывает StartPagePrinter и т. д., а вывод из очереди организуется процессором печати. Конечно, это всего лишь упрощенная концептуальная схема. Мы вернемся к диалоговым окнам принтера и структуре DEVMODE при описании печати средствами 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 выглядит следующим образом: BOOL GdiPlayEMFCLPWSTR pwszPrinterName. LPDEVMODEW pDevmode. ELPWSTR pwszDocName. EMFPLAYPROC prnEMFPlayFn. HANDLE hPageQuery); Как видите, процессор печати получает только имя принтера, DEVMODE и имя документа. Все, что он может, — вывести несколько экземпляров документа многократным вызовом GdiPlayEMF. Последние два параметра предназначены для избирательного воспроизведения отдельных страниц EMF, но в Windows NT 4.0 эта возможность не поддерживается. В Windows 2000 возможности процессора печати были расширены. Теперь он может управлять воспроизведением страниц EMF, объединять несколько логических страниц на одной физической странице, изменять порядок печати страниц в документе и даже применять преобразования к логическим страницам. С учетом этих усовершенствований процессор печати Windows 2000 позволяет выводить несколько страниц на одной физической странице, печатать страницы в обратном порядке, выводить несколько копий каждой страницы, печатать брошюры и контуры страниц. Все эти возможности реализуются централизованно и легко расширяются без модификации GDI и драйверов принтеров. Доступ к новым возможностям процессора печати открывают новые функции, экспортируемые GDI. Ниже приведены объявления трех важнейших функций. HANDLE GdlGetPageHandleCHANDLE SpoolFileHandle. DWORD Page. LPDWORD pdwReserved): BOOL GdiPIayPageEMF(HANDLE SpoolFlleHandle, HANDLE hEmf. RECT * prectDocument. RECT * prectBorder); HOC Gd1GetDC(HANDLE SpoolFileHandle); Функция 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, после чего в цикле вызывает функции Internal GdiPlayPageEMF и NextBand. Функция Internal GdiPlayPageEMF настраивает мировое преобразование и вызывает интересную функцию PrintBand. Функция PrintBand вызывает системную функцию NtGdiGetPerBandlnfo, соответствующую документированной точке входа драйвера принтера и передающую GDI информацию об очередной полосе. Затем PrintBand вызывает функцию PlayEnhMetaFile, которая и воспроизводит EMF в контексте устройства драйвера принтера. Где-то в этой схеме должен присутствовать цикл перебора логических страниц на физической странице. Вероятно, теперь вы гораздо лучше понимаете, почему EMF отводится центральное место в системе печати, особенно в Windows 2000.
Перечисление принтеров В спулерный интерфейс Win32 API входит функция EnumPrinter, при помощи которой приложение может получить список принтеров и запросить информацию о принтере. Функция EnumPrinter весьма сложна, она имеет большое количество параметров и возвращает разные типы структур. С ее помощью можно получить списки локальных принтеров, провайдеров печати, имен доменов, а также всех принтеров и серверов печати в домене. Функция EnumPrinter заполняет
960
Глава 17. Печать
массивы структур от PRINTER_INFO_1 до PRINTER_INFO_5. Наиболее полная информация о принтере хранится в структуре PRINTER_INFO_2, состоящей из 21 поля, в том числе из полей имени сервера, имени принтера, имени драйвера, DEVMODE, .процессора печати, типа данных спулинга и т. д. За подробной информацией о EnumPrinter обращайтесь к MSDN. Ниже приведен пример функции, которая перечисляет все локальные принтеры и подключения к удаленным принтерам. .void * EnumeratePrinters(DWORD flag. LPTSTR name. DWORD level. DWORD & nPrinters)
Знакомство со спулером
961
локальных принтеров, а затем — для удаленных принтеров. Имена принтеров заносятся в список, в котором пользователь может выбрать нужный принтер. Перечисление принтеров позволяет приложениям конструировать нестандартные диалоговые окна печати. Например, стандартные диалоговые окна Windows могут оказаться неподходящими для игры или учебной программы, написанной для DirectX. Некоторые приложения должны предоставлять пользователю быстрый доступ к принтеру по умолчанию. Имя текущего принтера по умолчанию можно получить функцией GetDefaultPrinter, которая поддерживается только в Windows 2000. BOOL GetDefaultPrinter(LPTSTR pszBuffer. LPDWORD pcchBuffer):
DWORD cbNeeded: nPrinters = 0; EnumPrinterstflag. 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 ListPrinters(HWND hWnd, int message) { • DWORD nPrinters: PRINTER_INFO_5 * plnfoS = (PRINTER_INFO_5 *) EnumeratePrinters(PRINTER_ENUM_LOCAL, NULL, 5, nPrinters): if ( plnfoS ) { for (unsigned i=0; i
PRINTER_INFO_1 * plnfol = (PRINTER_INFO_1 *) EnumeratePrinters(PRINTER_ENUM_CONNECTIONS. NULL. 1. nPrinters): if ( plnfol ) for (unsigned 1=0: i
Функция EnumeratePrinters представляет собой простую оболочку для EnumPrinters. Она управляет получением данных о размерах блока и выделением памяти. Функция ListPrinters сначала вызывает EnumeratePrinters для перечисления
Получение информации о принтере По манипулятору принтера функция GetPrinter возвращает разнообразные сведения о принтере. Она может возвращать различные структуры от PRINTER_INFO_1 до PRINTER_INFO_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 dmFormNameCCCHFORMNAME]: WORD dmLogPixels: . DWORD dinBitsPerPel : DWORD dmPelsWidth: DWORD dmPelsHeight; DWORD dmDisplayFlags: union { DWORD dmDisplayFlags: DWORD dmNup; 1 DWORD dmDisplayFrequency: DWORD dmlCMMethod: DWORD dmlCMIntent; DWORD dmMediaType; DWORD dniDi therType: 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. Поле dmFi el ds содержит информацию об инициализированных полях. Разным полям соответствуют разные флаги; например, флаг DMJ3RIENTATION относится к полю dmOrientation. В остальных полях DEVMODE хранятся в основном параметры устройства. Поле dmPrintQuality обычно определяет качество печати, которое существенно влияет на внешний вид напечатанных страниц, скорость, размер данных принтера и т. д. Качество печати обычно задается стандартными макросами DMRESJHIGH, DMRES_ MEDIUM, DMRES_LOW и DMRES_DRAFT. Драйвер принтера обычно сообщает GDI разные разрешения в зависимости от текущего значения поля dmPrintQuality, от чего зависит объем данных, получаемых им от GDI. Качество печати также влияет на воспроизведение графических команд драйвером принтера. Фактические значения этих макросов лежат в интервале от -4 до -1. Вместо этих значений драйвер принтера может присвоить полю dmPrintQuality фактически используемое разрешение (за подробностями обращайтесь к MSDN). Функция DocumentProperties заполняет структуру DEVMODE по имени и манипулятору принтера. LONG DocumentPropertiesCHDC hWND. HANDLE hPrinter, LPTSTR pDeviceName. PDEVMODE pDevModeOutput. PDEVMODE pDevModelnput. DWORD fMode): Последний параметр fMode определяет операцию, выполняемую функцией. Если fMode = 0, функция возвращает общий размер открытых и закрытых полей DEVMODE. Если fMode = DM_OUT_BUFFER, то структура, на которую указывает параметр pDevModeOutput, заполняется текущими параметрами DEVMODE заданного драйвера. Если fMode = DM_IN_BUFFER, параметр pDevModelnput указывает на структуру DEVMODE с новыми значениями параметров. Если fMode = DM_IN_PROMPT, функция выводит окно свойств принтера, в котором пользователь может изменить текущую конфигурацию. Следующая функция 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 = С DocumentPropertiesCNULL. hPrinter, PrinterName. pDevMode. pDevMode. DM_IN_PROMPT | DM_OUT_BUFFER | DM_IN_BUFFER ) == IDOK ): break:
ClosePrinter(hPrinter) :
if ( rslt ) return pDevMode: else delete [] (BYTE return NULL:
965
Базовая печать средствами GDI
ров пользовательского режима, интерфейсная библиотека DLL все равно должна существовать отдельно от базового драйвера. '"
'tixii
-2Ш
JS HP DeskJet 8S5Cxi Advanced Document Settings -tr'Qf Paper/Output •i jji] Graphic 3 fe Document Options 1 Enabled Colt» Printing Mode u.,'.jk". Printei Features Print Mode: Prnl Quality No ms
$ HP DeskJet 895Cxi Device Settings Form To Tray Assignment Manual Paper Feed: Envelope, Manual Feed: U
Рис. 17.2. Окна настройки драйвера ) pDevMode:
Работа функции GetDEVMODE начинается с вызова функции DocumentProperties, возвращающей реальный размер структуры DEVMODE. После выделения памяти 'функция DocumentProperties вызывается снова для получения текущих настроек DEVMODE, заданных пользователем в панели управления. Последний параметр GetDEVMODE указывает, следует ли предложить пользователю изменить настройки печати на странице свойств драйвера принтера или ограничиться окном дополнительных настроек (Advanced). Функция AdvancedDocumentProperties также поддерживается клиентской библиотекой DLL спулера Win32. Другая взаимосвязанная функция, PrinterProperties, выводит страницу свойств заданного принтера. На рис. 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 система допускает существование драйверов принте-
Реализация этих трех функций загружает интерфейсную библиотеку DLL в адресное пространство приложения для настройки параметров драйвера или свойств принтера. При этом загружаются и все используемые ей DLL. Загруженные библиотеки DLL обычно выгружаются перед выходом из этих функций. Похоже, в Windows 2000 предусмотрены какие-то меры оптимизации для простых запросов, но вывод страниц свойств по-прежнему сопровождается загрузкой десятка системных библиотек DLL.
Базовая печать средствами GDI Функции спулера, описанные в предыдущем разделе, обеспечивают взаимодействие приложения со спулером и интерфейсными библиотеками DLL драйвера принтера. Обычно приложение производит настройку принтеров при помощи стандартных диалоговых окон, а для печати использует функции GDI. За кулисами стандартные диалоговые окна и GDI взаимодействуют со спулером и драйвером печати при помощи функций спулера. В этом разделе рассматривается базовая процедура создания заданий печати с использованием стандартных диалоговых окон и функций GDI.
Стандартные диалоговые окна печати Стандартные диалоговые окна не относятся к числу основных средств Win32 API, поскольку все их возможности при желании можно имитировать другими
966
Глава 17. Печать
функциями Win32. Однако эти окна определяют фактический стандарт пользовательского интерфейса для выполнения некоторых действий в операционной системе Windows. До настоящего момента мы рассмотрели стандартные окна для выбора цвета и шрифта, а также открытия/сохранения файла; все они были вполне удобными. Для печати в Windows предусмотрены два стандартных диалоговых окна — окно печати и окно параметров страницы. typedef struct tagPD { DWORD IStructSize; HWND hwndOwner: HGLOBAL hDevMode: HGLOBAL hDevNames; HOC HOC: DWORD Flags: WORD nFromPage; WORD nToPage: WORD nMinPage: WORD nMaxPage: WORD nCopies: HINSTANCE hlnstance: LPARAM ICustData; LPPRINTHOOKPROC IpfnPrintHook; LPSETUPHOOKPROC IpfnSetupHook; LPCSTR IpPrintTemplateName: LPCSTR 1pSetupTemplateName: HGLOBAL hPrintTemplate; HGLOBAL hSetupTemplate: } PRINTDLG; typedef struct tagPSD DWORD HWND HGLOBAL HGLOBAL DWORD POINT RECT RECT HINSTANCE LPARAM LPPAGESETUPHOOK LPPAGEPAINTHOOK LPCSTR HGLOBAL } PAGESETUPDLG:
IStructSize; hwndOwner; hDevMode; hDevNames: Flags; ptPaperSize: rtMinMargin: rtMargin; hlnstance; ICustData; IpfnPageSetupHook; IpfnPagePaintHook: 1pPageSetupTemplateName: hPageSetupTemplate:
BOOL PageSetupDlg(LPPAGESETUPDLG Ippsd); Функция PrintDIg выводит стандартное диалоговое окно печати, в котором пользователь выбирает принтер, диапазон печатаемых страниц, количество копий, режим подбора по копиям, а также может вызвать страницы свойств принтера. Функция PageSetupDlg выводит стандартное окно параметров страницы, в котором пользователь выбирает размер бумаги, источник бумаги, ориентацию
967
Базовая печать средствами GDI
страницы и размеры полей. Функция PrintDIg также позволяет получить текущие настройки принтера по умолчанию без вывода диалогового окна; для этого при вызове ей передается флаг PD_RETURNDEFAULT. На рис. 17.3 показано, как выглядят оба окна в Windows 2000.
1^<&V^?;^" v-'^^^
; < t't>L^Li&d ;-!^«fe--v, *mi&t:',.vi?<">,.';!, r&&*.:,. .«.,I,A 1
^^^^I^^^^^^^~~^K «teftr *:
-f^vV. ji'- ^ ;;,ff4l^
Рис. 17.3. Стандартные диалоговые окна печати и параметров страницы в Windows 2000
Функция PrintDIg использует структуру PRINTDLG для получения параметров от приложения, возврата результатов приложению и настройки внешнего вида окна. В поле hDevMode хранится глобальный манипулятор структуры DEVMODE, содержащей параметры печати. Поле hDevNames содержит глобальный манипулятор структуры DEVNAMES, содержащей имена драйвера принтера, устройства и порта вывода. Глобальные манипуляторы (а точнее, манипуляторы глобальных блоков памяти) были унаследованы от Win 16 API, где все задачи в системе работали в общем адресном пространстве. Глобальный манипулятор используется для ссы-. лок на блок памяти, выделенный из глобальной кучи. В результате дефрагментации и освобождения места для больших блоков такие блоки памяти могли перемещаться в куче, поэтому их приходилось фиксировать функцией Global Lock, возвращавшей дальний указатель на блок, и освобождать функцией Gl obal Unl ock. В Win32 API поддержка глобальных манипуляторов была сохранена только для того, чтобы упростить адаптацию 16-разрядных приложений. Впрочем, и в программах Win32 манипулятор глобального блока памяти и указатель на этот блок — совершенно разные вещи. Чтобы преобразовать манипулятор глобального блока в указатель на блок данных, следует вызвать функцию Global Lock. Впрочем, манипуляторы ресурсов имеют те же значения, что и соответствующие указатели. Поле hDC структуры PRINTDLG содержит манипулятор контекста устройства GDI или информационного контекста, созданный и возвращенный функций PrintDIg
968
Глава 17. Печать
при передаче флага PD_RETURNDC или PD_RETURNIC. Поле Flags управляет интерпретацией входных данных структуры и построением выходных данных. Следующие четыре поля — nFromPage, nToPage, nMinPage и пМахРаде — управляют настройкой диапазона печатаемых страниц (см. левый нижний угол диалогового окна печати) — весьма удобная возможность для приложений, поддерживающих многостраничные документы. Поле nCopies задает количество копий. Остальные поля PRINTDLG предназначены для настройки диалогового окна печати. Программы с модифицированными окнами печати встречаются довольно часто. Например, программа бухгалтерского учета может поддерживать режим печати по интервалам дат вместо страниц. За подробным описанием структур PRINTDLG обращайтесь к MSDN. Функция PageSetupDlg использует аналогичную структуру PAGESETUP для получения параметров от приложения, возврата результатов приложению и настройки внешнего вида окна. Поля hDevMode и hDevNames структуры PAGESETUP имеют тот же смысл, что и в структуре PRINTDLG. Поле Flags также содержит десятки всевозможных флагов, управляющих работой PageSetupDl g. Главная информация PAGESETUP содержится в полях ptPageSize, rtMinMargin и rtMargin. Поле rtMinMargin задает минимальный размер полей при печати листа. Вследствие физических ограничений, обусловленных конструкцией принтера, на всех четырех сторонах листа имеются области, печать в которых невозможна. Поле rtMargin задает текущие поля, введенные пользователем в диалоговом окне, которые должны быть не меньше rtMinMargin. Ориентация листа сопровождается сменой горизонтальных и вертикальных метрик, а также отражается в структуре DEVMODE. Данные, возвращаемые функциями PrintDlg и PageSetupDlg, чрезвычайно важны — по ним приложение форматирует документ для печати. Следовательно, они должны использовать одни и те же манипуляторы DEVMODE и DEVNAMES, чтобы обеспечить согласованность настроек. Приложение также должно поддерживать настройку параметров печати на уровне документа, чтобы их можно было сохранить вместе с документом и восстановить при загрузке. Некоторые приложения предлагают пользователю выбрать принтер перед созданием документа. Многие приложения обращаются с запросом к драйверу принтера, чтобы синхронизировать параметры печати при загрузке документа. В листинге 17.1 приведено объявление и часть реализации класса KOutputSetup, инкапсулирующего функции 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 SetDefaulKHWND hwndOwner. int minpage. int maxpage):
969
Базовая печать средствами GDI
int PrintDialogfDWORD flag); BOOL PageSetuptDWORD flag); void GetPaperSize(POINT & p) const p = m_psd.ptPaperSize: void GetMargintRECT & rect) const rect = m_psd.rtMargin: void GetMinMargin(RECT & rect) const rect = m_psd.rtMinMargin; HOC 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; HOC CreatePrinterDC(void): }: KOutputSetup::KOutputSetup( voi d) memset (&m_pd. 0. sizeof(PRINTDLG)): m_pd.lStructSize = sizeof(PRINTDLG): memset(& ra_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: // m_psd.rtMargin.right = 1250; // m_psd.rtMargin.top = 1000: // m_psd.rtMargin.bottom- 1000; //
1.25 1.25 1.25 1.25
дюйма дюйма дюйма дюйма
Продолжение
970
Глава 17. Печать
971
Базовая печать средствами GDI
Листинг 17.1. Продолжение HOC hDC = CreatePrinterDCO:
PHYSICALWIDTH
int dpix = GetDeviceCaps(hDC, LOGPIXELSX): int dpiy = GetDeviceCapsthDC. LOGPIXELSY): m_psd.ptPaperSize.x mjDsd.ptPaperSize.y
t PHYSICALOFFSETY
GetOeviceCaps(hDC. PHYSICALWIDTH) * 1000 / dpix: GetDeviceCapsChDC. PHYSICALHEIGHT) * 1000 / dpiy:
m_psd.rtMi nMargi n. 1 eft 1000 / dpix: m_psd.rtMi nMargi n.top 1000 / dpiy:
-»
*— PHYSICALOFFSETX HQRZRES
GetDeviceCapsthDC, PHYSICALOFFSETX) *
\
GetDeviceCaps(hDC. PHYSICALOFFSETY) * PHYSICAL^ EIGI IT
m_psd. rtMi nMargi n. right - m_psd.ptPaperSize.x - m_psd. rtMi nMargin. left GetDeviceCaps(hDC, HORZRES) * 1000 / dpix:
VEP TRES
m_psd.rtMinMargin.bottom= m_psd.ptPaper$ize.y - m_psd. rtMi nMargin. top GetDeviceCaps(hDC. VERTRES) * 1000 / dpiy: DeleteObject(hDC);
int KOutputSetup: :PrintDialog(OWORD flag) {
•i r
m_pd. Flags = flag: return PrintD1g(&m_pd):
Рис. 17.4. Размеры бумаги, возвращаемые функцией GetDeviceCaps
BOOL KOutputSetup ::PageSetup( DWORD flag) m_psd . hDevMode m_psd . hDevNames m_psd. Flags
m_pd . hDevMode ; m pd.hDevNames: fTag | PSDJNTHOUSANDTHSOFINCHES
PSD_MARGINS:
return PageSetupD1g(& m_psd): Класс KOutputSetup напоминает классы CPrintDialog и CPageSetupDialog MFC, слитые воедино. Конструктор просто инициализирует переменные m_pd (структура PRINTDLG) и m_psd (структура PAGESETUPDLG). Метод SetDefault присваивает значения нескольким важным полям без вывода диалогового окна. Для m_pd вызов PrintDlg с флагом PD_RETURNDEFAULT возвращает манипуляторы DEVMODE и DEVNAMES. Поля выбора страниц заполняются по значениям параметров SetDefault. По умолчанию размеры полей равны 1,25 дюйма для левого и правого поля и 1 дюйм для верхнего и нижнего поля. Минимальные размеры полей вычисляются с учетом физических размеров листа, печатных размеров листа и физических смещений; все эти данные возвращаются функцией GetDeviceCaps с соответствующим индексом. Размеры бумаги, возвращаемые при передаче индексов PHYSICALWIDTH и PHYSICALHEIGHT, определяют физические размеры; так, при разрешении 300 dpi размер листа формата Letter (8,5 х 1 1 дюймов) в пикселах равен 2250 х 3300. Размеры печатной части листа, возвращаемые при передаче индексов HORZRES и VERTRES, определяют размеры печатной области в центре листа. Физические смещения (индексы PHYSICALOFFSETX и PHYSICALOFFSETY) определяют размеры верхних и нижних полей. На рис. 17.4 показан смысл шести метрик, возвращаемых функцией GetDeviceCaps.
Метод PrintDialog вызывает функцию PrintDlg, реализованную модулем стандартных диалоговых окон. Метод PageSetup вызывает функцию PageSetupDlg с теми же манипуляторами DEVMODE и DEVNAMES, которые используются при вызове PrintDlg; это обеспечивает синхронизацию их значений.
Создание контекста устройства принтера Функция PrintDlg (а следовательно, и класс KOutputSetup) может вернуть контекст устройства принтера, позволяющий организовать вывод на печать средствами GDI. Для этого при вызове PrintDlg передается флаг PD_RETURNDC. Рассмотрим простой пример: void Demo_OutputSetup(bool 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, на экран выводится диалоговое окно для настройки принтера; в противном случае используются текущие параметры. В обоих случаях возвращается манипулятор контекста устройства принтера, который может использоваться программой, и манипуляторы структур DEVMODE и DEVNAMES. Все эти ресурсы освобождаются деструктором класса KOutputSetup. В общем, не происходит ничего особенного — контекст устройства принтера создается хорошо знакомой функцией CreateDC. Вспомните, что функция CreateDC получает четыре параметра: имя драйвера, имя устройства, имя порта/файла вывода и указатель на структуру DEVMODE. Прототип CreateDC унаследован от Winl6 API, где графический драйвер представлял собой загружаемую 16-разрядную библиотеку DLL. В приложениях Win32 приложения уже не могут напрямую обращаться к драйверу графического устройства. Все параметры, необходимые для вызова CreateDC, хранятся в структуре PRINTDLG. Имена драйвера, устройства и порта вывода содержатся в структурах DEVMODE и DEVNAMES. Приведенный ниже метод KOutputSetup: :CreatePrinterDC создает контекст устройства для принтера по манипуляторам DEVMODE и DEVNAMES. HOC KOutputSetup::CreatePrinterDC(void) {
return CreateDC(NULL. GetDeviceNameO. NULL. GetDevModeO):
const TCHAR * KOutputSetup::GetDev1ceName(vo1d) const { const DEVNAMES * pDevNames = (DEVNAMES *) GlobalLock(m_pd.hDevNames): if ( pDevNames ) return (const TCHAR *) ( (const char *) pDevNames + pDevNames->wDeviceOffset ); else return NULL:
} Методы GetDriverName, GetDeviceName и GetOutputName берут имена драйвера, устройства и порта вывода из структуры DEVNAMES. Типичными значениями являются «winspool», имя устройства и «Iptl:». winspool.drv — клиентская библиотека DLL спулера Win32, предоставляющая все функции спулера приложениям Win32. Для создания контекста устройства принтера абсолютно необходим только один параметр — имя устройства. Имя драйвера подставляется автоматически; без имени порта вывода можно обойтись, поскольку оно передается GDI при вызове StartDoc; указатель на DEVMODE тоже необязателен. Если вместо указателя на DEVMODE передается NULL, драйвер принтера использует текущие настройки принтера, заданные в панели управления. Чтобы создать контекст устройства с нестандартными параметрами, необходимо передать правильно заполненную структуру DEVMODE. Самый простой способ создания контекста устройства принтера в Windows 2000 без стандартных диалоговых окон печати заключается в использовании GetDefaultPrinter и CreateDC. В следующем фрагменте создается контекст устройства для текущего принтера по умолчанию со стандартными параметрами: TCHAR PrinterName[64]: DWORD Size = 64:
Базовая печать средствами GDI
973
GetDefauHPrintertPrinterName. & Size): HOC hDC = CreateDC(NULL. PrinterName. NULL. NULL): // Использовать DC принтера DeleteDC(hDC);
При создании контекста устройства также можно запросить список принтеров, получить стандартную структуру DEVMODE, изменить ее и создать контекст устройства с именем принтера и настройками по вашему выбору.
Получение информации о контексте устройства принтера Получив манипулятор контекста устройства «принтер», вы можете получить информацию о контексте функцией GetDeviceCaps. Вызов GetDeviceCaps с индексом TECHNOLOGY позволяет узнать тип принтера. Для плоттеров возвращается значение DT_PLOTTER, а для любых растровых принтеров и даже принтеров PostScript возвращается DT_RASTPRINTER. Учтите, что некоторые плоттеры нового поколения тоже используют растровую технологию вместо традиционного набора из 8 перьев. При передаче индексов LOGPIXELSX и LOGPIXELSY функция возвращает разрешение принтера — важный показатель, используемый приложениями при настройке логического контекста устройства. В отличие от экранных устройств, которые при помощи логического разрешения увеличивают изображение на экране для удобства просмотра, на бумаге один дюйм всегда соответствует ровно одному дюйму. Впрочем, необходимо помнить о некоторых обстоятельствах, относящихся к разрешению принтера. О Разрешение принтера зависит от выбранного качества печати. Драйвер принтера может сообщать GDI разные значения — 1200 х 1200 dpi, 600 х 600 dpi или 300 х 300 dpi — в зависимости от качества печати и типа носителя в структуре DEVMODE. О Лист со всех четырех сторон ограничен полями (см. рис. 17.4). Вызовы GetDeviceCaps с индексами HORZRES и 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 и NUMCOLORS позволяют определить формат палитры и пикселов устройства. Впрочем, вам вряд ли удастся найти драйвер принтера с поддержкой палитры. Индексы CLIPCAPS, RASTERCAPS, CURVECAPS, LINECAPS, POLYGONALCAPS и TEXTCAPS, относящиеся к поддержке примитивов DDI со стороны драйвера устройства, не играют особой роли для приложений в системах семейства NT. В этих системах графический механизм гораздо лучше справляется с поддержкой вывода в стандартном кадровом буфере формата DIB и разбиением команд GDI на примитивы. Скорее, эти атрибуты используются графическим механизмом для получения информации о драйвере принтера. Впрочем, если у вас возникнут проблемы с конкретным драйвером принтера — проверьте значения этих атрибутов. Возможно, это поможет вам в процессе диагностики. В Windows 98/2000 добавились новые индексы SHADEBLENDCAPS и COLORMGMCAPS для проверки возможностей устройства по поддержке градиентных заливок, альфа-наложения и управления цветом. При помощи функций GDI также можно перечислить шрифты, поддерживаемые принтером. Современные принтеры часто поддерживают шрифты, недоступные для драйвера экрана, На рис. 17.5 показаны атрибуты контекста устройства принтера, полученные при помощи функции GetDevlceCaps и взятые из структуры PRINTER_INFO_2, заполненной при вызове GetPrinter. Для сбора информации использовалась несколько измененная версия программы Device из главы 5.
w^ff6f№SK^&m>^^ff^^ff^^m
777^; ,-,' . '' ' ','.:•/'••<•"?'•' ,'-'
1»Ф* - "^Г '^""'" iTvste '"*'" ' ;_.'.?-Ll± . '
' mini * сп^ги-г ь^л
РчМ.',
f
,
',
'
*
'
'" "'jVeluiw'
Driver Name Printer Name Share Name Port Name Driver Name Comment Location Separator Page Print Processor
HP DeskJet 970C Series HP DJ970C
LPT1: HP DeskJet 970C Series
WinPrint
0C.
is&L-.
TECHNOLOGY DRIVERVERSION HORZSIZE VERTSI2E
HURZRtS LI ("ID^DCC
VERTRES LOGPIXELSX LOGPIXELSY BITSPIXEL PLANES NUMBRUSHES NUMPENS NUMMARKERS NUMFONTS NUMCOLORS PDEVICESIZE CURVECAPS LINECAPS
ц
2
';•
0x4005 203 mm 265 mm 2400 pixels 3141 pixels 300 dpi 300 dpi 24 bits 1 planes
;.: /; '' ; • \
•
—1 .
-1
5000
0 0 1000
0 0x1 ff
-e ,.
QK
|
i >r* ,
,'
Рис. 17.5. Информация о контексте устройства принтера, возвращаемая функцией GetDeviceCaps
Базовая печать средствами GDI
975
Последовательность формирования заданий печати После настройки контекста устройства принтера можно переходить к построению задания печати средствами GDI. В GDI предусмотрены специальные функции для логической группировки команд GDI при построении заданий печати. typedef struct { int cbSize; LPCTSTR IpszDocName; LPCTSTR IpszOutput: LPCTSTR IpszDataType; DWORD fwType: } DOC INFO: int StartDoc(HDC hDC. CONST DOCINFO * Ipdi); tnt StartPage(HDC hDC); int EndPageCHDC hDC); int EndDocCHOC hDC); HDC ResetDCCHDC hDC, const DEVMODE * IpInitData); int AbortDocCHDC hDC): int SetAbortProc(HDC hdc. ABORTPROC IpAbortProc); Функция StartDoc сообщает GDI о начале нового задания печати. Структура DOCINFO, передаваемая при вызове StartDoc, содержит важнейшие сведения о задании, используемые GDI и спулером. В поле IpszDocName хранится имя документа, выводимое в диспетчере печати. Многие приложения включают в эту строку название приложения в формате <имя_приложения> - <имя_документа>. Поле IpszOutput содержит имя выходного устройства, которое получает данные, сгенерированные драйвером принтера. Изменяя значение этого поля, можно направить результаты вывода в файл вместо физического порта. Поле IpszDatatype содержит тип данных спулинга, рекомендуемый приложением; GDI и драйвер устройства могут игнорировать значение этого поля. Например, для применения особых возможностей Windows 2000 (скажем, печати нескольких страниц на листе) необходим спулинг в формате EMF, поэтому GDI может выбрать EMF-спулинг даже в том случае, если приложение запрашивает спулинг в низкоуровневом формате. Последнее поле содержит некоторые редкие флаги, используемые GDI и драйвером принтера. Флаг DI_APPBANDING сообщает, что разбиение страниц на полосы выполняется приложением; как упоминалось выше, GDI достаточно хорошо справляется с разбиением. Флаг DI_ROPS_READ_DESTINATION указывает, что приложение использует растровую операцию, читающую содержимое приемной поверхности. Растровые принтеры, у которых изображение строится на управляющем компьютере, обычно легко поддерживают любые растровые операции, но у принтеров PostScript поддержка операций, связанных с чтением с приемной поверхности, может вызвать затруднения. В Windows 95 флаг DI_ROPS_READ_DESTINATION фактически исключает спулинг в формате EMF. Функция StartPage начинает новую страницу задания печати, а функция EndPage завершает ее. Механизм работы спулера требует, чтобы весь вывод GDI четко делился на страницы. Графические команды не должны вызываться ни перед первым вызовом StartPage, ни между вызовами EndPage и StartPage. Из-за нетриви-
976
Глава 17. Печать
Базовая печать средствами GDI
977
альных возможностей вывода документов, реализованных в процессоре печати и драйвере принтера, StartPage и EndPage определяют только логические страницы, которые при выводе могут переставляться, разбиваться на листы или наоборот, выводиться по несколько страниц на одном физическом листе. Обычно страница выводится лишь после вызова EndPage. Это объясняется природой механизма спулинга и тем фактом, что команды GDI могут осуществлять вывод в любом месте страницы, поэтому драйвер принтера сначала получает все команды вывода для страницы и лишь потом выбирает, как действовать дальше. На этот процесс может влиять настройка спулера и особых возможностей вывода. Например, у спулера есть режим, при выборе которого печать начинается лишь после постановки в очередь последней страницы. Последнюю страницу приходится ожидать и при печати в обратном порядке, в режиме печати брошюр или двусторонней печати. При печати нескольких страниц на одном листе приходится ждать, пока в очередь будут поставлены все выводимые страницы. Функция EndDoc завершает задание печати, созданное функцией StartDoc. Вспомните, что при создании контекста устройства принтера обычно передается структура DEVMODE со всеми необходимыми параметрами. Эти параметры можно изменять между страницами функцией ResetDC. Функции ResetDC передается манипулятор контекста устройства и указатель на новую, вероятно, измененную структуру DEVMODE. При помощи этой функции приложение может изменить размер бумаги, ее ориентацию или другие параметры. Например, Microsoft Word позволяет выводить каждую страницу с новым размером, ориентацией и т. д. Функция AbortDoc предназначена для аварийного завершения задания печати и отмены вывода тех частей, которые еще не напечатаны. Функция SetAbortProc назначает функцию косвенного вызова, которая периодически вызывается GDI для проверки того, не следует ли отменить задание печати. Обычно эта функция используется средствами отмены печати в приложениях. В листинге 17.2 приведен несложный, но вполне реальный пример, демонстрирующий процесс формирования заданий печати в GDI.
if ( hDC ) { nCall_AbortProc = 0: SetAbortProc (hDC. SimpleAbortProc):
Листинг 17.2. Формирование заданий печати средствами GDI
wsprintf(temp, "AbortProc called Id times". nCall_AbortProc); MessageBoxtNULL. temp. "SimlePrint". MBJDK):
int nCall_AbortProc; BOOL CALLBACK SimpleAbortProc(HDC hOC, int iError) { nCall_AbortProc ++: return TRUE: void SimplePrint(int nPages) { TCHAR temp[MAX_PATH];
DWORD size = MAX_PATH: GetDefaultPrinterCtemp, & size): // Имя принтера по умолчанию HOC hDC - CreateDCCNULL. temp. NULL. NULL): // DC с параметрами по умолчанию
DOCINFO docinfo: docinfo.cbSize docinfo.lpszDocName docinfo.lpszOutput docinfo.lpszDatatype docinfo. fwType
= sizeof(docinfo): - _T("SimplePrint" - NULL; - _T("EMF");
= 0:
if ( StartDoc(hDC. & docinfo) > 0 ) for (int p-0: p
} Функция SimplePrint организует простой цикл постраничной печати. Сначала она запрашивает имя принтера по умолчанию, создает контекст устройства, назначает функцию отмены печати и начинает вывод функцией StartDoc. Если все идет нормально, SimplePrint в цикле последовательно печатает все страницы. Все команды вывода находятся между вызовами StartPage и EndPage. Для каждой страницы функция запрашивает размер печатной области и разрешение, рисует квадрат со стороной 1 дюйм в левом верхнем и правом нижнем углах страницы и выводит номер страницы в правом верхнем углу. Обратите внимание: для контекста устройства принтера точка (0,0) в системе координат устройства (совпадает с точкой (0,0) в логической системе координат в режиме отображения ММ_ТЕХТ) совмещается с первым печатным пиксе-
978
Глава 17. Печать
лом страницы, а не просто с первым пикселом. Следовательно, ее расстояние от левого верхнего угла листа определяется величиной полей, возвращаемых функциями GetDeviceCapsChDC. PHYSICALOFFSETX) и GetDeviceCapsChDC. PHYSICALOFFSETY). Иначе говоря, точное расположение вывода листинга 17.2 зависит от параметров устройства и шрифта, используемого при выводе текста, поскольку функция не выбирает собственный логический шрифт. По результатам, выведенным функцией SlmplePrint, можно оценить размер печатной области и посмотреть, соответствует ли логический дюйм физическому. При выводе нескольких страниц их фактический порядок также учитывает другие параметры печати (например, печать в обратном порядке). В завершение своей работы SlmplePMnt выводит диалоговое окно, в котором приводится количество вызовов функции отмены печати. Возможно, вас удивит тот факт, что иногда эта функция не вызывается вообще, а иногда вызывается только раз на страницу. Впрочем, функция отмены печати постепенно утрачивает свое значение в новых версиях операционных систем из-за спулинга EMF и в приложениях Win32 из-за улучшенной поддержки многозадачности и многопоточности.
Поддержка печати в программах Используя функции GDI и спулера, описанные в двух предыдущих разделах, вы сможете найти принтер, настроить его, начать и завершить задание печати. Весь непосредственный вывод зависит только от ваших навыков обращения с базовыми графическими примитивами GDI. Тема вроде бы исчерпана, и мы можем двигаться дальше. Однако в реальных приложениях Windows с печатью возникает немало про.блем, поскольку нигде толком не описано, как же реализуются сколько-нибудь нетривиальные возможности печати. Возможно, лучшим источником информации является система поддержки печати MFG, реализованная в архитектуре «документ/представление». Задачи, возникающие при поддержке печати в программе, делятся на несколько категорий. Нередко они оказывают влияние и на общую архитектуру программы. В этом разделе мы разработаем несколько классов, реализующих общие возможности печати в профамме, в том числе поддержку единой логической системы • координат, изменения масштаба, разметки печатной страницы на экране, установки полей, вывода многостраничных документов, а также печати нескольких страниц на одном листе. В следующих двух разделах приводятся более завершенные примеры профамм, предназначенных для вывода листингов с выделением синтаксических конструкций и печати фотоизображений.
Единая логическая система координат В приведенных выше программах использовался режим отображения ММ_ТЕХТ, в котором преобразование из логической системы координат в координаты устройства является почти тождественным (не считая возможного смещения). Команды вывода в режиме ММ_ТЕХТ не удается легко использовать для вывода как
Поддержка печати в программах
979
на экран, так и на принтер, поскольку высокое разрешение принтера зависит от устройства и даже от текущих настроек печати. Более правильное решение заключается в выборе логической системы координат с физическими единицами (например, дюймами или миллиметрами). В GDI существует несколько стандартных режимов отображения — MM_LOENGLISH, MM LOMETRIC, MM_TWIPS и т. д. Во многих профессиональных приложениях пользователь выбирает масштаб вывода на экран. Например, Microsoft Word позволяет изменять масштаб от 500 до 10 % от логического разрешения, не говоря уже об изменении ширины, выводе всей страницы вместе с полями и режиме размещения двух страниц. Наконец, в режиме предварительного просмотра (Print Preview) функция вывода масштабирует изображение по размерам окна. Следовательно, стандартные режимы отображения исключаются. Остается единственный вариант — определить собственный режим отображения, используя наиболее общий режим MM_ANISOTROPIC. Ниже перечислены основные требования к такому режиму. О Единая логическая система координат. Количество единиц для представления физической единицы измерения в логической системе координат должно быть фиксированной величиной. Допустим, вы решаете, что один дюйм в логической системе координат всегда представляется 300 единицами независимо от масштаба вывода и устройства. При этом вы получаете один фрагмент графического кода, не содержащий внешних ссылок вида GetDeviceCapsChDC. LOGPIXELS). О Поддержка масштабирования от 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 — такое же логическое разрешение используется в режиме отображения MM_TWIPS. Максимальное разрешение экрана равно 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, создать единую логическую систему координат совсем несложно. fifdef NT_ONLY #define BASE_DPI 9600 #else #define BASE_DPI 1440 #end1f int gcddnt m. int n) { . if ( m==0 ) return n; else return gcd(n % m, m);
void SetupULCS(HDC hDC, int zoom) { SetMapMode(hDC. MM_ANISOTROPIC): int mul = BASE_DPI * 100; int div = GetDeviceCaps(hDC, LOGPIXELSX) * zoom; int fac = gcd(mul. div);
mul /- fac: div /= fac;
}
SetWindowExtExthDC, mul. mul. NULL); SetViewportExtExChDC. div. div. NULL):
Макрос BASE_DPI определяет количество единиц логической системы координат, соответствующих одному дюйму. Обычно оно равно 1440, если только вы не определите макрос NTJDNLY, сообщая тем самым компилятору, что программа предназначена только для систем семейства NT. Функция SetupULCS получает манипулятор контекста устройства и масштаб. Манипулятор может относиться к контексту любого графического устройства.
981
Поддержка печати в программах
Масштаб задается в процентах и изменяется в интервале от 400 до 10. На основании масштаба и логического разрешения контекста устройства функция вычисляет две внутренние переменные, из которых затем исключается наибольший общий делитель. Затем вызываются функции SetWindowExtEx и SetViewportExtEx, определяющие отображение из логической системы координат в систему координат устройства. Обратите внимание: при вызове SetWindowExtEx и горизонтальные, и вертикальные габариты определяются значением BASE_DPI; это гарантирует, что BASE_DPI единиц в логической системе координат всегда соответствуют одному дюйму. В табл. 17.1 приведены примеры отображений из логических координат в координаты устройства, определяемых функцией SetupULCS. Таблица 17.1. Поддержка разных устройств при разных масштабах
Разрешение устройства
Масштаб, % Габариты окна Габариты области просмотра
96 dpi (экран)
500
(3,3)
(1,1)
96 dpi (экран)
100
(15,15)
(1,1)
96 dpi (экран)
10
(150,150)
(1,1)
120 dpi (экран)
50
(24,24)
(1,1)
120 dpi (экран)
10
(120,120)
(1,1)
360 dpi (принтер)
100
(4,4)
(1,1)
600 dpi (принтер)
100
(12,12)
(5,5)
1200 dpi (принтер)
100
(6,6)
(5,5)
Имитация внешнего вида страницы Вывод границ листа на экране относится к числу стандартных приемов, используемых в профессиональных графических пакетах и текстовых редакторах. Границы листа помогают пользователю лучше представить, как же будет выглядеть напечатанный документ. В Microsoft Word этот режим называется разметкой страницы (page layout). Разметка страницы часто требуется и в режиме предварительного просмотра перед печатью. В некоторых приложениях весь пользовательский интерфейс строится именно на разметке страницы. Разметка печатной страницы на экране состоит из нескольких элементов. Во-первых, клиентская область окна обычно закрашивается слегка затемненным цветом. Страницы рисуются белыми, с черной рамкой и простейшей имитацией тени. Небольшие промежутки отделяют страницы друг от друга и от границ клиентской области. В режиме предварительного просмотра перед печатью непечатаемые области обычно обозначаются пунктирной линией, чтобы любые нарушения границ были хорошо видны на экране. Некоторые приложения также тем или иным способом обозначают границы полей, установленных пользователем в диалоговом окне параметров страниц.
982
Глава 17. Печать
Ниже приведен пример разметки страницы на экране. Метод DrawPaper относится к классу KSurface, который будет рассматриваться ниже. Переменная m_Paper класса KSurface определяет размеры листа, в переменной m_MinMargin хранятся минимальные размеры полей, а в переменной m_Margin — текущие размеры полей. Значения этих переменных берутся из диалогового окна параметров страниц. Вспомогательная функция DrawFrame вызывается в DrawPaper трижды. В первый раз DrawFrame рисует границу листа с тенью, во второй обозначает минимальные поля, а в третий — текущие поля. Функции рх и ру обеспечивают масштабирование координат. void KSurface::DrawPaper(HDC HOC. 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(OxEO, OxEO. OxEO). true); // Минимальные поля: граница печатной области DrawFrame(hDC, px(m_MinMargin.left. col), py(m_MinMargin.top. row). px(m_Paper.cx - m_MinMargin.right, col). py(m_Paper.cy - m_MinMargin.bottom, row). RGBtOxFO. OxFO. OxFO). RGB(OxFO. OxFO. OxFO). false): // Поля DrawFramethDC, 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). RGBCOxFF. OxFF, OxFF). RGBtOxFF, OxFF, OxFF). false):
Поддержка печати в программах
983
DrawPaper(hDC. rcPaint. col. row): SetupULCSthDC. mjiZoom): OffsetViewportOrgEx(hDC, px(m_Margin.left. col). py(m_Margin.top. row). NULL): UponDrawPage(hDC. rcPaint. GetDrawableWidthO. GetDrawableHeightO. p): RestoreDCXhDC,
-1);
Метод UponDraw (имя было выбрано для предотвращения конфликта с OnDraw) получает общее количество страниц в документе (виртуальная функция GetPageCount) и количество столбцов в таблице (функция GetColumnCount). Затем он в цикле перебирает все страницы, находящиеся в разных строках и столбцах. Для каждой страницы UponDraw сохраняет состояние контекста устройства, вызывает функцию разметки и настраивает единую логическую систему координат. Перед виртуальным методом UponDrawPage, выводящим содержимое текущей страницы, вызывается метод OffsetViewportOrgEx, смещающий начало логической системы координат в позицию, определяемую полями текущей страницы. Функция UponDrawPage получает ширину и высоту печатной области (без учета полей) и номер страницы. Таким образом, ей не приходится беспокоиться о положении страницы на экране. После вывода страницы контекст устройства восстанавливается в прежнем состоянии для вывода следующей страницы.
Печать нескольких страниц на одном листе Одновременный вывод страниц В общем случае документ состоит из нескольких страниц. Иногда при достаточно малом масштабе все страницы удается одновременно разместить на экране, что помогает пользователю получить представление о документе в целом. В таких приложениях основное внимание следует уделять логике размещения уменьшенных страниц на экране, чтобы ее не приходилось реализовывать снова и снова. Ниже приведена функция UponDraw, управляющая одновременным выводом для класса KSurface. // Вывод страниц в несколько столбцов с обозначением границ листов. // масштабированием и прокруткой void KSurface::UponDraw(HOC hDC, const RECT * rcPaint)
{
int nPage - GetPageCountO: int nCol - GetColumnCountO; int nRow - (nPage + nCol - 1) / nCol: for (int p=0: p
Проблема одновременного вывода нескольких логических страниц уже рассматривалась в предыдущем разделе. Основная трудность заключается в том, чтобы использовать при печати тот же виртуальный метод UponDrawPage. В сущности, задача сводится к правильной настройке логической системы координат. Ниже приведен пример реализации для класса KSurface. // Одновременная печать, масштаб 100 % bool KSurface::UponPrint(HDC hDC. const TCHAR * pDocName) { int scale - GetDeviceCapsthDC. LOGPIXELSX): SetMapMode(hDC. MM_ANISOTROP1C): SetWindowExtEx(hDC. BASE_DPI. BASE_DPI. NULL): SetViewportExtEx(hDC. scale, scale. NULL); OffsetViewportOrgExthDC. (m_Margin.left - m_MinMargin.left) * scale / BASE_DPI, (m_Margin.top - m_MinMargin.top ) * scale / BASE_DPI. NULL); DOCINFO docinfo: docinfo.cbSize docinfo.lpszDocName docinfo.lpszOutput docinfo.lpszDatatype
= sizeof(docinfo): = pDocName: - NULL; - _T("EMF"):
984
Глава 17. Печать
docinfo.fwType
= 0:
if ( StartDoc(hDC, & docinfo) <= 0) return false: int nFrom = 0; int nTo = GetPageCountO: for (int pageno=nFrom: pageno
UponDrawPage(hDC. NULL. GetDrawableWidthO. GetDrawableHeightO, pageno); EndPage(hDC): EndDoc(hDC):
985
Поддержка печати в программах
class KSurface { public: typedef enum { BASE_DPI = ONEINCH. MARGINJ = 16. MARGINJ = 16 KOutputSetup mJMputSetup; int m_nSurfaceWidth: // Ширина поверхности в пикселах int mjiSurfaceHeight: // Высота поверхности в пикселах SIZE m_Paper; RECT m_Margin: RECT m_MinMargin:
// в BASE_DPI // в BASE_DPI // в BASE_DPI
int mjiZoom: int m nScale:
// 100 для масштаба 1:1 // GetDeviceCaps(hDC. LOGPIXELSX) * zoom / 100
int px(int x. int col)
// Из base_dpi в экранное разрешение
{
return ( x + m_Paper.cx * col ) * mjiScale / BASE_DPI + MARGINJ * (col+1):
return true; }
Логическая система координат для печати настраивается проще, поскольку вывод всегда осуществляется в масштабе 100 %. На принтере не нужно имитировать разметку страницы, но для использования той же функции UponDrawPage нам придется переместить начало логической системы координат в точку, определяемую размерами левого и верхнего полей страницы. Задача решается вызовом OffsetViewportOrgExt. Обратите внимание: смещение определяется только разно. стью между размерами полей и непечатаемой области, поскольку в системе координат устройства начало отсчета устанавливается в первую точку печатаемой области.
Родовой класс печати Пора представить родовой класс KSurface, предназначенный для решения общих задач печати средствами GDI. В листинге 17.3 приведено объявление класса и те части реализации, которые не приводились выше. Листинг 17.3. Класс KSurface: одновременный вывод и печать нескольких страниц // 1440 = НОКС72. 96, 120. 360) Удобно при ограничениях // в 22.75 дюйма в Win95/98 // 7200 = НОК(72. 96, 120. 360. 600) Идеально подходит для большинства // устройств вывода fifdef NT_ONLY typedef enum { ONEINCH = 7200 }: #else
typedef enum { ONEINCH - 1440 }; fendif
int py(int y. int row)
{
// Из base_dpi в экранное разрешение
return ( у + m Paper.су * row ) * mjiScale / BASE_DPI + MARGINJ * (row+1):
} public: int GetDrawableWidth(void) { return m_Paper.cx - mjiargin.left - mjiargin.right:
} int GetDrawableHeight(void) { return m_Paper.cy - mjiargin.top - mjiargin.bottom:
} virtual int GetColumnCount(void) { return 1: virtual int GetPageCount(void) { return 1: // Одна страница }
virtual const TCHAR * GetDocumentName(void) Продолжение
986
Глава 17. Печать
Листинг 17.3. Продолжение return virtual virtual virtual virtual
Поддержка печати в программах
bool KSurface::UponSetZoom(int zoom) { if ( zoom — mjnZoom ) return false;
TC'KSurface - Document"):
void DrawPaper(HDC hDC, const RECT void CalculateSize(void): void SetPaper(void); void RefreshPaper(void);
rcPaint, int col. int row);
m_nZoom = zoom: HDC hDC = GetDC(NULL): mjiScale = zoom * GetDeviceCapsthDC, LOGPIXELSX) / 100: DeleteDC(hDC):
virtual void UponDrawPage(HDC hDC. const RECT * rcPaint. int width, int height, int pageno=l): virtual bool UponSetZoom(int zoom): virtual void UponInitialize(HWND hWnd): virtual void UponDraw(HDC hDC, const RECT * rcPaint): virtual bool UponPrintCHDC hDC, const TCHAR * pDocName): virtual bool UponFilePrint(void): virtual bool UponFilePageSetup(void): // Перейти от 1/1000 дюйма к BASE_DPI
CalculateSizeO: return true: void KSurface::RefreshPaper(void) int zoom = mjiZooifl; mjiZoom = 0; SetPaperO: UponSetZoom(zoom): void KSurface::UponInitialize(HWND hWnd)
void KSurface::SetPaper(void) { POINT paper: RECT margin: RECT minmargin;
mJMputSetup.SetDefault(hWnd, 1, GetPageCountO): m_nZoom - 100: RefreshPaperO;
mJMputSetup. GetPaperSi ze( paper): mJMputSetup. GetMargi n (ma rgi n); mJMputSetup. GetMi nMargi n (mi nmargi n); m_Paper.cx m_Paper.cy
paper,x paper,у
m_Margin.left m_Margin.right m_Margin.top m_Margin.bottom-
margin.left margin.right margin.top margin.bottom
m_MinMargin.left m_MinMargin.right = m_MinMargin.top = m_MinMargin.bottom=
void KSurface::UponDrawPage(HDC hDC, const RECT * rcPaint. int width, int height, int pageno) for (int h-0: h<»(height-BASE_DPI): h += BASEJDPI) for (int w-0: w<=(width-BASE_DPI); w += BASEJDPI) Rectangle(hDC. w. h. w+BASEJDPI, h+BASEJDPI):
BASEJDPI / 1000: BASE DPI / 1000: * BASE_DPI * BASE_DP1 * BASE_DPI * BASE_OPI
minmargin.left minmargin.right minmargin.top minmargin.bottom
* * * *
/ / / /
1000: 1000: 1000: 1000:
BASE_DPI BASE_DPI BASE_DPI BASE DPI
/ / / /
bool KSurface::UponFilePrint(void) int rslt = mJMputSetup.PrintDialog(PD_RETURNDC | PDJELECTION):
1000: 1000: 1000: 1000:
// Вычислить общий размер поверхности для вывода nPage страниц в nCol столбцов void KSurface::CalculateSize(void) { int nPage = GetPageCountO: int nCol - GetColumnCountO: int nRow = (nPage + nCol - 1) / nCol: mjnSurfaceWidth = px(m_Paper.cx, 0) * nCol + MARGIN_X: m_nSurfaceHeight = py(m_Paper.cy. 0) * nRow + MARGIN_Y:
if ( rslt—IDOK )
UponPrint(m_OutputSetup.GetPrinterDC(). GetDocumentNameO); mJMputSetup.DeletePri nterDCt): return false;
} bool KSurface::UponFilePageSetup(void) if ( mJDutputSetup.PageSetup(PSD_MARGINS) Г RefreshPaperO: return true: return false;
987
988
Глава 17. Печать
Класс KSurface решает общую задачу одновременного вывода и печати нескольких страниц с поддержкой разных масштабов. Он настолько универсален, что даже не ассоциируется ни с каким окном — для работы с ним достаточно передать манипулятор контекста устройства. Следовательно, этот класс может использоваться для окон SDI и MDI, для диалоговых окон и страниц свойств и даже в элементах ActiveX, EMF или совместимых контекстах устройств. Переменная mJ)utputSetup является экземпляром класса KOutputSetup, управляющего настройкой печати и параметров страниц. Переменные m_nSurfaceWidth и ffl_nSurfaceHeight определяют ширину и высоту поверхности вывода в пикселах и могут использоваться для организации прокрутки. Код вывода полностью поддерживает возможность прокрутки. Значения следующих трех переменных берутся из диалогового окна параметров страниц и преобразуются в единую логическую систему координат методом SetPaper. Переменные m_nZoom и m_nScale определяют масштаб вывода на экран и используются в подставляемых (in-line) функциях преобразования рх и ру. Смысл виртуальных и обычных методов класса вполне очевиден. Переопределение метода UponDrawPage играет ключевую роль при выводе. По умолчанию этот метод рисует на странице квадраты со стороной в один дюйм. Метод UponSetZoom связывается с командой меню или кнопкой панели инструментов и управляет масштабом вывода. Метод Uponlnitialize инициализирует переменные класса. Метод UponDraw связывается с обработчиком сообщения WM PAINT, если класс используется для вывода в окне. Метод UponFilePrint связывается с командой печати в меню. Наконец, метод UponFilePageSetup связывается с командой меню, обеспечивающей настройку параметров страниц.
Рис. 17.6. Пример использования классов KSurface и KPageCanvas
Вывод в контексте устройства принтера
989
На прилагаемом компакт-диске приведен класс KPageCanvas, производный от классов KScrollCanvas (поддержка прокрутки в дочерних окнах MDI) и KSurface (одновременный вывод и печать нескольких страниц). На рис. 17.6 показан результат вызова стандартной функции UponDrawPage. Функция рисует квадраты со стороной 1 дюйм на листе размером 4 x 6 дюймов в альбомной ориентации, с полями размером 0,5 дюйма и в масштабе 75 %.
Вывод в контексте устройства принтера Интерфейс GDI проектировался как аппаратно-независимый интерфейс, поэтому вывод в экранном контексте устройства не должен сильно отличаться от вывода в контексте принтера. И все же при работе с контекстом устройства принтера приходится учитывать ряд обстоятельств, особенно если результаты печати отличаются от желаемых. Некоторые проблемы связаны не столько с контекстом устройства принтера, сколько с методикой разработки графических алгоритмов, сохраняющих все пропорции при разных настройках логической системы координат.
Единицы измерения Если в вашей программе вывод на экран и на принтер должен выполняться одним фрагментом кода, то от режима отображения ММ_ТЕХТ и системы координат устройства вам придется перейти на логическую систему координат. Однозначное соответствие между единицами логической системы координат и системы координат устройства при этом теряется. Например, класс KSurface из предыдущего раздела использует логическую систему координат с разрешением 1440 dpi как для печати, так и для вывода на экран. У большинства графических устройств разрешение не достигает 1440 dpi, поэтому один пиксел поверхности устройства обычно соответствует нескольким логическим единицам. Впрочем, в ближайшем будущем появятся принтеры с разрешением 2400 dpi. На таких устройствах одна логическая единица будет соответствовать нескольким пикселам поверхности устройства. Итак, при программировании аппаратно-независимого графического вывода следует обратить внимание на следующие обстоятельства, относящиеся к логической системе координат. О Если толщина пера превышает один пиксел, она задается в логической системе координат. При написании универсального графического кода толщину пера приходится задавать в реальных единицах, а не в пикселах. Например, при работе с классом KSurface, представляющим один дюйм 1440 единицами, при определении пера толщиной один пункт будет указываться толщина 20. О При преобразовании координат из логической системы в систему координат устройства следует использовать функции LPtoDP и DPtoLP GDI, поскольку рассчитывать на постоянную связь между этими системами координат уже не приходится.
990
Глава 17. Печать
О Некоторые графические алгоритмы при работе с большим изображением используют приращение для перехода к следующей координате. Например, закраска области может осуществляться выводом серии линий, расположенных вплотную друг к другу. Проанализируйте такие алгоритмы и проверьте, не возникают ли при выводе пропуски, перекрытия или иные нарушения. О Функция BitBlt, часто используемая при выводе растров, хорошо работает лишь при выводе на экран или в режиме отображения ММ_ТЕХТ. В универсальном графическом коде вызовы BitBlt следует заменить более общей функци'eftStretchBlt. О Размеры узоров в штриховых кистях 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::SyntaxHighl1ght(HDC hDC. int x. int y. const TCHAR * mess) { BYTE
flag[MAX_PATH]:
int len = _tcslen(mess); assertClen < MAX_PATH-50): memset(flag. 0. MAX_PATH); Co1orText(mess. flag): float width - 0 . 0 ; for (int k=0; k
(flag[k]==flag[k+next]) )
SetTextColor(hDC. crText[flag[k]]): m_formator.TextOut(hOC, (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. О, width, height }: LPtoOP(hDC. (POINT *) & rect, 2): if ( ! IntersectRecttS rect. rcPaint. & rect) ) return: HGDIOBJ hOld = SelectObjecUhDC. mJiFont): SetBkMode(hDC. TRANSPARENT): Set Text Align (hDC. TA_LEFT TAJOP); KGetline parser(m_pBuffer. m_nSize): 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() * 1 + 0.5). parser.m_line):
Растры
} else break: SelectObject(hDC,
Аппаратно-зависимые растры и DIB-секции всегда ассоциируются с совместимым контекстом устройства. Если совместимый контекст устройства ориентируется на конкретный целевой контекст, аппаратно-зависимые растры, созданные для совместимости с экраном, наверняка окажутся несовместимыми с контекстом устройства принтера. Например, если воспользоваться функцией LoadBItmap для загрузки растрового ресурса в формате DDB и создать совместимый контекст устройства для контекста принтера, в общем случае вам не удастся выбрать растр в совместимом контексте, поскольку он может оказаться несовместимым. Вместо этого совместимый контекст устройства следует создать для экранного контекста. И вообще, использовать при печати аппаратно-зависимые растры не рекомендуется — особенно в видеорежимах с 256 цветами, поскольку принтеры не поддерживают аппаратную палитру. Некоторые приложения разделяют большие растры на маленькие фрагменты, чтобы оптимизировать вывод растра на экран или обойти старое ограничение размеров растра в 64 Кбайт. Это особенно важно в Windows 95/98, поскольку до завершения графического вызова GDI все обращения к GDI от других программных потоков блокируются (во избежание проблем с реентерабельностью 16-разрядного кода GDI). С другой стороны, стратегия деления может вызвать большие проблемы с печатью. Во-первых, как было показано выше, при построении EMF у GDI не хватает сообразительности для исключения из EMF неиспользуемых частей исходного растра, в результате чего сгенерированные EMF-файлы могут иметь громадные размеры. Во-вторых, передача большого количества мелких растров драйверу принтера усложняет их обработку драйвером. В-третьих, при делении растра необходимо позаботиться о том, чтобы между частями растра не оставалось пробелов. Если у вас возникли проблемы с печатью растров, вы можете получить важную диагностическую информацию при помощи утилит просмотра и расшифровки EMF-файлов из предыдущей главы.
hOld):
}
Класс KProgramPageCanvas переопределяет метод KSurface::GetPageCount и реализует более точный способ подсчета страниц, основанный на подсчете строк исходного текста и точной информации о высоте строки. Метод UponOrawPage выводит одну страницу в таблице. Сначала он преобразует прямоугольник страницы из логической системы координат в координаты устройства, чтобы узнать, пересекается ли он с текущим перерисовываемым прямоугольником. Страницы, которые не видны на экране, пропускаются. Затем UponDrawPage при помощи класса KGetline последовательно читает все строки исходного текста, пропускает строки, относящиеся к предыдущим страницам, и выводит только текущую страницу. Вероятно, для повышения быстродействия следовало бы построить индекс. Функция SyntaxHighlight выводит одну строку программы. Она назначает цвета каждому символу в соответствии с лексическими правилами C/C++, обращаясь к лексическому анализатору ColorText, и использует методы класса KTextFormat для точного форматирования выводимого текста. На рис. 17.7 показано окно программы CodePrint, причем в качестве примера выбран ее собственный исходный текст.
I •
Рис. 17.7. CodePrint: форматирование текста, одновременный просмотр и печать нескольких страниц
993
Печать графики в формате 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 StretchJPEG(HDC hDC. int x. int у. int w. int h, void * pJPEGImage, unsigned nJPEGSize. int width, int height) { DWORD esc = CHECKJPEGFORMAT; if ( ExtEscapethDC, QUERYESCSUPPORT, sizeof(esc), (char *) &esc. 0. 0) <=0 ) return FALSE;
Вывод в контекстеустройства принтера
реализовала сжатие и восстановление 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 next_i nput_byte
DWORD rslt = 0; if ( ExtEscape(hDC. CHECKJPEGFORMAT. nJPEGSize. (char *) pJPEGImage. sizeof(rslt). (char *) &rslt) <=0 ) return FALSE:
len: buffer;
void init_source(j_decompress_ptr cinfo)
}
if ( rslt!=l ) return FALSE;
virtual void term_source(j_decompress_ptr cinfo)
BITMAPINFO bmi: meroset(&bmi, 0. sizeofCbmi)): bmi.bmiHeader.biSize bmi.bmiHeader.biWidth bmi.bmiHeader.biHeight bmi.bmiHeader.biplanes bmi .bmiHeader.biBHCount bmi.bmiHeader.biCompression = bmi.bmiHeader.biSizelmage
995
if (cinfo->src) sizeof(BITMAPINFOHEADER): width: - height: // Перевернутое изображение 1: 0; BI_JPEG; nJPEGSize:
return GDIJRROR != StretchDIBitsthDC. x. y, w. h. 0. 0. width, height. pJPEGImage. & bmi. DIB RGB COLORS, SRCCOPY); Функция StretchJPEG вызывает ExtEscape дважды. В первый раз она проверяет, поддерживается ли команда CHECKJPEGFORMAT, а во второй — приемлем ли формат JPEG, который мы собираемся передать. Если обе проверки завершаются успешно, изображение JPEG упаковывается в DIB и передается функции StretchDIBits. Если вызов StretchJPEG завершается неудачей, вызывающая сторона должна самостоятельно расшифровать JPEG и передать расшифрованные данные устройству. Расшифровка JPEG считается очень непростой задачей из-за сложности алгоритма сжатия JPEG. С другой стороны, группа IJG (Independent JPEG Group)
delete (const_source_mgr *) cinfo->src; cinfo->src - NULL:
GLOBAL(void) jpeg_const_src Cj_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. Печать
Вывод в контексте устройства принтера
BYTE * addr = m_pBits + bps * h: dinfo.jpeg_read_scanlines(& addr, 1);
Листинг 17.4. Класс KPicture: шифрование/расшифровка изображений в формате JPEG
}
class KPicture { void Release(void):
dinfo.jpeg_finish_decompress(): di nfо.j peg_destroy_decompress ():
int AllocateCint width, int height, int channels, bool bBits=true):
m_pJPEG - (BYTE *) jpegimage: mjiJPEGSize = jpegsize:
public: BITMAPINFO * m_pBMI: BYTE * m_pBits; BYTE int
}
catch ( . . . ) {
m_pJPEG; m nJPEGSize:
return FALSE; } return TRUE:
KPictureO; -KPictureO: int GetWidth(void) const { return m_pBMI->bmiHeader.biWidth; } int GetHeight(void) const { return m_pBMI->bmiHeader.biHeight; BOOL OecodeJPEG(const 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::DecodeJPEG(const 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 nfо.j peg_read_header(TRUE): di nfо.j peg_sta rt_decompress(): intbps = Allocate(dinfo.image_width. dinfo.imagejieight. di nfо.out_color_components. 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) return: int sw int sh
- m_pPicture->GetWidth(): = m_pPicture->GetHeight():
int dpix = sw * ONEINCH / width: int dpiy - sh * ONEINCH / height: int dpi = max(dpix. dpiy);
if ( m_pBits==NULL ) return FALSE: for (int h=dinfo.imagejieight-1: h>=0: h--) {
997
// Перевернутое // изображение
int dispwidth =• sw * ONEINCH / dpi; int dispheight = sh * ONEINCH / dpi: SetStretchBltMode(hDC. STRETCH DELETESCANS):
998
Глава 17. Печать
int x - ( width- dispwidth)/2: int у - (height-dispheight)/2; if ( StretchJPEGChDC, 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 была задумана как простейшее средство для печати фотографий, поэтому метод KlmageCanvas:: UponDrawPage пытается с максимальной эффективностью использовать дорогую поверхность носителя. Он вычисляет максимальный размер изображения, помещающегося без искажения пропорций на бумаге при текущих размерах полей. Руководствуясь границами листа и обозначениями полей на экране, вы можете легко отрегулировать размеры полей или переключиться на другую ориентацию листа. Сначала метод вызывает функцию StretchJPEG и пытается вывести небольшое изображение в формате JPEG. Если попытка окажется неудачной, приходится передавать большое изображение в формате BMP. Кстати, существует как минимум один драйвер принтера, принимающий изображения в формате JPEG — это драйвер HP 8500 Color PostScript. При выводе в файл вы увидите, что изображение JPEG кодируется в файле PostScript в двоичные данные; это приводит к значительному уменьшению размеров файла.
Итоги
нет еще выше. Следующая глава посвящена одному из направлений развития GDI — программированию для DirectDraw.
Дополнительная информация Если вы захотите еще больше узнать о печати и спулере, обратитесь к Microsoft DDK. Вы найдете очень подробное описание интерфейса DDI, архитектуры спулера и приемов написания мини-драйверов в архитектуре UniDriver. К DDK также прилагаются исходные тексты драйвера и мини-драйвера, процессора печати EMF и монитора порта. В старые версии Windows NT DDK даже входил полный исходный текст драйвера принтера PostScript. Некоторые проблемы с печатью решаются анализом EMF-файлов спулинга. За информацией и утилитами для работы с метафайлами обращайтесь к главе 16.
Примеры программ К главе 17 прилагается несколько примеров программ (табл. 17.2). Таблица 17.2. Программы главы 17 Каталог проекта
Описание
Samples\Chapt_17\PrinterDevice
Программа для получения информации о работе спулера и атрибутов контекста устройства принтера. Создана па основе аналогичной программы для работы с экранными устройствами (см. главу 5)
Samples\Chapt_17\Printer
Тестовая программа для передачи спулеру низкоуровневых данных и EMF. Иллюстрирует работу с диалоговыми окнами печати и параметрами страниц, простейший цикл печати, применение классов KSurface и KPageCanvas, вывод линий и кривых, заливку замкнутых фигур и аппаратно-независимую работу с растрами и текстом
Samples\Chapt_17\CodePrint
Вывод на экран и печать исходных текстов программ с выделением синтаксических элементов в режиме WYSIWYG
Samples\Chapt_17\ImagePrint
Просмотр и печать графики в формате JPEG. Программа позволяет передавать на принтер изображения JPEG. Использует библиотеку JPEG из каталога Samples\include\jlib
Итоги Настоящая глава объединяет многие темы, рассматривавшиеся в книге (контексты устройств, линии и кривые, заливки, растры, шрифты, текст и EMF) применительно к печати. Мы рассмотрели архитектуру спулера, общую последовательность действий при печати, функции API спулера, предназначенные для получения информации и настройки принтеров, функции поддержки печати в GDI, а также — что самое важное — реализацию профессиональных возможностей печати в приложениях средствами Win32 API. В разделе «Поддержка печати в программах» представлен родовой класс KSurface, предназначенный как для вывода на экран, так и для печати. Этот класс обеспечивает полноценное WYSIWYG-представление графических данных в единой логической системе координат. В разделе «Вывод в контексте устройства принтера» приведены два примера нетривиальных программ, использующих классы KSurface и KPageCanvas для решения вполне реальных задач — печати исходных текстов программ и графики в формате JPEG. На этом завершается наше знакомство с традиционным графическим программированием для Windows. Впрочем, GDI, как и любая технология, продолжает развиваться. Благодаря аппаратному ускорению в будущем программы начнут работать с еще большим количеством цветов, а их быстродействие ста-
999
Технология COM
1001
центов GDI. Другими словами GDI+ это будет GDI + DirectDraw + DirectSD + что-нибудь еще. Как видите, у нас есть все причины, чтобы поскорее заняться DirectDraw и DirectSD. DirectDraw — относительно сложный интерфейс API двумерной графики, для сколько-нибудь приличного описания которого понадобится не менее 200 страниц. Впрочем, непосредственный режим (Immediate Mode) DirectSD настолько сложен, что вам придется прочитать немало книг по компьютерной графике хотя бы для того, чтобы в нем разобраться, не говоря уже об эффективном применении. Эту короткую главу можно рассматривать лишь как краткое введение в DirectDraw и DirectSD. Основное внимание уделяется следующим темам:
Глава 18 DirectDraw и непосредственный режим DirectsD Интерфейс GDI в течение долгого времени считался основным интерфейсом API графического программирования для Windows. Впрочем, в мире персональных компьютеров произошло так много изменений, что и в GDI пришло время фундаментальных усовершенствований (хотя мы знаем, что GDI постепенно усовершенствуется в каждой новой версии Windows). Будущей версии GDI присвоено кодовое название GDI+; это будет GDI нового поколения от Microsoft. Согласно опубликованной документации Microsoft (www.microsoft.com/hwdev/video/~GDInext.htm), GDI+ создаст инфраструктуру для нововведений в области пользовательского интерфейса. В частности, GDI+ обеспечит простую интеграцию двумерной и трехмерной графики, упростит обработку оцифрованных изображений и установит новые стандарты в области качества графики и быстродействия настольных систем. GDI+ будет поддерживать нетривиальные графические возможности — альфа-наложение, размытие, прозрачные окна, второй буфер, гамма-коррекцию, трехмерный пользовательский интерфейс и т. д. Возникает впечатление, что интерфейс GDI+ в первую очередь направлен на интеграцию традиционного интерфейса GDI с новыми интерфейсами API от Microsoft, предназначенными для программирования игр (DirectDraw и DirectSD). Интеграция плоской и трехмерной графики начнется на уровне API и будет распространяться до уровня DDI (интерфейс драйверов устройств). На уровне DDI GDI+ полное аппаратное ускорение будет обеспечиваться комбинациями двумерных и трехмерных команд. В GDI+ будут определены новые команды для примитивов, не поддерживаемых существующими командами DirectDraw и DirectSD. Говорят, что DirectDraw и DirectSD уже не будут ограничиваться программированием игровых и учебных приложений, а войдут в число базовых компо-
О знакомство с базовыми концепциями, интерфейсами и методами для программистов GDI; О разработка классов C++, упрощающих программирование для DirectDraw и DirectSD; О возможности применения DirectDraw и DirectSD в «традиционном» программировании для Windows.
Технология СОМ Подсистема GDI в Win32 API содержит примерно 500 функций, образующих довольно сложную иерархию без четкой группировки. При проектировании DirectX компания Microsoft позаимствовала модель интерфейса между приложениями и операционной системой из технологии СОМ. Понимание базовых принципов СОМ абсолютно необходимо для написания правильно работающих программ DirectX.
СОМ-интерфейсы В технологии СОМ семантически связанные абстрактные методы группируются в абстрактных базовых классах, которые в терминологии СОМ называются интерфейсами. СОМ-интерфейс, как и абстрактный базовый класс, только определяет прототипы всех методов интерфейса на синтаксическом уровне и задает порядок этих методов. Для определения семантики этих методов вместо формального языка, подходящего для машинной проверки, используется запись, более или менее напоминающая естественный язык — просто потому, что на роль такого формального языка не нашлось подходящего кандидата. Все СОМ-интерфейсы являются производными от общего корневого интерфейса IDnknown, который определяется следующим образом: class lUnknown {
public: virtual HRESULT _stdca11 QueryInterface(REFIID riid. void ** ppvObject) = 0:
1002
Глава 18. DirectDraw и непосредственный режим DirectSD virtual ULONG _stdcall AddRef(void) = 0: virtual ULONG _stdcall ReleaseCvoid) • 0;
С каждым СОМ-интерфейсом связывается 128-разрядный идентификатор который обычно содержит гораздо больше информации, чем ISBN книги, номер машины или водительского удостоверения. Идентификаторы интерфейсов должны быть глобально-уникальными, поэтому они обычно называются GUID (Global Unique ID, глобально-уникальный идентификатор). Например, GUID интерфейса JUknown называется IID_IUnknown и определяется следующим образом: DEFINE_GUID(IID_IUnknown. 0x00000000. 0x0000. 0x0000. ОхСО. 0x00. 0x00. 0x00. 0x00, 0x00. 0x00. 0x46);
СОМ-классы СОМ-интерфейс - всего лишь абстрактная спецификация. Чтобы интерфейс приносил практическую пользу, он должен быть воплощен в СОМ-классе. СОМкласс, реализующий некоторый СОМ-интерфейс, должен определяться как производный от него и реализовывать все методы этого интерфейса. Ниже приведен пример реализации интерфейса lUnknown: class KUnknown : p u b l i c lUnknown ULONG m_nRef: public: KUnknown() { m nRef = 0; ULONG AddRef(void) { return ++ m nRef:
ULONG Release(void) { if ( -- m_nRef==0) {
// Счетчик ссылок
// В начальном состоянии счетчик ссылок равен О
// AddRef увеличивает счетчик ссылок
// Release уменьшает счетчик ссылок // Если счетчик ссылок достиг О
delete this; return 0:
} return m nRef: HRESULT QueryInterface(REFIID id, void * * ppvObj) if ( iid -= IIDJUnknown) // Поддерживается только lUnknown * ppvObj = this: AddRefO; return S OK:
// Вернуть указатель на текущий обьект // Увеличить счетчик ссылок
Технология СОМ
1003
return E NOINTERFACE;
Создание, применение и удаление СОМ-объектов обычно зависит от счетчика ссылок. Единственным исключением является единичный СОМ-объект, который создается в виде глобальной переменной и поэтому не нуждается в удалении. Следовательно, СОМ-объект обычно содержит хотя бы одну переменную — счетчик ссылок. При создании СОМ-объекта счетчик ссылок инициализируется нулевым значением. Метод AddRef увеличивает счетчик ссылок; этот метод должен вызываться при создании нового указателя на СОМ-объект. Метод Release уменьшает счетчик ссылок и вызывается в том случае, когда указатель на СОМ-объект перестает использоваться. Если счетчик ссылок упал до 0, соответствующий СОМ-объект (кроме единичных объектов) можно удалить. Класс KUnknown предполагает, что его экземпляры создаются в куче оператором new, поэтому они должны удаляться оператором delete. Соответствие между вызовами AddRef и Release чрезвычайно важно для работы программ СОМ. Лишний вызов AddRef не позволит удалить неиспользуемый СОМ-объект, что вызовет утечку памяти/ресурсов. Лишний вызов Release приведет к преждевременному удалению СОМ-объекта, и вероятнее всего — к ошибкам защиты. СОМ-объект должен предоставить реализацию для всех СОМ-интерфейсов, от которых он является производным. Следовательно, он должен реализовывать как минимум интерфейс lUnknown; вероятно, наряду с lUnknown будут реализованы и другие интерфейсы. Метод Querylnterface позволяет клиенту СОМ-класса узнать, поддерживается ли тот или иной интерфейс. Querylnterface получает ссылку на GUID, возвращает указатель на СОМ-объект, преобразованный к типу конкретного СОМ-интерфейса, а также признак успеха или неудачи. В классе KUnknown, реализующем единственный интерфейс lUnknown, метод Querylnterface проверяет, равен ли переданный идентификатор GUID идентификатору IID_ lUnknown. Если идентификаторы совпадают, метод возвращает указатель this, увеличивает счетчик ссылок и возвращает код S_OK; в противном случае возвращается код ошибки E_NOINTERFACE. Указатель, возвращаемый функцией Querylnterface, называется указателем на объект интерфейса, или просто интерфейсным указателем. Строго говоря, интерфейсный указатель не является указателем на СОМ-интерфейс, поскольку интерфейс — всего лишь спецификация, «условность», а не реально существующий объект. Интерфейсный указатель указывает на адрес СОМ-объекта; первое двойное слово по этому адресу содержит указатель на таблицу указателей на реализации виртуальных методов, определенных в СОМ-интерфейсе. Проще говоря, интерфейсный указатель ссылается на другой указатель, который, в свою очередь, ссылается на массив реализаций виртуальных методов. В нашем простом примере с одним интерфейсом интерфейсный указатель совпадает с указателем на объект. С каждым СОМ-классом также связывается однозначно идентифицирующий его идентификатор GUID. Идентификаторы GUID СОМ-классов обычно относятся к отдельному типу CLSID, формат которого в точности совпадает с форматом GUID.
1004
Глава 18. DirectDraw и непосредственный режим Direct3D
Создание СОМ-объекта Итак, у нас имеется СОМ-интерфейс и СОМ-класс. Как воспользоваться ими в других компонентах? Преимущества технологии СОМ главным образом обусловлены четким отделением интерфейсов от реализации, из чего следует, что клиентские компоненты СОМ-класса не видят объявления этого класса. Если объявление класса недоступно, вы не сможете создать новый экземпляр класса оператором new, удалить объект оператором delete или создать СОМ-объект в стеке. Чтобы клиентские компоненты могли создавать объекты, в СОМ определяется обобщенный СОМ-интерфейс ICIassFactory, отвечающий за создание СОМобъектов. Createlnstance, главный метод ICIassFactory, получает GUID интерфейса, создает новый СОМ-объект и возвращает интерфейсный указатель. К СОМклассам обычно прилагается специальный класс (называемый фабрикой класса), предназначенный исключительно для создания экземпляров формального класса. СОМ-классы обычно реализуются в виде библиотеки DLL, главная экспортируемая функция которой DllGetClassObject определяется следующим образом: STDAPI DllGetClassObjectCREFCLSID rclsid. REFIID riid. LPVOID * ppv);
Функция DllGetClassObject DLL COM проверяет GUID всех классов текущей библиотеки DLL. Обнаружив совпадение, она находит нужную фабрику класса и возвращает указатель на объект фабрики класса, который может использоваться для создания одного или нескольких экземпляров СОМ-класса. Операционная система должна регистрировать новые библиотеки DLL COM, чтобы точно знать, где они находятся и какие СОМ-классы реализуют. Общий способ создания СОМ-объектов заключается в использовании функции CoCreateInstance 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 соответствует отрицательной величине. Методы СОМ обычно возвращают SJ3K (0) в случае успешного завер-
Технология СОМ
1005
шения, однако сравнивать HRESULT с SJ3K не рекомендуется. Методы DirectDraw обычно возвращают признак успешного завершения DD_OK (0). Код подсистемы, как правило, заносится в HRESULT лишь в случае неудачного вызова, чтобы программа могла в какой-то степени локализовать ошибку. В DirectX используются коды подсистемы 0x876 и 0x878. Ниже показано, как формируется значение HRESULT для ошибок DirectDraw/DirectSD. fdefine JACDD 0x876 fdefine MAKEJ)DHRESULT(code) MAKE_HRESULT(1. JACDD. code)
Например, при создании поверхности DirectDraw с недопустимым форматом пикселов (код DDERRJNVALIDPIXELFORMAT) HRESULT = MAKE_DDHRESULT(145).
В DirectX определено свыше 200 кодов ошибок HRESULT, поскольку очень важно, чтобы в случае ошибки приложение смогло обнаружить ее возможные причины.
DirectX и СОМ В DirectX используются десятки интерфейсов и классов СОМ, однако каноны модели СОМ соблюдаются не в полной мере. Самое заметное нарушение заключается в том, что СОМ-объекты DirectX либо создаются специальной экспортируемой функций, либо строятся на базе существующих СОМ-объектов, не используя родовую функцию CoCreatelnstance. Центральное место в DirectDraw и в непосредственном режиме DirectSD занимает серия интерфейсов I DirectDraw. Опубликованный (то есть формально документированный, распространяемый и используемый) СОМ-интерфейс изменить уже нельзя. Чтобы включить в него новые функциональные возможности, приходится создавать новый интерфейс. Например, после исходного интерфейса IDirectDraw появились интерфейсы IDirectDraw2, IDirectDraw4 и последний интерфейс IDirectDraw?, используемый в DirectX 7.O. DirectDraw экспортирует специальную функцию для создания объекта DirectDraw с поддержкой интерфейса IDirectDraw?: HRESULT WINAPI DirectDrawCreateEx(GUID * IpGUID. LPVOID *-lplpDD. REFIID iid. lUnknown * pUnkOuter); В первом параметре передается указатель на GUID, определяющий графическое устройство с поддержкой DirectDraw/DirectSD на уровне аппаратной реализации, аппаратной реализации на втором мониторе или программной эмуляции. Если передается NULL, используется активное устройство вывода. Константа DDCREATE_EMULATEONLY выбирает программную эмуляцию, a DDCREATE_HARDWAREONLY реализацию с аппаратным ускорением. Данная возможность особенно удобна при тестировании программы и диагностике проблем, встретившихся в другой реализации. Перечисление текущих реализаций DirectDraw/DirectSD в системе осуществляется функцией DirectDrawEnumerateEx. При помощи этой функции ваша программа может найти реализацию, отвечающую ее требованиям. Во втором параметре передается указатель на переменную, в которой сохраняется интерфейсный указатель при успешном создании объекта DirectDraw функцией Di rectDrawCreateEx. Третий параметр может быть равен только IID_ IDirectDraw7 — GUID интерфейса IDirectDraw7. Последний параметр зарезерви-
1006
Глава 18. DirectDraw и непосредственный режим DirectSD
1007
Общие сведения о DirectDraw
рован для обеспечения совместимости с механизмом агрегирования СОМ и в настоящее время должен быть равен NULL. Ниже приведен пример инициализации среды DirectDraw функцией DirectDrawCreateEx. void Demo_DirectDrawCreateEx(KI_ogWindow * pLog) {
(unknown
IDirectDraw? IDirectDraw4
IDirectDraw7 * pDD - NULL;
IDirectDraw2
HRESULT hr = DirectDrawCreateEx(NULL, (void **) S pDO, IID_IDirectDraw7, NULL); If ( FAILED(hr) ) { pLog->Log("DirectDrawCreateEx failed Ux)", hr);
IDirectDraw IDDVideoPortContanter
Объект DirectDraw
IDirectDrawKernel IDirect3D7
return;
CheckInterface(pLog. pDD. IID_IDirectDraw7. "IDirectDraw?"); ChecklnterfaceCpLog, ChecklnterfaceCpLog, CheckInterface(pLog. CheckInterface(pLog.
pOD. pDD. pDD, pDD,
IID_IDirectDraw4. IID_IDirectDraw2. IID_IDirectOraw. IIO_IUnknown,
"IDirectDraw4"): "IDirectDraw2"); "IDirectDraw" ); "lUnknown" );
ChecklnterfaceCpLog, pDD, IID_IDDVideoPortContainer,
"IDOVideoPortContainer" ); ChecklnterfaceCpLog. pDD. IID_IDirectDrawKernel.
"IIDJDirectDrawKernel" );
ChecklnterfaceCpLog. pDD. 11D_IDirectSD?, ChecklnterfaceCpLog. pDD. IID_IDirect3D3.
"IDirect3D7"); "IDirect3D3");
pDD->Release():
Функция Demo_Di rectDrawCreateEx обращается к системе с требованием создать объект DirectDraw и вернуть интерфейсный указатель IDirectDraw?. Если в системе установлена библиотека DirectX 7.0, функция проверяет поддержку других СОМ-интерфейсов при помощи функции Checklnterface. Функция Checklinterface использует Queryl interface для получения нового интерфейсного указателя, выводит данные в служебном окне и освобождает указатель. Наконец, Demo_DirectDrawCreateEx освобождает объект DirectDraw методом Release. Эксперимент показывает, что объект DirectDraw, созданный функцией DirectDrawCreateEx, поддерживает все перечисленные интерфейсы, кроме IDirect3D3. На рис. 18.1 в традиционном формате диаграмм СОМ изображены СОМ-интерфейсы, поддерживаемые объектом DirectDraw. СОМ-объект с поддержкой всех интерфейсов, показанных на рисунке, имеет очень сложную структуру — особенно при смешанной поддержке интерфейсов DirectDraw и DirectSD. По указателям, возвращаемым функцией Querylnterface, можно заметить, что объект DirectDraw не создается как единое целое. Система достаточно умна, чтобы создавать и инициализировать части объекта по мере необходимости.
Рис. 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, мы не будем вдаваться в подробности использования каждого метода. Вместо этого мы
1008
Общие сведения о DirectDraw
Глава 18. DirectDraw и непосредственный режим DirecGD
GetClientRect(hWnd. & m_rcDest): ClientToScreenChWnd. (POINT*)& ra_rcDest.left): ClientToScreen(hWnd. (POINT*)& m rcDest right):
рассмотрим методы в контексте классов C++ и примерах вывода. Полная информация о любом СОМ-интерфейсе и его методах приведена в MSDN.
}
Интерфейс IDirectDraw?
HRESULT KDirectDraw::SetupDirectDraw(HWND hTop. HWND hWnd. int nBufferCount. bool bFullScreen. int width, int height, int bpp) { HRESULT hr = DirectDrawCreateExtpOriverGUID. (void **) &m_pDD. IIDJDirectDraw/. NULL): if ( FAILED( hr ) ) return hr:
В листинге 18.1 приведен класс KDirectDraw, который представляет собой несложную оболочку для работы с интерфейсом IDirectDraw?. Листинг 18.1. Класс для работы с интерфейсом IDirectDraw #define SAFE_RELEASE(inf) { if (inf) { inf->Release(); inf = NULL: }} // Оболочка для интерфейса IDirectDraw/ с поддержкой первичной поверхности class KDirectDraw { protected: IDirectDraw/ * m_pDD; RECT m_rcDest: // Приемный прямоугольник KDDSurface m_primary;
if ( bFullScreen )
hr - m_pDD->SetCooperativeLevel(hTop,
• I
if ( bFullScreen ) { hr = m_pDD->SetDisplayMode(width, height, bpp. 0. 0): if ( FAILED(hr) ) return hr;
public: KDirectDraw(void): virtual -KDirectDraw(void) { DischargeO: }
SetRect(& mj-cDest. 0. 0. width, height):
} else SetClientRect(hWnd):
void SetClientRect(HWND hWnd):
}:
KDirectDraw::KDirectDraw(void) { m_pDD = NULL; } HRESULT KDirectDraw::Discharge(void) { m_primary.DischargeO: SAFE_RELEASE(m_pDD);
return S_OK:
}
void KDirectDraw::SetClientRect(HWND hWnd)
DDSCLJULLSCREEN | DDSCLJXCLUSIVE); else hr = m_pDD->SetCooperativeLevel(hTop. DDSCL_NORMAL); if ( FAILED(hr) ) return hr;
virtual HRESULT Discharge(void):
virtual HRESULT SetupDirectDraw(HWND hTop. HWND hwnd, int nBufferCount=0. bool bFullScreen = false, int width=0. int height=0, int bpp=0);
1009
hr = m_primary.CreatePrimarySurface(m_pDD. nBufferCount):
r
f
if ( FAILED(hr) ) return hr: if ( ! bFullScreen ) hr = m_primary.SetClipper(m_pDD, hWnd):
return hr; } В классе KDi rectDraw определяются три переменные: интерфейсный указатель m_pDD на IDirectDraw?, приемный прямоугольник m_rcDest и первичная поверхность вывода m_primary. Поверхность представлена классом KDDSurface, о котором речь пойдет ниже. Конструктор присваивает m_pDD указатель NULL, метод Discharge освобождает выделенные ресурсы (этот метод вызывается в деструкторе). Метод SetupDi rectDraw создает объект DirectDraw и осуществляет подготовку к выводу средствами DirectDraw. Метод получает семь параметров: манипулятор окна верхнего уровня, манипулятор дочернего окна, использующего DirectDraw, количество резервных буферов, флаг полноэкранного режима и три целых
1010
Глава 18. DirectDraw и непосредственный режим Direct3D
числа, определяющих формат экрана в полноэкранном режиме. Метод SetupDirectDraw создает объект DirectDraw вызовом функции DirectDrawCreateEx, возвращающей интерфейсный указатель на IDirectDraw? в переменной m_pDD. Если выполнение функции прошло успешно, вызывается метод IDirectDraw?: :SetCooperativeLevel, который передает манипулятору главного окна информацию о том, какой нужен режим — полноэкранный или оконный. Полноэкранные программы DirectX обычно относятся к категории игровых или обучающих. Как правило, такие программы присваивают монопольные права на распоряжение всеми ресурсами DirectX. DirectX также поддерживает вывод в оконном режиме и даже одновременный вывод в нескольких окнах несколькими экземплярами DirectDraw. Полноэкранная программа DirectX обычно изменяет разрешение и цветовой формат экрана в соответствии со своими потребностями. Например, программы, использующие анимацию на базе палитры, должны переключить экран в режим с 256 цветами; программы, стремящиеся добиться максимального быстродействия, могут переключиться в режим с пониженным разрешением, чтобы уменьшить затраты видеопамяти и объем пересылаемых данных. Метод SetupDi rectDraw переключает видеорежим при помощи метода IDirectDraw::SetDisplayMode. Полноэкранная программа должна произвести перечисление поддерживаемых видеорежимов методом IDirectDraw: :EnumerateDisplayModes, иначе попытка переключения может завершиться неудачей. Например, многие видеоадаптеры поддерживают видеорежимы с 32-разрядным цветом, но не поддерживают 24-разрядных цветов. Метод SetupDi rectDraw также вычисляет прямоугольник поверхности вывода и сохраняет его в переменной m_rcDest. В полноэкранном режиме приемный прямоугольник соответствует всему экрану; в оконных режимах — клиентской области окна, определяемого параметром hWnd. Обратите внимание: при вызове SetupDi rectDraw передаются два манипулятора, поэтому мы не ограничены использованием DirectDraw только в главном окне. В завершение метод SetupDi rectDraw создает первичную графическую поверхность и настраивает в ней отсечение. Для решения этих задач нам понадобится класс KDDSurface.
оболочкой для работы с интерфейсом, но и содержит немало методов, упрощающих работу с поверхностями DirectDraw. В листинге 18.2 приведено объявление класса KDDSurface, а фрагменты реализации будут приводиться по мере надобности. Листинг 18.2. Класс для работы с интерфейсом IDirectDrawSurface? class KDDSurface
{ protected: IDirectDrawSurface? * m_pSurface: DDSURFACEDESC2 m_ddsd; HOC m_hDC: public: KDDSurfaceO: virtual void Discharge(void);
virtual --KDDSurfaceO
{
Все операции вывода в DirectDraw осуществляются с поверхностями, отдаленно напоминающими контексты устройств GDI. Поверхности DirectDraw могут представлять текущий экран или внеэкранный буфер, находящийся в памяти. Для первого случая можно провести аналогию с экранным контекстом устройства, а для второго — с совместимым контекстом устройства, созданным на базе DIB-секции. В настоящее время для работы с поверхностями DirectDraw используется интерфейс IDirectDrawSurface?, содержащий около 50 методов. Если прикинуть, скольким функциям GDI передается манипулятор контекста устройства, возникает желание дополнить IDirectDrawSurface новыми методами, чтобы упростить программирование. Некоторые базовые методы IDirectDrawSurface будут описаны по мере их использования в классе KDDSurface. Класс KDDSurface не только является простой
// Освобождение ресурсов
// перед вызовом деструктора // Освобождение всех ресурсов
DischargeO:
operator IDirectDrawSurface? * & О { return m_pSurface;
} operator HOC О
{
return mJiDC;
} int GetWidth(void) const
{
Интерфейс IDirectDrawSurface?
1011
Общие сведения о DirectDraw
return mjJdsd.dwWidth:
•} int GetHeight(void) const
{
return m ddsd.dwHeight:
HRESULT CreatePrimarySurface(IDirectDraw7 * pDD. int nBackBuffer): const DDSURFACEDESC2 * GetSurfaceDesc(void): virtual HRESULT RestoreSurface(void): // Восстановление // потерянных поверхностей // Блиттинг в DirectDraw HRESUtT SetClipper(IDirectDraw7 * pDD. HWND hWnd); HRESUtT Blt(LPRECT prDest. IDirectDrawSurface? * pSrc.
Продолжение
1012
Глава 18. DirectDraw и непосредственный режим DirectSD
Листинг 18.2. Продолжение LPRECT prSrc. DWORD dwFlags. LPDDBLTFX pDDB1tFx=NULL) { return m_pSurface->Blt(prDest. pSrc. prSrc. dwFlags. pDDBltFx); DWORD
ColorMatchtBYTE red. BYTE green. BYTE blue):
HRESULT FillColortint xO. int yO. int xl. int yl. DWORD fillcolor): HRESULT BitBlt(int x. int y. int w. int h. IDirectDrawSurface? * pSrc. DWORD flag=0); HRESULT BitBltCint x. int y. KDDSurface & src. DWORD flag=0) { return BitBlUx. y. src.GetWidthO, src.GetHeightO. src. flag): } HRESULT SetSourceColorKeyCDWORD color): // Вывод с использованием контекста устройства GDI HRESULT GetDC(void): // Получение манипулятора DC HRESULT ReleaseDC(void): HRESULT DrawBitmap(const BITMAPINFO * pDIB. int dx. int dy. int dw. int dh): // Прямой доступ к пикселам BYTE * LockSurface(RECT * pRect=NULL): HRESULT Unlock(RECT * pRect=NULL):
int GetPitch(void) const {
return mjldsd.lPitch:
Класс KDDSurface содержит три переменные: указатель на интерфейс IDirectDrawSurface?, структуру с описанием поверхности и манипулятор контекста устройства GDI. Указатель на IDi rectDrawSurface7 возвращается системой при создании поверхности DirectDraw, и все взаимодействие с поверхностью происходит через этот указатель. Структура DDSURFACEDESC2 описывает формат поверхности. В ней хранятся важнейшие атрибуты поверхности — тип, ширина, высота, смещение строк развертки, адрес, формат пикселов и т. д. С каждой поверхностью DirectDraw может быть связан манипулятор контекста устройства GDI, позволяющий осуществлять вывод на поверхности DirectDraw средствами GDI. Хотя поверхности DirectDraw создаются всего одним методом IDirectDraw?:: CreateSurface, существует несколько способов создания поверхности. В классе KDDSurface предусмотрены дополнительные методы, упрощающие создание поверхностей. Ниже приведен конструктор класса KDDSurface и метод создания первичной поверхности, используемой классом KDirectDraw. KDDSurface::KDDSurface()
1013
Общие сведения о DirectDraw
m_pSurface = NULL: mJiDC = NULL: m_nDCRef - 0: memset(& m_ddsd, 0. sizeof(m_ddsd)): m ddsd.dwSize = sizeoffm ddsd):
HRESULT KDDSurface: :CreatePrimarySurface(IDirectDraw7 int nBufferCount) if ( nBufferCount==0 ) { m_ddsd.dwFlags m_ddsd.ddsCaps.dwCaps
pDD.
= DDSD_CAPS: = DDSCAPS PRIMARYSURFACE:
else m_ddsd.dwFlags m_ddsd.ddsCaps.dwCaps
= DDSD_CAPS | DDSDJACKBUFFERCOUNT: - DDSCAPS_PRIMARYSURFACE | DDSCAPSJLIP DDSCAPS_COMPLEX | DDSCAPSJ/IDEOMEMORY; m ddsd.dwBackBufferCount - nBufferCount;
return pDD->CreateSurface(& m_ddsd. & m_pSurface. NULL); } В полноэкранных программах DirectX видеоадаптер может поддерживать простые поверхности, а также поверхности с двумя или тремя буферами. Простая поверхность состоит из одного буфера, в котором производится весь вывод и по содержимому которого генерируется видеосигнал. Поверхность с двумя буферами содержит два буфера: один буфер отображается на экране, а во втором выполняются операции вывода. Переключение буферов в DirectX выполняется методом IDirectDrawSurface?: :Flip. Также существуют поверхности с тремя буферами: один буфер отображается, другой ждет отображения, а в третьем выполняются операции вывода. Поверхности с двумя и тремя буферами играют важную роль для обеспечения плавного вывода без мерцания. Впрочем, они возможны только в полноэкранном монопольном режиме, поскольку аппаратное переключение буфера может выполняться только на всем экране, но не в отдельном окне. Чтобы организовать качественный вывод в оконной программе DirectDraw, вам придется использовать внеэкранную поверхность и самостоятельно копировать ее содержимое на первичную поверхность. Метод CreatePrimarySurface получает указатель на интерфейс IDirectDraw? и количество вторичных буферов. Если количество вторичных буферов равно О, метод устанавливает в структуре DDSURFACEDESC2 два флага создания простой первичной поверхности; в противном случае устанавливаются дополнительные флаги и присваивается значение полю количества вторичных буферов. Переменная m_ddsd, относящаяся к типу DDSURFACEDESC2, частично инициализируется в конструкторе класса.
1014
Глава 18. DirectDraw и непосредственный режим DirectSD
Вывод на поверхности DirectDraw От создания поверхности DirectDraw можно переходить к графическому выводу. Существует три варианта вывода на поверхности DirectDraw: методами IDirectDrawSurface?, использующими аппаратное ускорение, средствами GDI или прямыми операциями с пикселами кадрового буфера поверхности.
Вывод с аппаратным ускорением Интерфейс IDirectDrawSurface содержит всего три метода вывода: Bit, BltFast и BltBatch (причем последний метод не реализован). Поскольку методы Bit и BltFast могут ускоряться на аппаратном уровне, рекомендуется использовать их всюду, где это возможно, чтобы добиться хорошего быстродействия. Ниже приведено объявление метода ВТ t. HRESULT BltdPRECT IpDestRect, LPDIRECTDRAWSURFACE7 IpDDSrcSurface. LPRECT IpSrcRect. DWORD dwFlags. LPDDBLTFX IpDDBltFx): Метод Bit напоминает функцию StretchBlt GDI — он тоже копирует прямоугольный участок поверхности-источника в прямоугольный участок приемной поверхности. Приемная поверхность определяется текущим указателем на IDirectDrawSurface?, а приемный прямоугольник задается параметром IpDestRect. Источник определяется параметром 1 pDDSrcSurfасе, а исходный прямоугольник — параметром IpSrcRect. В параметре dwFlags передаются флаги, управляющие процессом блиттинга, а последний параметр содержит указатель на структуру DDBLTFX с дополнительными управляющими полями. Простейшим применением функции Bit является заполнение приемного прямоугольника однородным цветом (по аналогии с функцией PatBlt). Ниже приведен метод KDDSurface: -.Fill Col or, инкапсулирующий однородную заливку. HRESULT K D D S u r f a c e : : F i l l C o l o r ( i n t xO, int yO, int x l . int y l . 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); } Метод F i l l Col or заполняет структуру RECT четырьмя переданными параметрами. Поверхность и прямоугольник источника в данном случае не нужны. Параметр dwFlags равен DDBLT_COLORFILL, а структура DDBLTFX в основном определяет цвет заливки.
Вывод средствами GDI Интерфейс DirectDraw разрабатывался для того, чтобы программисты могли отойти от GDI. Впрочем, уходить слишком далеко все равно не удастся — время от времени вам понадобится помощь со стороны GDI. Хотя технология DirectDraw обеспечивает вывод с аппаратным ускорением, функции вывода в ней очень ограничены. В DirectX GDI по-прежнему занимает важное место при вы-
Общие сведения о DirectDraw
1015
воде текста и инициализации поверхности растрами. Чтобы использовать GDI для работы с поверхностью DirectDraw, следует вызвать метод IDirectDrawSurface:: GetDC для получения манипулятора контекста устройства GDI. Полученный манипулятор позднее можно освободить методом Rel easeDC. Ниже приведены методы для вызовов GetDC и Rel easeDC, а также метод для вывода DIB на поверхности DirectDraw средствами GDI. HRESULT KDDSurface::GetDC(void) {
return mj>Surface->GetDC(&m_hDC);
HRESULT KDDSurface::ReleaseDC(void) { if ( m_hDC--NULL ) return S_OK: HRESULT hr = rn_pSurface->ReleaseDC(m_hDC); mJiDC = 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_COLORS, SRCCOPY): return ReleaseDCO:
} else
return E_FAIL: } Метод DrawBitmap выводит упакованный аппаратно-независимый растр на поверхности DirectDraw. При этом используется функция StretchDIBits, идеально подходящая для загрузки растров на поверхность DirectDraw. Если быстродействие критично, функция DrawBitmap требуется только для загрузки растра на внеэкранную или текстурную поверхность, которая затем выводится на первичной поверхности аппаратно-ускоренным методом Bit. В большинстве книг по DirectX для загрузки растра применяются DDB и DIB-секции в сочетании с совместимыми контекстами устройств, для чего приходится создавать два объекта GDI. Я предпочитаю загрузку растра с использованием DIB, поскольку при этом не изменяются цвета (как при использовании DDB) и не расходуются дополнительные ресурсы GDI. Манипулятор DC, возвращаемый методом IDirectDrawSurface?::GetDC, интерпретируется как манипулятор совместимого контекста устройства. Если вызвать для него функцию GetObjectType, GDI вернет OBJ_MEMDC. Впрочем, его не стоит принимать за обычный манипулятор совместимого контекста устройства, поскольку он не был создан функцией CreateCompatibleDC или хотя бы CreateDC.
1016
Глава 18. DirectDraw и непосредственный режим DirectSD
Этот манипулятор создается специальной системной функцией NtGdiDcGetDC. Зная манипулятор DC, можно воспользоваться вызовом GetCurrentObject(m_hDC, OBJ_BITMAP) для получения растра, выбранного в контексте; функция возвращает манипулятор 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). Метод IDirectDrawSurf асе? : : Lock отображает кадровый буфер поверхности в блок памяти, адресуемый в пользовательском режиме. Фиксация одинаково работает как для первичной поверхности, так и для внеэкранных поверхностей. Работа с зафиксированной поверхностью через указатель на кадровый буфер практически не отличается от работы с массивом пикселов DIB или DIB-секции, что позволяет использовать множество интересных алгоритмов. Фиксация поверхностей навевает воспоминания о старых игровых DOS-программах, которые напрямую работали с видеопамятью и добивались высокого быстродействия, недостижимого средствами GDI. Ниже приведены методы фиксации и освобождения поверхностей. BYTE * KDDSurface::LockSurface(RECT * pRect) { if ( FAILED(m_pSurface->Lock(pRect. & m_ddsd. DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL)) ) return NULL: else return (BYTE *) m_ddsd. IpSurface:
HRESULT KDDSurface::Unlock(RECT * pRect) { m_ddsd. IpSurface = NULL; // Содержимое поверхности // становится недоступным return m_pSurface->Un1ock(pRect) : }
Метод LockSurface фиксирует прямоугольный участок поверхности, вызывая метод ID1rectDrawSurface7: :Lock, что приводит к заполнению структуры DOSURFACEDESC2. Самые важные поля заполненной структуры содержат информацию о формате пикселов поверхности, ширине, высоте, смещении строк развертки, а также указатель на кадровый буфер. Если при вызове метода Lock передается допустимый
1017
Общие сведения о DirectDraw
прямоугольник, указатель IpSurface ссылается на левый верхний пиксел этого прямоугольника; в противном случае он относится к первому пикселу поверхности. Метод Unl ock освобождает зафиксированную поверхность. Указатель, возвращаемый методом Lock, может использоваться для непосредственной работы с содержимым поверхности, однако необходима крайняя осторожность, поскольку прямой доступ не учитывает отсечения. Обращение к пикселам, находящимся за допустимыми границами, приведет к ошибкам защиты или порче содержимого других окон (если программа работает в оконном режиме). Приложение должно самостоятельно реализовать необходимое отсечение. Приведенная ниже функция уже не ограничивается простой закраской участка поверхности однородным цветом. BOOL PixelFillRecUKDDSurface & surface, int x. int y. int width. int height. DWORD dwColor[], int nColor) BYTE * pSurface = surface. LockSurface(NULL); const DDSURFACEDESC2 * pDesc = surf ace. GetSurfaceDescO; if (pSurface) { int pitch = surface.GetPitchO; int byt = pDesc->ddpf Pixel Format. dwRGBBi tCount / 8; for (int j=0: j
byt;
int i: switch (byt) { case 1: memseUpS. 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 (1-0; i<width; 1++)
1018
Глава 18. DirectDraw и непосредственный режим DirectSD
* (unsigned *) pS - color: pS += sizeof (unsigned) ; break: default: return FALSE:
surface.UnlockO; return TRUE: else return FALSE: Функция Pixel FillRect заполняет прямоугольную область разноцветными горизонтальными линиями. Она получает указатель на поверхность функцией LockSurface, вычисляет адрес графических данных в соответствии с форматом пикселов, а затем прямым копированием данных в кадровый буфер поверхности рисует линию за линией, пиксел за пикселом. Методы Blt/BltFast, вывод средствами GDI и прямой доступ к пикселам являются взаимоисключающими. При открытом манипуляторе контекста устройства GDI попытка зафиксировать поверхность завершается неудачей; вывод средствами GDI на зафиксированной поверхности тоже ни к чему не приводит. В Windows 95/98 фиксация поверхности обычно сопровождается установкой системного мьютекса, блокирующего другие программные потоки от работы с 16-разрядной реализацией GDI, из-за проблем реентерабельности. Следовательно, поверхности должны фиксироваться лишь в случае необходимости, а когда такая необходимость отпадает, поверхности следует освобождать.
1019
Общие сведения о DirectDraw
DWORD KDDSurface: : Col orMatch( BYTE red BYTE green. BYTE blue) { if ( m_ddsd . ddpf Pixel Format. dwSize==0 ) // Поверхность не инициализирована GetSurfaceDescO; // Получить описание поверхности const DDPIXELFORMAT & pf = m_ddsd. ddpf Pixel Format: if ( pf.dwFlags & DDPF_RGB ) // x-5-5-5 if ( (pf.dwRBitMask == 0/7COO)
(pf .dwGBitMask == ОхОЗЕО) (pf.dwBBitMask==Ox001F) ) return ((red»3)«10) | ((green»3)«5) | (blue»3):
// 0-5-6-5 if ( (pf.dwRBitMask == OxFSOO) && (pf .dwGBitMask == Ox07EO) && (pf.dwBBitMask==Ox001F) ) 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(mJiDC. 0. 0); SetPixel(m_hDC. О, 0. RGB(red. green, blue)): ReleaseDCO;
// Сохранить исходный пиксел // Присвоить // пиксел RGB
const DWORD * pSurface = (DWORD *) LockSurfaceO; // Зафиксировать
Подбор цветов Параметр KDDSurface:: F i l l Color, определяющий цвет заливки, относится к типу DWORD вместо типа COLORREF, знакомого нам по GDI. Значение задается в физическом цветовом формате конкретной поверхности, а не в общем формате GDI. DirectDraw в действительности является тонкой прослойкой над аппаратным уровнем. Эта прослойка настолько тонка, что в DirectDraw не существует простого способа определения цветов с использованием цветовых каналов RGB. Физические цвета, приемлемые для DirectDraw, зависят от формата пикселов поверхности. Ниже приведен простой способ подбора цветов, реализованный в виде родового метода класса KDDSurface. const DDSURFACEDESC2 * KDDSurface::GetSurfaceDesc(void) if ( SUCCEEDED(m_pSurface->GetSurfaceDesc(& mjJdsd)) ) return & m_ddsd; else return NULL;
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: else assert(false): return rslt:
// Вернуть исходный пиксел // Освободить GDI DC
1020
Глава 18. DirectDraw и непосредственный режим Direct3D
Метод ColorMatch преобразует цвет, заданный красным, зеленым и синим каналами, в физический цвет — двойное слово (DWORD), готовое к занесению в кадровый буфер. Сначала он проверяет структуру DDPIXELFORMAT; если проверка оказывается неудачной, вызывается метод- GetSurfaceDesc, возвращающий структуру с описанием текущей поверхности. Затем метод Col orMatch сравнением масок каналов пытается определить, относится ли поверхность к одному из стандартных 15-, 16-, 24 или 32-разрядных форматов RGB. Если маски совпадают, ColorMatch объединяет каналы RGB в правильный физический цвет. Если быстрый путь не приводит к успеху, приходится обращаться к GDI. Программа получает для поверхности манипулятор устройства GDI, сохраняет текущее состояние пиксела (0,0) при помощи функции GetPixel, присваивает пикселу (0,0) значение RGB функцией SetPixel, а затем читает физический цвет из зафиксированной поверхности. Перед возвратом из функции исходное состояние пиксела (0,0) восстанавливается еще одним вызовом SetPixel. Поскольку манипуляторы GDI и фиксация поверхности не могут использоваться одновременно, программа освобождает манипулятор DC перед фиксацией поверхности, а затем снова получает его для восстановления измененного пиксела. Как видите, для простого преобразования RGB-значения в физический цвет работы получается слишком много, поэтому результаты вызова ColorMatch следует по возможности использовать многократно. Метод KDDSurf асе:: F i l l Color получает физический цвет вместо логического, чтобы можно было организовать кэширование физических цветов.
Интерфейс IDirectDrawClipper Первичная поверхность, создаваемая DirectDraw, всегда распространяется на весь экран. Она позволяет рисовать в любой точке экрана, как и контекст устройства, возвращаемый вызовом GetDC(NULL). Для ограничения области вывода в DirectDraw поддерживается механизм отсечения с объектами отсечения, абстрагированными в интерфейсе IDirectDrawClipper. Сначала объект отсечения создается, а затем присоединяется к поверхности DirectDraw. Область отсечения задается так называемым списком отсечения (clip list), который представляет собой не что иное, как структуру RGNDATA, используемую GDI при операциях с объектами регионов. Чтобы инициализировать объект отсечения правильным списком отсечения, проще всего ассоциировать его с окном. Операционная система автоматически управляет списком отсечения при перемещении или изменении размеров окна с учетом его видимости. Приведенный ниже метод KDDSurf асе: :SetClipper создает объект отсечения, ассоциирует его с окном и присоединяет к поверхности. Этот метод вызывается методом KDirectDraw: :SetupDirectDraw после создания первичной поверхности. HRESULT KDDSurface::SetClipper(IDirectDraw7 * pOD. HWNO hWnd) { IDirectDrawClipper * pClipper: HRESULT hr - pDD->CreateClipper(0. & pClipper, NULL): if ( FAILEDt 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 KWindow. public KDirectDraw
{
void OnNCPaint(void)
{
RECT rect: GetWindowRect(m_hWnd. & rect): DWORD dwColor[18]: for (int i-0; i<18: i++) dwColor[i] = m_primary.ColorMatch(0. 0. 0x80 + abs(i-9)*12): Pixel FinRect(m_primary. rect.left+24. rect.top+4, rect. right - 88 - rect. left, 18, dwColor. 18); BYTE * pSurface = m_primary.LockSurface(NULL): . m_pri ma ry . Unl ock ( NULL ) ; if ( SUCCEEDED(m_primary.GetDCO) )
{
TCHAR temp[MAX_PATH] ; const DDSURFACEDESC2 * pDesc = m_primary.GetSurfaceDesc() ; if ( pDesc ) wsprintf(temp, "*dx*d *d-bpp. pitch-Xd, lpSurface=Ox*x". pDesc->dwWidth. pDesc->dwHeight. pDesc ->ddpf Pi xel Format . dwRGBBi tCount . pDesc->l Pitch. pSurface): else strcpyttemp, "LockSurface failed"): SetBkMode(m_primary. TRANSPARENT); SetTextColor(tn_primary. RGB(OxFF. OxFF. 0)); TextOut(m_primary. rect.left+24. rect.top+4,
Продолжение,
1022
Глава 18. DirectDraw и непосредственный режим DirecOD
1023
Построение графической библиотеки DirectDraw
Листинг 18.3. Продолжение temp. _tcslen(temp)); m_pnmary.ReleaseDC();
void OnDraw(void) { SetClientRect(mJiWnd); int n = min(m_rcDest.right-m_rcDest .left. m_rcDest . bottom-tn_rcDest . top ) /2 :
for (int i=0: i
LRESULT WndProc(HWND hWnd. UINT uMsg. WPARAM wParam. LPARAM IParam)
KWindow::GetWndClassEx(wc): we.style |= (CS_HREDRAW | CS_VREDRAW); Класс KDDWin объявлен производным от классов KWindow и KDirectDraw. Поддержка DirectDraw инициализируется в обработчике сообщения WM_CREATE вызовом метода KDirectDraw: :SetupDirectDraw. Обработчик сообщения WM_PAINT использует метод KDDSurf асе:: F i l l Color для заполнения клиентской области окна прямоугольниками, цвет которых постепенно изменяется от желтого к синему. Этот пример иллюстрирует вывод с аппаратным ускорением с использованием IDirectDraw7: :Blt. Обработчик WM_NCPAINT рисует фон заголовка окна функцией PixelFillRect и при помощи функции вывода текста GDI выводит в заголовке строку с описанием формата первичной поверхности (рис. 18.2). Этот простой класс показывает, как легко включить поддержку DirectDraw в обычной оконной программе при помощи классов KDirectDraw и KDDSurf асе, а также демонстрирует три способа вывода на поверхности DirectDraw. J1152x864 32 bpp, pitch=4B08, lpSurface=Ox9700UO
,Jtt,xj
switch( uMsg ) { case WM_CREATE: m_hWnd = hWnd:
if ( FAILED(SetupDirectDraw(GetParent(hWnd). hWnd false)) ) { MessageBox(NULL. _T("Unable to Initialize DirectDraw") _T("KDDWin"). MB_OK): CloseWindow(hWnd)•
} return 0: case WM_PAINT: OnDrawO: ValidateRect(hWnd. NULL): return 0: case WM_NCPAINT: DefWindowProc(hWnd. uMsg, wParam. IParam): OnNCPaintO; return 0: case WM_DESTROY: PostQuitMessage(O): return 0: default: return DefWindowProc(hwnd, uMsg, wParam, IParam):
void GetWndClassExtWNDCLASSEX & we)
Рис. 18.2. Простой пример вывода DirectDraw в оконном режиме
Построение графической библиотеки DirectDraw Как говорилось в предыдущем разделе, DirectDraw поддерживает всего два метода вывода с аппаратным ускорением, Bit и BltFast, позволяющие заполнять прямоугольные участки однородным цветом и копировать фрагменты изображений между поверхностями DirectDraw. Более сложные графические запросы приходится разбивать на серии Blt/BltFast, использовать прямой доступ к пикселам фиксированной поверхности или прибегать к помощи GDI. DirectDraw хорошо подходит для переноса в Windows игровых DOS-программ, которые обычно работают с обширными графическими библиотеками, ограничивающимися прямым доступом к пикселам. Если тексты графической библиотеки недоступны, вам придется строить свою собственную библиотеку или возвращаться к GDI.
1024
Глава 18. DirectDraw и непосредственный режим Direct3D
В этом разделе мы рассмотрим пример построения простейшей библиотеки DirectDraw, поддерживающей операции с пикселами, заполнение замкнутых фигур, вывод линий, текста, простых и прозрачных растров.
Построение графической библиотеки DirectDraw
1025
DWORD & DWordAt(int x. int y) DWORD * pPixel = (DWORD *) (pSurface + pitch * y); return pPixel [x];
Вывод пикселов Когда поверхность DirectDraw фиксируется в памяти, программа получает указатель на ее кадровый буфер. Вывод пиксела сводится к простому определению его позиции в кадровом буфере и копированию нескольких байтов. Фиксация и освобождение поверхностей DirectDraw связаны с дорогостоящими вызовами системных функций; мы не можем себе позволить такие затраты для каждого пиксела поверхности. Следовательно, архитектура графической библиотеки должна позволять приложению один раз зафиксировать поверхность и вывести сразу несколько пикселов перед ее освобождением. Ниже приведен родовой класс, который обеспечивает фиксацию/освобождение поверхностей DirectDraw и организует прямой доступ к пикселам. class KLockedSurface public: BYTE int int
pSurface: pitch: bpp;
boo! InitializetKDDSurface & surface) pSurface = surface.LockSurface(NULL); pitch = surf ace. GetPitchO: bpp = surface.GetSurfaceDesc()->ddpfPixel Format.dwRGBBitCount; return pSurface!=NULL; BYTE & ByteAUint x. int y) { BYTE * pPixel - (BYTE *) (pSurface + pitch * y): return pPixel[x]: WORD & WordAt(int x. int y) {
BOOL SetPixeKint x. int y. DWORD color) { switch ( bpp ) case 8: ByteAtCx. y) case 15: case 16: WordAtfx, y) case 24: RGBTripleAt(x. y) case 32: DWordAttx, y) default: return FALSE;
return pPixel[x]: RGBTRIPLE & RGBTripleAt(int x. int y) { RGBTRIPLE * pPixel = (RGBTRIPLE *) (pSurface + pitch * y): return pPixel[x]:
= (WORD) color: break; = * (RGBTRIPLE *) & color; break: = (DWORD) color; break:
return TRUE: DWORD GetPixeKint x, int у); // Не приводится void Line(int xO. int yO, int xl. int yl. DWORD color); В классе KLockedSurface зафиксированная поверхность представлена тремя переменными: указателем на кадровый буфер, смещением соседних строк развертки и цветовой глубиной пикселов. Метод I n i t i a l i z e фиксирует поверхность 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.Initialized this) ) frame.SetPixel(x. y, color); UnlockO; return TRUE:
WORD * pPixel = (WORD *) (pSurface + pitch * y):
}
= (BYTE) color: break;
}
else return FALSE: Чтобы класс обладал достаточно высоким быстродействием, вывод нескольких пикселов должен выполняться за одну фиксацию поверхности. Класс KLockedSurface позволяет использовать при выводе пикселов логические или другие растровые операции. Примеры:
1026
Глава 18. DirectDraw и непосредственный режим DirectSD
ByteAtCx. WordAt(x. DwordAt(x. ByteAtCx.
у) у) у) у)
|= (BYTE) color; '*= (DWORD) color: - 0: = (CByteAtCx-1. у)
// R2 MERGEPEN // R2 XORPEN // R2_BLACK + ByteAUx. y-1).
(ByteAt(x+l, у) + ByteAt(x'. 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 + randО % ( m_rcDest.right - m_rcDest.left), m_rcDest.top + randO % ( 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: int d_pixel_neg. d_error_neg: int dots:
// Поправки для error>=0 // Поправки для error<0 // Количество выводимых точек
int dx, dy. inc_x. inc_y:
if ( xl > xO ) { dx = xl - xO: inc_x - bps: } else
Построение графической библиотеки DirectDraw
{
1027
dx - xO - xl: inc_x - -bps; }
if ( yl > yO ) { 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
Построение графической библиотеки DirectDraw
RGBQ27. 255. 0). RGB(0. 127. 255). RGB(255. 0. 127)
рекомендующих программировать графические примитивы на ассемблере. Если вам доводилось просматривать ассемблерный код, сгенерированный современным компилятором, и вы уверены, что справитесь лучше — что ж, попробуйте... но учтите, что неоптимизированный ассемблерный код замедлит работу вашей программы. Если вы хотите посмотреть, на что способен компилятор, сгенерируйте листинги с командами C/C++ и ассемблерным кодом. Вот как выглядит цикл вывода 32-разрядных пикселов, обработанный компилятором VC 6.0: //
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_primary) ) {
eax : color
_repeat:
ebx dots ecx error edx pPixel esi d_error_pos edi d error neg ebp d_pixel_neg test ebx. ebx jl _finish mov eax. color inc ebx test ecx. ecx mov [edx] . eax jl _elsepart add edx. d_pixel_pos add ecx. esi jmp next
N
for (int p=0: p
frame.Line( (intKx + Radius * sin(p * theta)). (intXy + Radius * cos(p * theta)). (intKx + Radius * sin(q * theta)). (intKy + Radius * cos(q * theta)), dwColor[min(p-q, N-p+q)]):
if ( dots < 0 ) goto __finish;
m_primary.Unlock():
eax color dots
* (D *) pPixel = color: ir<0 ) goto _elsepart: if ( pPix • d_pixel_pos erro +=d_error_pos error goto next _elsepart: add edx. ebp pPix += d_pixel_neg add ecx. edi erro += d_error_neg error next: dec ebx dots dots--: jne _repeat ifif((dots!=0 ) goto_repeat На процессоре Intel, работающем в 32-разрядном режиме, имеется 7 регистров общего назначения, которые могут использоваться компилятором. В нашем цикле вывода, определяющем быстродействие вывода линий, компилятору хватило «ума» задействовать все 7 регистров. Места не осталось лишь для одного важного значения — d_pixe1_pos. В цикле вывода используются всего две операции, операндами которых не являются регистры, — обращение к d_pixe1_pos и запись пиксела в кадровый буфер. Компилятор отделяет инструкцию проверки от последующего относительного перехода, чтобы «включились» оба конвейера обработки инструкций. Метод KLockedSurf асе:: Line можно расширить для вывода стилевых линий, линий с растровыми операциями и даже с альфа-наложением. Впрочем, более толстые линии следует преобразовывать в заливки замкнутых фигур. Также предполагается, что координаты предварительно прошли отсечение. Ниже приведен пример использования метода Li ne. void LineDemoCKDDSurface & surface, int x, int y. int Radius) { const int
1029
=19;
const double theta = 3.1415926 * 2 / N: const COLORREF color[10] = { RGB(0. 0. 0). RGB(255.0.0). RGB(0.255.0). RGB(0.0, 255). RGB(255.255.0). RGBCO. 255. 255). RGB(255. 255. 0).
Заливка замкнутых областей DirectDraw поддерживает заливку прямоугольных областей однородным цветом. Непрямоугольные области приходится разбивать на прямоугольные участки или преобразовывать в регионы отсечения. Ниже приведена реализация метода KDDSurfасе::FillRgn, закрашивающего произвольный регион однородным цветом. RGNDATA * GetClipRegionData(HRGN hRgn) { DWORD dwSize = GetRegionDatathRgn, 0. NULL): RGNDATA * pRgnData = (RGNDATA *) new BYTE[dwSize]; if ( pRgnData ) GetRegionData(hRgn. dwSize. pRgnData): return pRgnData: }
BOOL KDDSurface::FillRgn(HRGN hRgn. DWORD color) { RGNDATA * pRegion = GetClipRegionData(hRgn): if ( pRegion==NULL ) return FALSE: const RECT * pRect = (const RECT *) pRegion->Buffer: for (unsigned i=0: irdh.nCount: i++) { FillColor(pRect->left. pRect->top. pRect->right. pRect->bottom. color); pRect ++: } delete [] (BYTE *) pRegion: return TRUE:
1030
Глава 18. DirectDraw и непосредственный режим DirecGD
Построение графической библиотеки DirectDraw
GDI содержит немало разнообразных функций регионов, позволяющих выводить простые геометрические фигуры и их комбинации, создавать замкнутые траектории и даже контуры текста. В программах DirectDraw рекомендуется опираться на поддержку регионов в GDI. Если быстродействие особенно важно, данные регионов можно обсчитывать заранее и кэшировать. Метод KDDSurface: :FillRgn получает манипулятор объекта региона GDI; он разбивает регион на серию прямоугольников (структура RGNDATA) функцией GetRegionData GDI, после чего закрашивает каждый прямоугольник однородным цветом при помощи метода IDirectDrawSurface7: :Blt с учетом состояния текущего объекта отсечения DirectDraw. Возможна и другая реализация — преобразовать список отсечения текущего объекта отсечения DirectDraw в регион GDI, получить его пересечение с выводимым регионом, создать новый список отсечения и вывести результат методом Bit. Недостаток подобного решения заключается в том, что вам придется создать второй объект отсечения DirectDraw и организовать переключение объекта отсечения и поверхности. В следующем примере на поверхности DirectDraw рисуется однородный эллипс:
Отсечение Поверхности DirectDraw поддерживают отсечение с использованием объектов отсечения DirectDraw, создаваемых методом IDirectDraw::Createdipper. Объекты отсечения DirectDraw делятся на две категории: ассоциированные с окном и созданные на базе списка отсечения. Когда объект отсечения ассоциируется с окном методом IDirectDrawClipper:: SetHWnd, операционная система неким волшебным образом следит за тем, чтобы объект отсечения всегда синхронизировался с обновляемым регионом конкретного окна. Следовательно, вывод на поверхности DirectDraw с присоединенным объектом отсечения может ограничиваться видимой частью клиентской области. Мы уже видели, как объекты отсечения обеспечивают правильность работы метода Bit в оконном режиме. Приложение также может напрямую управлять объектом отсечения DirectDraw, изменяя содержимое его списка отсечения, который представляет собой обычную структуру RGNDATA GDI. В документации DirectX предполагается, что программисты DirectX достаточно хорошо разбираются в программировании GDI, поэтому в ней почти ничего не говорится о том, как правильно работать со списками отсечения. Приведенные ниже функция и класс связывают объект региона GDI с объектом отсечения DirectDraw. BOOL SetClipRegiondDirectDrawClipper * pClipper. HRGN hRgn) {
void RegionDemo(void) { HRGN hRgn = CreateEllipticRgnInd1rect(& m_rcDest): if ( hRgn ) {
m_pri ma ry.Fi HRgn (hRgn, m_primary.ColorMatch(OxFF. OxFF, 0 ) ) ; DeleteObject(hRgn);
RGNDATA * pRgnData = GetClipRegionData(hRgn): if ( pRgnData==NULL ) return FALSE:
На рис. 18.3 изображено дочернее окно MDI, в котором средствами DirectDraw нарисованы пикселы, линии и эллипс.
HRESULT hr = pdipper->SetClipList(pRgnData. 0); delete (BYTE *) pRgnData; return SUCCEEDED(hr);
class KRgnCIipper {
IDirectDrawClipper * m_pNew: IDirectDrawClipper * m_p01d: IDirectDrawSurface7 * m_pSrf;
I,
Рис. 18.3. Пикселы, линии и фигуры на поверхности DirectDraw
1031
public: KRgnCIipperCIDirectDraw? * pDD. IDirectDrawSurface7 * pSrf. HRGN hRgn) {' pDD->CreateClipper(0. & m_pNew. NULL): // Создать объект отсечения SetClipRegion(m_pNew. hRgn);// Получить список отсечения // по данным региону m_pSrf - pSrf: pSrf->GetClipper(& m_p01d): // Получить старый объект отсечения pSrf->SetClipper(m_pNew); // Заменить новым объектом отсечения
1032
Глава 18. DirectDraw и непосредственный режим Direct3D
m_pSrf->SetClipper(m_p01d):
m_p01d->Release(); m_pNew->Release();
// Восстановить старый объект отсечения // Освободить старый объект отсечения // Освободить новый объект отсечения
Функция SetClipper заполняет список отсечения объекта отсечения DirectDraw данными объекта региона GDI. Для получения данных она вызывает функцию GetRegionData GDI по манипулятору региона. Как говорилось выше, поскольку данные региона имеют переменный размер, функция должна вызываться дважды — сначала вы получаете размер данных, выделяете память, а затем получаете сами данные. Класс KRgnClipper заменяет объект отсечения, связанный с поверхностью DirectDraw, новым объектом отсечения, созданным по данным объекта региона GDI. Конструктор создает новый объект отсечения, заполняет его список отсечения данными региона GDI и заменяет текущий объект отсечения, связанный с поверхностью. При всех последующих операциях вывода используется новый объект отсечения. Деструктор восстанавливает исходный объект отсечения и освобождает ресурс. В приведенной ниже функции класс KRgnClipper используется для заливки областей.
Внеэкранные поверхности Как показывает метод KDDSurface: :DrawBitmap, для вывода растра на поверхности DirectDraw проще всего воспользоваться функциями GDI. Хотя данные растра можно самостоятельно скопировать на зафиксированную поверхность DirectDraw, для обработки сжатия, поддержки разных форматов растров, масштабирования и палитры, а также преобразования формата пикселов вам придется написать довольно большой объем кода, а это приведет к снижению быстродействия и потере всех преимуществ DirectDraw. Правильный подход к выводу растров в DirectDraw использует преимущества как GDI, так и DirectDraw. Сначала растр загружается на внеэкранную поверхность средствами GDI, а затем выводится на главную поверхность средствами DirectDraw. Создание внеэкранной поверхности и загрузка растра обеспечиваются классом KOffScreenSurface, производным от KDDSurface. typedef
HRGN hUpdate - CreateRectRgn(0, 0. 1. 1): GetUpdateRgn(m_hWnd. hUpdate. FALSE); // Обновляемый регион OffsetRgn(hUpdate. mjxDest.left. m_rcDest.top): // Экранные координаты HRGN hEllipse = CreateEllipticRgn(m_rcDest.left-20. // Большой эллипс m_rcDest.top-20. m_rcDest.ri ght+20. m_rcDest.bottom+20): CombineRgn(hEllipse. hEllipse. hUpdate. RGN_AND): // Обновляемый регион AND эллипс DeleteObject(hUpdate):
class KOffScreenSurface : public KDDSurface { public: HRESULT CreateOffScreenSurfacedDirectDraw/ * pDD. int width. int height, int mem=mem_default); HRESULT CreateOffScreenSurfaceBpp(IDirectDraw7 * pDD. int width. int height, int bpp. int mem=mem_default): HRESULT CreateBitmapSurface(IDirectDraw7 * pDD. const BITMAPINFO * pDIB. int mem-mem_defau1t); HRESULT CreateBitmapSurfacedDirectDraw/ * pDD. const TCHAR * pFileName. int mem=mem default)-
KRgnClipper clipper(m_pDD, m_primary, hEllipse); DeleteObjectChEllipse); m_primary.FillColor(m_rcDest.left-20. m_rcDest.top-20. m_rcDest.right+20. m_rcDest.bottom+20. m_primary.ColorMatch(0. 0. OxFF)):
} Функция ClipDemo запрашивает обновляемый регион текущего окна и преобразует его из клиентских координат в экранные, как того требует DirectDraw. Затем функция создает эллиптический регион, размеры которого превышают размеры клиентской области, находит его пересечение с обновляемым регионом и определяет новую область отсечения. Метод KDDSurface:: F i l l Col or использует-
enum
mem_default. mem_system, memjnonlocalvi deo. mem localvideo
void ClipDemo(void)
{
1033
ся для заполнения области, большей клиентской части окна, но благодаря отсечению вывод ограничивается как границами эллипса, так и обновляемым регионом.
-KRgnClipperO
{
Построение графической библиотеки DirectDraw
const DWORD MEMFLAGS[] =
{ 0.
DDSCAPS_SYSTEMMEMORY. DDSCAPSJONLOCALVIDMEM | DDSCAPSJ/IDEOMEMORY. DDSCAPS_LOCALVIDMEM | DDSCAPSJ/IDEOMEMORY
HRESULT KOffScreenSurface::CreateOffScreenSurface(IDirectDraw7 * pDD int width, int height, int mem)
1034
Глава 18. DirectDraw и непосредственный режим DirectSD
m_ddsd.dwFlags - DOSDJAPS | DDSDJEIGHT | DDSD_WIOTH; mJdsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPSJ3DDEVICE | MEMFLAGS[mem]: . m_ddsd.dwWidth - width; m_ddsd.dwHeight = height: return pDD->CreateSurface(& m_ddsd, & m_pSurface. NULL);
HRESULT KOffScreenSurface::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 ( FAILEDChr) ) return hr;
return DrawBitmaptpDIB. 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
После того как растр загружен на внеэкранную поверхность, его можно скопировать на другую поверхность методом IDirectDrawSurface?: :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);
Поддержка прозрачности посредством цветовых ключей Метод IDirectDrawSurface?: :Blt поддерживает вывод прозрачных растров с использованием цветовых ключей. Цветовой ключ является атрибутом поверхности DirectDraw и может задаваться как для исходной, так и для приемной поверхностей. Цветовой ключ, который может представлять собой как отдельный цвет, так и интервал цветов, определяется структурой DDCOLORKEY. Ниже приведен метод SetSourceCol огКеу класса KDDSurface, задающий цветовой ключ источника с использованием одного физического цвета. HRESULT KDDSurface::SetSourceColorKey(DWORD color)
DDCOLORKEY key: key.dwColorSpaceLowValue = color: key.dwColorSpaceHighValue = color; return m_pSurface->SetColorKey(DDCKEY_SRCBLT, & key):
Чтобы скопировать внеэкранную растровую поверхность с цветовым ключом источника, вызовите метод Bit с флагом DDBLT_KEYSRC. При этом копируются только те пикселы, значения которых отличны от цветового ключа источника. В DirectDraw также поддерживаются цветовые ключи приемника.
Шрифт и текст DirectX как простой низкоуровневый интерфейс API, оптимизированный для максимального быстродействия, не обладает встроенной поддержкой шрифтов или вывода текста. Даже реализация OpenGL для Windows работает со шрифтами при помощи специальных расширений. Если вы задействуете средства GDI для операций со шрифтами и вывода текста на поверхностях DirectDraw, можно подумать и об использовании контекста устройства GDI. Однако в играх и приложениях, требующих высокого быстродействия, применение медленных функций GDI неприемлемо. В играх часто встречается другой вариант — программа заранее строит растр с полным
1036
Глава 18. DirectDraw и непосредственный режим Direct3D
набором требуемых глифов (назовем его шрифтовым растром). Вместо шрифта программа работает с растром, а вывод текста сводится к копированию фрагментов шрифтового растра. Слабой стороной такого решения является недостаточная гибкость, поскольку программа задействует ограниченный набор шрифтов заданного размера. Оптимальное решение, как и прежде, объединяет два подхода — GDI и шрифтовые растры. Идея заключается в том, чтобы динамически построить шрифтовые растры для заданной гарнитуры и кегля, а затем воспользоваться методами DirectDraw для вывода текста из шрифтовых растров. Шрифтовые растры строятся только при загрузке приложения, что расширяет выбор гарнитур и кеглей без особой потери быстродействия. Шрифтовые растры даже можно кэшировать в растровых файлах на диске и загружать на внеэкранные поверхности (вероятно, в локальную видеопамять для максимального быстродействия) методом ВТ t, поддерживающим аппаратное ускорение. В листинге 18.4 приведен класс KDDFont, поддерживающий работу с динамически сгенерированными шрифтовыми растрами на внеэкранных поверхностях.
Построение графической библиотеки DirectDraw
if ( hFont==NULL ) return EJNVALIDARG: HRESULT hr;
ABC
abc[MaxChar]:
int height: { HOC hDC = ::GetDC(NULL): if ( hDC ) { HGDIOBJ hOld = SelectObject(hDC. hFont):
TEXTMETRIC tm: GetTextMetrics(hDC, & tm): height = tm.tmHeight; if ( GetCharABCWidths(hDC. firstchar. lastchar. abc) ) hr = S_OK; else
Листинг 18.4. Класс KDDFont: работа с динамическими шрифтовыми растрами и вывод текста
hr = EJNVALIDARG;
template class KDDFont : public KOffScreenSurface int .int int int
m_offset [MaxChar]: // Метрика А m_advance[MaxChar]; // A + В + С m_pos [MaxChar]; // Горизонтальная позиция m_width [MaxChar]; // - min(A. 0) + В - min(C.O)
unsigned mjnrstchar: int mjiChar: public: HRESULT CreateFontUDirectDraw/ * pDD. const LOGFONT & If. unsigned firstchar, unsigned lastchar, COLORREF crColor); int TextOut(IDirectDrawSurface7 * pSurface. int x, int y, const TCHAR * mess, int nChar-0): template HRESULT KDDFont<MaxChar>::CreateFont(IDirectDraw7 * pDD. const LOGFONT & If. unsigned firstchar. unsigned lastchar. COLORREF crColor) { m_firstchar = firstchar; mjiChar = lastchar - firstchar + 1;
1037
SelectObjectthDC. 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:
if ( mjiChar > MaxChar ) return EJNVALIDARG;
PatBlttmJiDC. 0. 0. GetWidthO. GetHeightO. BLACKNESS);
HFONT hFont = CreateFontlndirect(Slf):
SetBkMode(m_hDC. TRANSPARENT); SetTextColor(m_hDC. crColor): // Белый основной цвет
Продолжение
Глава 18. DirectDraw и непосредственный режим Direct3D
1038
Листинг 18.4. Продолжение HGDIOBJ hOld = SelectObject(m_hDC, hFont); SetTextAlign(m_hDC. TA_TOP | TA_LEFT): for (int i=0: 1<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): // Цветовой ключ источника - черный
DeleteObject(hFont): return hr:
}: tempiate int KDDFont<MaxChar>::TextOut(IDirectDrawSurface7 * pDest, Int x. Int y. const TCHAR * mess, int nChar)
{
if ( nChar<=0 ) nChar = _tcslen(mess); for (int i=0;
{
i
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->B1t(& 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: rCreateFont инициализирует шрифтовой растр по исходным данным — указателю на объект IDirectDraw7, структуре LOGFONT GDI, интервалу символов и цвету текста. Он создает логический шрифт GDI по структуре LOGFONT и запрашивает метрики ABC, на основе которых заполняются четыре массива. В результате вычислений определяется ширина и высота шрифтового растра, после чего создается внеэкранная поверхность DirectDraw. Очистка растра и вывод в нем всех глифов осуществляются средствами GDI. К сожалению, мы не можем преобразовать все символы интервала в строку и вывести их одним вызовом функции, поскольку в строке символы могут перекрываться по горизонтали. Каждый символ рисуется отдельно в заранее вычисленной позиции растра. Символы выводятся на черном фоне, и черный цвет назначается цветовым ключом шрифтовой поверхности. Метод KDDFont: :TextOut использует содержимое поверхности шрифтового растра для вывода строки символов на поверхности DirectDraw. Для поиска глифов и их выравнивания в строке задеиствуются четыре массива, хранящихся в переменных класса KDDFont. Каждый символ выводится в прозрачном режиме с цветовым ключом источника (шрифтовой поверхности) методом IDirectDrawSurface7: :Blt. Метод TextOut выводит текст с цветовым ключом, заданным при создании шрифтовой поверхности. Класс KDDFont можно дополнить методами, изменяющими цвет текста или выводящими текст с применением специальных эффектов. Шрифтовую поверхность можно сохранить в растре и в дальнейшем обойтись без повторных вызовов GDI. Возможны и другие усовершенствования — скажем, разделение глифов дополнительными интервалами.
Спрайты Многие игровые программы основаны на использовании простых и прозрачных растров. Прозрачные растры, называемые в играх спрайтами, изображают различные перемещающиеся объекты, при соприкосновении которых в игре происходят те или иные события. Для управления перемещением спрайтов в играх задействуется клавиатура, мышь или другое устройство ввода. Ниже приведена псевдоигровая программа для DirectDraw, иллюстрирующая вывод растров, спрайтов и текста на поверхности DirectDraw. class KSpriteDemo : public KMDIChild. public KDirectDraw { KOffScreenSurface m_background: POINT m_backpos: KOffScreenSurface m_sprite: POINT m_spritepos: KDDFont<128> m_font: HINSTANCE
m_hlnst;
1040
Глава 18. DirectDraw и непосредственный режим DirecUD
m hTop:
HWND
Построение графической библиотеки DirectDraw
1041
mjilnst = hModule: m_hTop = hTop;
void OnDraw(void); void OnCreate(void): void MoveSpritednt 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: OnDrawO;
ValidateRect(hWnd, NULL); return 0:
case WMJCEYDOWN: switch ( wParam ) { case VK_HOME : MoveSprite(-l. -1); break: case VKJJP : MoveSpritet 0. -1): break: case VKJRIOR: MoveSpriteC+1, -1); break: case VKJ.EFT : MoveSpriteM, 0): break; case VKJUGHT: MoveSprite( 1. 0): break: case VK_END : MoveSprite(-l. 1): break: case VK_DOWN : MoveSprite( 0, 1): break: case VK_NEXT : MoveSprite( 1, 1): break;
} return 0:
Класс KSpriteDemo объявлен производным от классов KMDIChild (поддержка дочерних окон MDI) и KDirectDraw (поддержка DirectDraw). Переменные m_background и m_backpos предназначены для управления фоновым растром, загруженным на внеэкранную поверхность DirectDraw. Размеры фонового растра могут превышать размеры окна, в этом случае организуется прокрутка окна в фоновом растре. Переменные m_sprite и m_spritepos используются при выводе самолета поверх фоновой сцены. В переменной 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(IDB_NIGHT)): if ( pDIB ) m_background.CreateBitmapSurface(m_pDD, pDIB): pDIB = LoadBMP(m_hInst. MAKEINTRESOURCE(IDB_PLANE)); if ( pDIB )
{
m_sprite.CreateBitmapSurface(m_pDD. pDIB); m_sprite.SetSourceColorKey(0): // Черный
LOGFONT If;
case WM_CREATE: m_hWnd = hWnd; OnCreateO: // Продолжить
memset(&lf. 0. sizeof(lf)): If.lfHeight = - 36: If.IfWeight = FW_BOLD; If.lfltalic = TRUE; If.lfQuality - ANTIALIASED_QUALITY; _tcscpy(lf.IfFaceName, "Times New Roman"):
default: return KMDIChild::WndProc(hWnd. uMsg. wParam, IParam):
m_font.CreateFont(m_pDD. If. ' ' . Ox7F. RGB(OxFF, OxFF. 0 ) ) : else
public: KSpriteDemo(HMODULE hModule. HWND hTop) { m_spritepos.x m_spritepos.y m_backpos.x m_backpos.y
= 0; =0: = 0: = 0:
MessageBox(NULL. _T("Unable to Initialize DirectDraw"). _T("KSpriteDemo"). MB_OK); CloseWindow(m hWnd):
Метод OnCreate инициализирует среду DirectDraw в оконном режиме, вызывая метод KDirectDraw: :SetupDirectDraw. Затем он загружает фоновое изображение из ресурса в формате упакованного DIB-растра и инициализирует поверхность фо-
1042
Глава 18. DirectDraw и непосредственный режим DirectSD
нового растра. Спрайт тоже загружается из ресурса, и в качестве цветового ключа источника ему назначается черный цвет. Поверхность шрифтового растра инициализируется по структуре LOGFONT, представляющей сглаженный курсивный шрифт кегля 36 пунктов. Ниже приведена реализация метода OnDraw. void KSpriteDemo::OnDraw(void) SetCli entRect(m_hWnd); int dy = (m_rcDest.bottom - m_rcDest.top - m_background.GetHeight())/2: // Выводимая область выходит за границы фонового изображения if ( dy>0 ) DWORD color = m_primary.ColorMatch(Ox80. 0x40, 0):
Непосредственный режим DirectSD
1043
Вывод «игровой» сцены состоит из четырех этапов. На первом этапе рисуется клиентская область, не закрываемая фоновым изображением. В нашей программе используется фон в виде длинной и узкой полосы. Программа выравнивает фон по центру окна вдоль вертикальной оси, после чего заполняет полосы в верхней и нижней части окна однородными заливками. Фоновое изображение выводится на втором этапе, однако большая часть кода предназначена для вычисления новой позиции фонового изображения, при которой самолет будет виден на экране. На третьем этапе выполняется вывод прозрачной поверхности спрайта с цветовым ключом. На последнем, четвертом этапе в левой верхней части окна выводится простой фиксированный текст. Запустите программу. При помощи клавиш управления курсором можно управлять перемещением самолета по фоновому изображению с автоматической прокруткой. На рис. 18.4 показано окно программы DEMODD, использующей класс KSpriteDemo.
m_priтагу.Fi11 Color(m_rcDest.1eft, m_rcDest.top, m_rcDest.right. m_rcDest.top + dy, color); // Верхняя полоса color = m_primary.ColorMatch(0. 0x40, 0x80): m_primary. Fill Col or(m_rcDest. left, mjrDest. 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. mjxDest.top + m_spritepos.y + m_backpos.y. m_sprite. DDBLT_KEYSRC);
m_font.TextOut(m_primary. m_rcDest.left+5. n_rcDest.top+l. "Hello. DirectDraw!");
Рис. 18.4. Вывод текста, растров и спрайтов в DirectDraw
Если вы увлекаетесь программированием игр, попробуйте в виде спрайтов создать объекты вражеских самолетов, реализуйте проверку соприкосновений и стрельбу из оружия.
Непосредственный режим DirectSD Хотя технология DirectDraw обеспечивает аппаратное ускорение, возможности вывода в ней весьма ограничены. При работе с DirectDraw все время кажется, что вы пишете драйвер устройства, а не прикладную программу, поскольку вам приходится принимать во внимание множество мелочей. С другой стороны, непосредственный режим DirectSD как API графического программирования обладает гораздо более широкими возможностями. В DirectSD
1044
Глава 18. DirectDraw и непосредственный режим DirectSD
поддерживаются логические цвета, Z-буфер, отсечение, альфа-наложение, текстуры, области просмотра, мировые преобразования, матрицы вида, проекции, источники света, линии и треугольники, эффект тумана и т. д. Хотя непосредственный режим DirectSD проектировался как API трехмерной графики, ничто не мешает применять его и при двумерном выводе, который просто расположен в одной плоскости трехмерного пространства. В этом разделе мы в общих чертах рассмотрим программирование для непосредственного режима DirectSD.
Подготовка среды непосредственного режима DirectSD Для работы в непосредственном режиме DirectSD вам понадобится нечто большее, чем объект DirectDraw и поверхности DirectDraw, инкапсулированные в классе KDirectDraw. Обычно для этого необходим объект DirectSD, объект Direct SDDevice, поверхность вывода фона и Z-буфер. Объект DirectSD управляет доступом к поддержке DirectSD. В фоновом буфере выполняется весь графический вывод, а Z-буфер управляет отсечением скрытых поверхностей. Объект DirectSDDevice играет роль графического устройства. В листинге 18.5 приведен класс KDirectSD, инкапсулирующий среду DirectSD. Листинг 18.5. KDirectBD: класс среды непосредственного режима DirectSD class KDirectSD : public KDirectDraw
protected: IDirect3D7 * m_pD3D; IDirect3DDevice7 * m_pD3DDevice: KOffScreenSurface KOffScreenSurface bool
m_backsurface; m_zbuffer; m_bReady:
virtual HRESULT Discharge(void): virtual HRESULT OnRender(void) return S OK: virtual HRESULT OnlnitCHINSTANCE hlnst) { m_bReady = true: return S_OK:
1045
Непосредственный режим DirectSD
public: KDirectSD(void): -KDirectSD(void) { DischargeO; virtual HRESULT SetupDirectDrawtHWND hWnd. HWND hTop. int nBufferCount=0, bool bFul!Screen=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 hlnst. HWND hTop, HWND hWnd); virtual HRESULT OnResizeCHINSTANCE hlnst. int width, int height. HWND hTop. 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 ( FAILEDt 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 доступ к DirectSD 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) )
virtual HRESULT OnDischarge(void) { m_bReady = false;
iidDevice = IID_IDirect3DRGBDevice: hr = m_zbuffer.CreateZBuffer(m_pD3D. m_pDD. iidDevice, width, height);
return S_OK;
}
Продолжение,
1046
Глава 18. DirectDraw и непосредственный режим DirectSD
Листинг 18.S. Продолжение 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: D3DVIEWPORT7 vp - { 0. 0, width, height. (float)O.O. (float)l.O return m_pD3DDevice->SetViewport( &vp ); Класс KDirectSD добавляет в KDIrectDraw пять переменных: указатель на объект Direct3D7, указатель на объект DirectSDDeviceV, фоновый буфер, Z-буфер и логический флаг. Процедура инициализации начинается с настройки среды DirectDraw вызовом KDirectDraw:: SetupDi rectDraw. После инициализации DirectDraw функция проверяет текущий видеорежим, и если в нем используется палитра — возвращает код ошибки. Программы DirectSD лучше всего работают в режимах High Color и True Color; режим с палитрой тоже поддерживается, но в нем действует слишком много ограничений. Вывод DirectSD чрезвычайно сложен, поэтому все операции следует выполнять на фоновой поверхности. В полноэкранном режиме можно использовать поверхности с двумя или тремя буферами, а в оконном режиме вывод осуществляется на отдельной фоновой поверхности. Функция создает внеэкранную поверхность, размеры которой совпадают с размерами клиентской области окна. Чтобы создать Z-буфер для поверхности вывода, сначала необходимо получить указатель на интерфейс IDirect3D7 объекта DirectDraw, созданного функцией DirectDrawCreateEx. Z-буфер тоже является внеэкранной поверхностью, если не считать того, что для получения информации о форматах Z-буфера, поддерживаемых текущим устройством, используется функция IDirect3D7:: EnumZBufferformats. Функция пытается создать Z-буфер для устройства с аппаратным ускорением, но в случае неудачи переключается на устройство с программной эмуляцией. Созданный Z-буфер необходимо присоединить к фоновой поверхности. Завершающая часть метода KDirectSD:: SetupDi rectDraw создает объект DirectSDDevice? для фоновой поверхности и определяет область просмотра для устройства. Объект DirectSDDevice? обеспечивает интерфейс к средствам построения трехмерных изображений, реализованных для поверхностей с включенной SD-поддержкой. Первые четыре поля области просмотра определяют прямоугольный участок поверхности, в котором осуществляется вывод; два последних поля определяют интервал значений в Z-буфере.
Непосредственный режим DirectSD
1047
Изменение размеров окна В оконном режиме фоновая поверхность и Z-буфер создаются по размерам клиентской области окна. Тем не менее, когда пользователь изменяет размеры окна, эти поверхности необходимо создать заново для новых размеров. Самое простое решение — уничтожить все объекты DirectDraw/DirectSD и создать их с самого начала. Ниже приведены методы удаления и повторного создания объектов среды DirectSD. HRESULT KDirectSD::DischargeCvoid) SAFE_RELEASE(m_pD3DDevice): ra_backsurface.Di scharge(); m_zbuffer.Discharge(): SAFE_RELEASE(m_pD3D); return KDirectDraw::Discharge():
HRESULT KDirectSD: :ReCreate(HINSTANCE hlnst. HWND hTop. HWND hWnd) { if ( FAILED(OnDischargeO) ) return E_FAIL: if ( FAILED( DischargeO ) ) // Освободить все ресурсы return E_FAIL: SetClientRect(hWnd): HRESULT hr = SetupDi rectOrawC 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 KDirectSD : :OnResize(HINSTANCE hlnst. int width, int height. HWND hTop. HWND hWnd) { if С ! m_bReady ) return S_OK: if ( width —(mj-cDest. right - mj-cDest.left) ) if ( height— (m_rcDest. bottom - m_rcDest.top) ) return S_OK: return ReCreate(hInst, hTop. hWnd):
1048
Глава 18. DirectDraw и непосредственный режим DirecQD
Метод Discltarge освобождает все ресурсы, связанные с объектом KDirectSD. Метод Recreate вызывает Discharge, чтобы освободить все ресурсы, а затем создает новую среду DirectSD вызовом SetupDi rectDraw. Метод OnSize вызывает Recreate при изменении размеров окна.
Двухэтапный вывод При использовании фоновой поверхности изображение строится в два этапа: сначала происходит вывод на фоновой поверхности, а потом результат копируется с фоновой поверхности на первичную. Аналогичная методика применяется и к объектам DirectDraw, чтобы подавить мерцание при выводе. Ниже приведены два метода, обеспечивающие двухэтапный вывод в классе KDirectSD. HRESULT KDirectSD-Render (HWND hWnd)
{
if ( ! m_bReady ) return S_OK; HRESULT hr - OnRenderO: If ( FAILED(hr) ) return hr: hr = ShowFrame(hWnd): if ( hr - DDERRJURFACELOST )
return RestoreSurfacesO: else return hr;
HRESULT KDirectSD::ShowFrame(HWND hWnd) if ( m_bReady )
SetClientRect(hWnd); return m_priтагу.Bit(& m_rcDest, m_backsurface. NULL, DDBLT_WAIT): } else return S_OK; Метод KDirectSD::Render сначала вызывает виртуальный метод OnRender, выполняющий фактический вывод, а затем метод ShowFrame, копирующий данные с фоновой поверхности на первичную. При запуске нескольких приложений DirectX память, выделенная для поверхности, может быть захвачена другими приложениями. Программа проверяет условие потери поверхности и восстанавливает все потерянные поверхности вызовами IDirectDrawSurface?:: Restore.
Непосредственный режим DirectSD
1049
Использование DirectSD в окне Класс KDirectSD разрабатывался как родовой класс, который может использоваться где угодно. По этой причине обработку сообщений пришлось реализовать в отдельном классе. Ниже приведен простой класс окна, поддерживающего непосредственный режим DirectSD. class KDSDWin : public KWindow, public KDirect3DDemo { bool m_bActive: HINSTANCE mjilnst: LRESULT WndProctHWND hWnd. UINT uMsg, WPARAM wParam. LPARAM IParam) { 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. LOWORD(lParam). HIWORD(lParam). hWnd. hWnd)) ) CloseWindow(hWnd); break: case WMJIMER: if ( m_bActive ) Render(hWnd); return 0; case WM_DESTROY: KillTimer(hWnd. 101); DischargeO; PostQuitMessage(O); return OL: return DefWindowProc( hWnd. uMsg. wParam, IParam ): void GetWndClassEx(WNDCLASSEX & we)
1050
Глава 18. DirectDraw и непосредственный режим Direct3D
Непосредственный режим DirectSD
if ( (pddpf->dwF1ags & (DDPF_LUMINANCE|ODPF_BUMPLUMINANCE|DDPF_BUMPDUDV|DDPF_ALPHAPIXELS))==0 if ( (pddpf->dwFourCC == 0) && (pddpf->dwRGBBitCount>=16) )
KWindow::GetWndClassEx(wc); we.style |= (CS_HREDRAW | CSJREDRAW): wc.hlcon = l_oadlcon(m hlnst. MAKEINTRESOURCE(IDI_GRAPH));
{
memcpyCparam. pddpf. sizeof(DDPIXELFORMAT) ): return DDENUMRET_CANCEL: // Прекратить поиск
public:
.
KD3DWin(HINSTANCE hlnst) { m hlnst = hlnst;
Класс KDSDWin объявлен производным от классов KWindow (общая поддержка окна) и KDirect3D (поддержка DirectSD). Среда DirectSD инициализируется при обработке сообщения WM_CREATE, изменяется при получении сообщения WM_SIZE и уничтожается при обработке сообщения WM_DESTROY. Обработчик WM_PAINT выводит данные с фоновой поверхности простым вызовом KDirectSD: :ShowFrame. Класс KDSDWin создает таймер, управляющий анимацией в окне. Обработчик сообщения WM_TIMER выводит новый кадр методом KDirectSD::Render. Частота поступления сообщений таймера зависит от архитектуры операционной системы. В Windows 95/98 программа получает не более 18-19 сообщений таймера в секунду; в Windows NT/2000 в секунду может поступать до 100 сообщений. Программы DirectX обычно увеличивают частоту смены кадров за счет использования пассивных циклов при обработке сообщений. С другой стороны, изменение цикла обработки сообщений главного программного потока возможно не всегда. В альтернативном решении смена кадров выделяется в отдельный программный поток.
return DDENUMRET_OK; // Продолжить
} HRESULT KOffScreenSurface::CreateTextureSurface(IDirect3DDevice7 * pDSDOevice. IDirectDraw7 * pDD. unsigned width, unsigned height) { // Запросить информацию о возможностях устройства D3DDEVICEDESC7 ddDesc: HRESULT hr = pD3DDevice->GetCaps(&ddDesc): if С FAILED(hr) ) return hr; m_ddsd.dwFlags
= DDSD_CAPS | DDSO_HEIGHT | DDSD_WIDTH DDSD_PIXELFORMAT | ODSDJEXTURESTAGE: m_ddsd.ddsCaps.dwCaps - DOSCAPSJEXTURE; m_ddsd.dwWidth = width; m_ddsd.dwHeight = height; // Включить управление текстурами для устройств с аппаратным ускорением if ( (ddDesc.deviceGUID == IIDJDirectSDHALDevice) | (ddDesc.deviceGUID == IID_IOirect3DTnLHalDevice) ) m_ddsd.ddsCaps.dwCaps2 = DDSCAPS2JEXTUREMANAGE:
else
Текстурные поверхности Основные объекты, выводимые средствами DirectSD — точки, линии и треугольники, — обеспечивают вывод простейших геометрических форм в одномерном, двумерном и трехмерном пространстве. Чтобы геометрические фигуры больше походили на объекты реального мира, DirectSD позволяет накладывать текстуры на выводимые треугольники. Текстурный растр должен быть предварительно загружен на текстурную поверхность, используемую DirectSD. Текстурная поверхность представляет собой внеэкранную поверхность с загруженным растром. Для повышения быстродействия и расширения возможностей устройства DirectSD поддерживает несколько разновидностей форматов текстурных растров. Приложению остается лишь выбрать правильный формат текстуры в списке доступных форматов. Приведенный ниже метод KOffScreenSurface: :CreateTextSurface обеспечивает простейшее создание текстурных поверхностей. Создание текстурной поверхности на базе растра требует нескольких дополнительных действий. HRESULT CALLBACK TextureCallbacMDDPIXELFORMAT* pddpf. void * param) { // Найти простой формат текстуры >=16 бит/пиксел
mJdsd.ddsCaps.dwCaps |= DDSCAPSJYSTEMMEMORY;
// Отрегулировать ширину и высоту, если этого требует драйвер if ( ddDesc.dpcTriCaps.dwTextureCaps & D3DPTEXTURECAPS_POW2 ) { for ( m_ddsd.dwWidth=l; width > m_ddsd.dwWidth; m_ddsd.dwWidth«=l ); for ( m_ddsd.dwHeight=l; height > m_ddsd.dwHeight: m_ddsd.dwHeight«=l ): } if ( ddDesc.dpcTriCaps.dwTextureCaps & D3DPTEXTURECAPS_SQUAREONLY ) { if ( m_ddsd.dwWidth > m_ddsd.dwHeight ) m_ddsd.dwHeight - m_ddsd.dwWidth; else m_ddsd.dwWidth - m_ddsd.dwHeight: memset(& m_ddsd.ddpfPixel Format, 0, sizeof(m_ddsd.ddpfPixelFormat)): pD3DDevice->EnumTextureFormats(TextureCallback, & m_ddsd.ddpfPixel Format);
1051
1052
Глава 18. DirectDraw и непосредственный режим DirectSD
Непосредственный режим DirectSD
if ( m_ddsd.ddpfPixel Format.dwRGBBitCount ) return pOD->CreateSurface( & m_ddsd. & m_pSurface. NULL ); else return E FAIL;
// Разрешить использование Z-буфера m_pD3DDevice->SetRenderState( 03DRENDERSTATE_ZENABLE. TRUE): for (int 1=0: i<4: i++) { const int nResID[] = { IDBJIGER. IDB_PANDA. IDB_WHALE. IOBJLEPHANT }:
Пример использования непосредственного режима DirectSD Итак, в нашем распоряжении имеется среда, подготовленная классами KDi rectSD и KD3DWin, и поддержка текстурных растров. Чтобы реализовать окно DirectSD, достаточно создать класс, производный от KDirect3D, и переопределить в нем несколько методов. В листинге 18.6 приведен класс простого окна непосредственного режима DirectSD, в котором выводится вращающаяся пирамида. Пример иллюстрирует работу с текстурами и Z-буфером, а также создание анимации. Листинг 18.6. class KDirect3DDemo : public KDirect3D { KOffScreenSurface m_texture[4]: public: HRESULT OnRender(void): HRESULT OnlnitCHINSTANCE hlnst): HRESULT OnDischarge(void): HRESULT KDirect3DDemo::OnInit(HINSTANCE hlnst) { 03DMATERIAL7 mtrl; memset(&mtrl. 0. sizeof(mtrl)); mtrl.ambient.г - l.Of: mtrl.ambient.g = l.Of; mtrl.ambient.b = l.Of: m_pD3DDevice->SetMaterial( Smtrl ); m_pD3DDevice->SetRenderState( D3DRENDERSTATE_AMBIENT. RGBA_MAKE(255. 255. 255, 0) ); D3DMATRIX mat; memset(& mat. 0. sizeof(mat)): mat._ll = mat._22 = mat._33 = mat._44 = l.Of: // Матрица вида. 10 единиц по оси z D3DMATRIX matView = mat: matView._43 = 10.Of: m_pD3DDevice->SetTransform( D3DTRANSFORMSTATEJIEW. SmatView ): mat._ll = 2.Of: mat._22 = 2.Of; mat._34 = l.Of: mat._43 = -O.lf; mat._44 = O . O f ; m_pD3DDevice->SetTransform( D3DTRANSFORMSTATE_PROJECTION. &mat):
1053
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 KDirectSDDemo::OnDischarge(void) { m_bReady = false;
for (int i=0: i<4; i++) m_texture[i].Di scharge(); return S_OK: } Класс KDi rectSDDemo объявлен производным от класса KDirectSD. Он содержит массив объектов KOffScreenSurface для хранения четырех текстур и переопределяет три метода (инициализация, уничтожение и вывод). Метод 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]: D3DVECTOR pl( (float)xO. (float)yO. (float)zO ); D3DVECTOR p2( (float)xl, (float)yl. (float)zl ); D3DVECTOR p3( (float)x2. (float)y2. (float)z2 ): D3DVECTOR vNormal = Normalize(CrossProduct(pl-p2. p2-p3)); // Инициализировать З вершины фронтальной стороны треугольника
1054
Глава 18. DirectDraw и непосредственный режим DirecQD
vertices[0] = D30VERTEXC pi. vNormal. 0.5f, O.Of ): vertices[l] = D3DVERTEXC p2. vNormal. l.Of. l.Of ): vert1ces[2] = D3DVERTEX( p3, vNormal. O.Of, l.Of ): return pDevice->DrawPrimitive(D3DPT_T'RIANGLELIST. 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);
1055
Итоги
Простая вершина содержит координаты точки, вектор нормали и координаты текстуры. Координаты точки определяют местонахождение вершины в пространстве; вектор нормали задает направление поверхности, на которой находится точка, а координаты текстуры определяют позицию соответствующего пиксела на текстурном растре. При наложении текстуры DirectSD автоматически интерполирует текстуру для каждого пиксела треугольника. Функция DrawTri angle преобразует целочисленные координаты к формату с плавающей точкой, вычисляет вектор нормали к поверхности и задает для каждой вершины фиксированные координаты текстуры. Данные сохраняются в массиве D3DVERTEX и выводятся одним вызовом IDirectSDDevice: :DrawPrimitive — основным методом, предназначенным для вывода на устройствах DirectSD. На рис. 18.5 показан один из кадров при вращении пирамиды.
if ( FAILED( 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( D3DTRANSFORMSTATE_WORLD, SmatLocal ); m_p03DDevice->SetTexture( 0. m_texture[0] ): DrawTriangle(m_pD3DDevice. 0. 3. 0. 3. -3. 0. 0. -3. 3): m_pD3DDevice->SetTexture( 0. m_texture[l] ): OrawTriangle(m_pD3DDevice. 0. 3. 0. 0. -3. -3, 3. -3. 0); m_pD300evice->SetTexture( 0. m_texture[2] ); DrawTriangle(m_pD3DDevice. 0. 3. 0, -3. -3. 0, 0. -3. -3); m_pD3DDevice->SetTexture( 0. m_texture[3] ); OrawTriangle(m_pD3DDevice. 0. 3. 0. 0. -3, 3. -3. -3. 0); m_pD3DDevice->EndScerte(): return S_OK: } Метод OnRender заполняет поверхность устройства DirectSD однородным синим цветом и сбрасывает Z-буфер, используя для этого метод IDirect3DDevice7::Clear. Вывод начинается с вызова BeginScene и завершается вызовом EndScene. Метод запрашивает системное время и использует его для настройки матрицы поворота вдоль оси у. Период вращения составляет примерно 12 секунд (2 х pi x 2). Затем метод выводит четыре грани пирамиды, причем на каждую грань накладывается своя текстура. Грани пирамиды рисуются в виде треугольников в трехмерном пространстве. Вспомогательная функция DrawTriangle получает три точки пространства в целочисленных координатах. Для представления вершин, требующихся при выводе точек, линий и треугольников, в DirectSD используется структура D3DVERTEX.
Рис. 18.5. Пример использования непосредственного режима DirectBD
Из-за богатых возможностей непосредственного режима DirectSD програм мирование для него оказывается слишком сложным делом, чтобы его можн< было подробно описать на страницах этой книги. Обращайтесь к документацш DirectX от Microsoft - в ней вы найдете неплохой учебник и примеры про грамм.
Итоги В этой главе были представлены азы программирования для DirectDraw и непс средственного режима DirectSD. Мы рассмотрели процесс создания классов С+-
1056
Глава 18. DirectDraw и непосредственный режим DirectSD
для обобщенной поддержки DirectDraw/DirectSD. Эти классы отделены от манипулятора окна, поэтому они могут интегрироваться с любыми окнами. В части, посвященной DirectDraw, мы довольно подробно рассмотрели, как использовать метод Bit DirectDraw для вывода с аппаратным ускорением, как напрямую работать с зафиксированной поверхностью и как обратиться за помощью к GDI. Попутно были разработаны классы и методы для работы с внеэкранными поверхностями, текстурными поверхностями, Z-буферами и шрифтовыми поверхностями, а также для вывода текста. Надеюсь, автору удалось показать, что программирование для DirectDraw/ DirectSD — не такая уж сложная задача, особенно при наличии хорошо спроектированных классов C++. Область применения DirectDraw/DirectSD не ограничивается игровыми и учебными программами. Обычные оконные приложения тоже могут воспользоваться аппаратной поддержкой DirectDraw/DirectSD, чтобы улучшить качество вывода и сделать пользовательский интерфейс более удобным. Microsoft предоставляет неплохую документацию, учебники и примеры программ для DirectDraw, непосредственного режима Direct3D и других компонентов DirectX. Документацию и учебники можно найти в MSDN, а примеры программ - в Platform SDK и DirectX SDK. Microsoft также предлагает библиотеку классов для построения приложений непосредственного режима DirectSD; центральное место в этой библиотеке занимает класс CDSDApplication. Программный код находится в подкаталогах Include и Src\D3DFrame каталога Samples\MultiMedia\D3DIM комплекта SDK. В этой главе классы DirectSD от Microsoft не использовались, поскольку они слишком жестко объединяют приложение, окно и поддержку DirectDraw/DirectSD. Эта библиотека позволяет создавать приложения с единственным окном, поддерживающим DirectSD. Даже цикл обработки сообщений использует глобальную переменную для передачи сообщений виртуальным обработчикам, определенным в классе CD3DApp1ication. В библиотеку входит очень удобный класс для работы с текстурами, но нет класса общей поддержки поверхностей DirectDraw. Также есть чрезвычайно полезный класс для загрузки файлов DirectX в формате .X (формат Microsoft для представления трехмерных моделей). В целом библиотека содержит немало полезного кода, но для того, чтобы включить поддержку DirectSD в готовую оконную программу C++, вам придется немало потрудиться над ее адаптацией. Технология DirectX? появилась относительно недавно. На момент написания этой книги еще не было хороших учебников, которые можно было бы порекомендовать. Возможно, наряду с документацией, учебниками и примерами от Microsoft вам понадобится хорошая книга по OpenGL, поскольку непосредственный режим DirectSD имеет с OpenGL много общего.
Примеры программ К главе 14 прилагаются три программы и несколько классов для работы с DirectDraw и непосредственного режима DirectSD (табл. 18.1).
1057
Итоги Таблица 18.1. Программы главы 18 Каталог проекта
Описание
Samples\Chapt_18\ddbasic
Демонстрация основных возможностей DirectDraw
Samples\Chapt_18\DemoDD
Применение DirectDraw для вывода в дочерних окнах MDI, рисования пикселов, линий и замкнутых фигур, вывода растров, спрайтов и текста
Samples\Chapt_18\DemoD3D
Использование непосредственного режима DirectSD в окне 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 CIoseFigure, 463 стар, таблица, 767 COLOR_GRADIENTINACTIVECAPTION, 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
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 CreatePatternBmsh, 484 CreatePen, 430, 918 CreatePenlndirect, 433 CreatePolygonRgn, 509 CreatePrimarySurface, 1013 CreateRectRgn, 171, 393 CreateRoundRectRgn, 508 CreateScalableFontResource, 793 CreateService, 181 CreateWmdow/CreateWindowEx, 40, 390 CS (Code Segment), 47 CURVECAPS, 974
D3DVERTEX, структура, 1054 DD_SURFACE_INT, структура, 237 DD_SURFACE_LOCAL, структура, 237 DD_SURFACE_MORE, структура, 237 DBA, алгоритмы, 445 DDBLTFX, структура, 1014 DDCOLORKEY, структура, 1035 DDCREATE_EMULATEONLY, 1005 DDCREATE_HARDWAREONLY, 1005 DDI, интерфейс, 275 DDK (Device Driver Kit), 31 DDPIXELFORMAT, структура, 1020
1059 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 DirectSD, 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 IDirectDrawSurface?, интерфейс, 103 I Direct Draw Videoport, интерфейс, 103 архитектура, 103 прямой доступ к пикселам, 1016 структуры данных, 232 Directlnput, 100 DirectMusic, 100 DirectPlay, 100 DirectSetup, 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
EXTLOGFONTW, структура, 930 EXTLOGPEN, структура, 214 ExtSelectClipRegion, 395 ExtTextOut, 818, 864, 928
EASTEUROPE_CHARSET, 746 EDD_DIRECTDRAW_CLOBAL, структура, 234 EDD_DIRECTDRAW_LOCAL,
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
структура, 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
GCPJUSTIFY, флаг, 853 GCP_REORDER, флаг, 853 GGP_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
Алфавитный указатель
GetChar ABC Widths, 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 GetPrinter, 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 GRADIENT_TRIANGLE, структура, 524 GradientFill, 523 GREEK_CHARSET, 746 GUID, 1002
H HAL, 105 HANDLETABLE, структура, 918 HANGUL_CHARSET, 746 HBITMAP, 210, 594 HBRUSH, 151, 480 HOC, 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
IClassFactory, интерфейс, 1004 ICM (Image Color Management), 86 IDirect3D7, интерфейс, 1046 IDirectDraw, интерфейс, 103 IDirectDraw2, интерфейс, 1005 IDirectDraw7, интерфейс, 1005
1062 IDirectDrawClipper, интерфейс, 103, 1020 IDirectDrawColorControl, интерфейс, 103 IDirectDrawGammaControl, интерфейс, 103 IDirectDrawPalette, интерфейс, 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 lUnknown, интерфейс, 1001
JOHAB_CHARSET, 746 JPEG, 607
К KERNEL32.DLL, 97, 159, 183
LastChar, 759 LAYOUTJBITMAPORIENTATIONPRESERVED, 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
Novell Netware, провайдер печати, 109 NTFS (NT File System), 75 NTOSKRNL.EXE, 92 NULL, регион отсечения, 393 NULLREGION, 393 NUMCOLORS, 974
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 MMJSOTROPIC, режим отображения, 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
Алфавитный указатель
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 PolyDraw, 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 PSJOIN_ROUND, 433 PS_NULL, 433 PS_SOLID, 457 PS_USERSTYLE, 435 PT CLOSEFIGURE, 466
Querylnterface, 1006 QuickDraw GX (Apple), 855
1064
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, 403 RGBQUAD, структура, 544 RGBTRIPLE, структура, 544, 667 RGNDATA, структура, 520, 926 RoundRect, 522 RUSSIAN CHARSET, 746
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 SetClipper, 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 SetPixelV, 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
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
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 WINSPOOLDRV, 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_QUERYNEWPALETTE, сообщение, 710 WM_SIZE, сообщение, 1050 WNDCLASSEX, структура, 37 WriteFile, 381 WritePrinter, 112,957 WS_EX_LAYOUTRTL, 837 WS EX RIGHT, 837
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
замкнутые фигуры градиентные заливки, 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 назначение, 110
рабочий стол, вывод, 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 MMJTWIPS, 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
ш
шрифты {продолжение) кодировка, 745 логические, 767 моноширинные, 754 получение информации, 818 растровые, 760 семейства, 754 установка, 793 устройств, 810 штриховые кисти, 482
ширина символа, 840 шрифты, 744 FontSmart Homage Page
(HP), 799
эллипсы,360, 498
PANOSE, 815 TrueType, 765 TrueType/OpenType, 812
в GDI, 224 глифы, 751
язык описания страниц (PDL), 951 языковой монитор, 112
ИЗДАТЕЛЬСКИЙ
ДОМ
СПЕЦИАЛИСТАМ КНИЖНОГО
WWW.PITER.COM
БИЗНЕСА!
УВАЖАЕМЫЕ ГОСПОДА! ИЗДАТЕЛЬСКИЙ ДОМ «ПИТЕР» ПРИГЛАШАЕТ ВАС К ВЗАИМОВЫГОДНОМУ СОТРУДНИЧЕСТВУ. МЫ ПРЕДЛАГАЕМ ЭКСКЛЮЗИВНЫЙ АССОРТИМЕНТ КОМПЬЮТЕРНОЙ, МЕДИЦИНСКОЙ, ПСИХОЛОГИЧЕСКОЙ, ЭКОНОМИЧЕСКОЙ И ПОПУЛЯРНОЙ ЛИТЕРАТУРЫ. МЫ ГОТОВЫ РАБОТАТЬ ДЛЯ ВАС НЕ ТОЛЬКО В САНКТ-ПЕТЕРБУРГЕ. НАШИ ПРЕДСТАВИТЕЛЬСТВА НАХОДЯТСЯ В МОСКВЕ, МИНСКЕ, КИЕВЕ, ХАРЬКОВЕ. ЗА ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ ОБРАЩАЙТЕСЬ ПО СЛЕДУЮЩИМ АДРЕСАМ: Россия, г. Москва Представительство издательства «Питер», м. «Калужская», ул. Бутлерова, д. 176, оф. 207 и 240, тел./факс (095) 777-54-67. E-mail: sales@piter.msk.ru
Россия, г. С.-Петербург Представительство издательства «Питер», м. «Электросила», ул. Благодатная, д. 67, тел. (812) 327-93-37,294-54-65. E-mail: sales@piter.com
Украина, г. Харьков Представительство издательства «Питер», тел. (0572) 14-96-09, факс: (0572) 28-20-04, 28-20-05. Почтовый адрес: 61093, г. Харьков, а/я 9130. E-mail: piter@tender.kharkov.ua
Украина, г. Киев Филиал Харьковского представительства издательства «Питер», тел./факс: (044) 490-35-68, 490-35-69. Адрес для писем: 04116, г. Киев-116, а/я 2. Фактический адрес: 04073, г. Киев, пр. Красных Казаков, д. 6, корп. 1. E-mail: office@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 Редакции компьютерной, психологической, экономической, юридической, медицинской, учебной и популярной (оздоровительной и психологической) литературы Издательского дома «Питер» приглашают к сотрудничеству авторов. Обращайтесь по телефонам: Санкт-Петербург — тел. (812) 327-13-11, Москва-тел.: (095) 234-38-15, 777-54-67.
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
замкнутые фигуры градиентные заливки, 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 назначение, 110
рабочий стол, вывод, 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 MMJTWIPS, 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
ш
шрифты {продолжение) кодировка, 745 логические, 767 моноширинные, 754 получение информации, 818 растровые, 760 семейства, 754 установка, 793 устройств, 810 штриховые кисти, 482
ширина символа, 840 шрифты, 744 FontSmart Homage Page
(HP), 799
эллипсы,360, 498
PANOSE, 815 TrueType, 765 TrueType/OpenType, 812
в GDI, 224 глифы, 751
язык описания страниц (PDL), 951 языковой монитор, 112
ИЗДАТЕЛЬСКИЙ
ДОМ
СПЕЦИАЛИСТАМ КНИЖНОГО
WWW.PITER.COM
БИЗНЕСА!
УВАЖАЕМЫЕ ГОСПОДА! ИЗДАТЕЛЬСКИЙ ДОМ «ПИТЕР» ПРИГЛАШАЕТ ВАС К ВЗАИМОВЫГОДНОМУ СОТРУДНИЧЕСТВУ. МЫ ПРЕДЛАГАЕМ ЭКСКЛЮЗИВНЫЙ АССОРТИМЕНТ КОМПЬЮТЕРНОЙ, МЕДИЦИНСКОЙ, ПСИХОЛОГИЧЕСКОЙ, ЭКОНОМИЧЕСКОЙ И ПОПУЛЯРНОЙ ЛИТЕРАТУРЫ. МЫ ГОТОВЫ РАБОТАТЬ ДЛЯ ВАС НЕ ТОЛЬКО В САНКТ-ПЕТЕРБУРГЕ. НАШИ ПРЕДСТАВИТЕЛЬСТВА НАХОДЯТСЯ В МОСКВЕ, МИНСКЕ, КИЕВЕ, ХАРЬКОВЕ. ЗА ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ ОБРАЩАЙТЕСЬ ПО СЛЕДУЮЩИМ АДРЕСАМ: Россия, г. Москва Представительство издательства «Питер», м. «Калужская», ул. Бутлерова, д. 176, оф. 207 и 240, тел./факс (095) 777-54-67. E-mail: sales@piter.msk.ru
Россия, г. С.-Петербург Представительство издательства «Питер», м. «Электросила», ул. Благодатная, д. 67, тел. (812) 327-93-37,294-54-65. E-mail: sales@piter.com
Украина, г. Харьков Представительство издательства «Питер», тел. (0572) 14-96-09, факс: (0572) 28-20-04, 28-20-05. Почтовый адрес: 61093, г. Харьков, а/я 9130. E-mail: piter@tender.kharkov.ua
Украина, г. Киев Филиал Харьковского представительства издательства «Питер», тел./факс: (044) 490-35-68, 490-35-69. Адрес для писем: 04116, г. Киев-116, а/я 2. Фактический адрес: 04073, г. Киев, пр. Красных Казаков, д. 6, корп. 1. E-mail: office@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 Редакции компьютерной, психологической, экономической, юридической, медицинской, учебной и популярной (оздоровительной и психологической) литературы Издательского дома «Питер» приглашают к сотрудничеству авторов. Обращайтесь по телефонам: Санкт-Петербург — тел. (812) 327-13-11, Москва-тел.: (095) 234-38-15, 777-54-67.