Учебное пособие: Основы графического вывода
Основы графического вывод а
В предыдущих разделах мы вскользь касались темы, связанной с графическим выводом. При рассмотрении самого первого приложения (пример 1), были коротко отмечены основные правила работы с графическими устройствами, введено понятие контекст устройства (device context, DC), обсуждены некоторые основные правила работы с контекстом. При первом знакомстве мы ограничились только применением контекста устройства при обработке сообщения WM_PAINT.
Здесь еще раз будут повторены и несколько углублены рассмотренные вопросы, а также будет рассмотрено много новых, связанных с осуществлением вывода на графическое устройство. Здесь же будут рассмотрены некоторые вопросы осуществления вывода на принтер, не связанные с GDI непосредственно.
Контекст устройства
Повторим вкратце основные положения, сформулированные при первом знакомстве:
Все средства вывода в Windows относятся к графическому интерфейсу устройств (GDI). GDI представляет собой библиотеку функций для выполнения графического вывода на различных устройствах, не только на дисплее.
Все функции GDI взаимодействуют с контекстом устройства (device context, DC). Так что для осуществления вывода на устройство необходимо выполнить три основных шага:
получить хендл контекста этого устройства
осуществить собственно вывод на это устройство (рисование, вывод текста и пр.)
обязательно освободить контекст устройства.
Существует два способа получения хендла контекста устройства — создание и получение контекста устройства. Создаются достаточно специфичные контексты, например принтера. Такие контексты после использования необходимо уничтожать. Так как создание и уничтожение контекста занимает некоторое, хотя и незначительное время, и, кроме того, в большинстве случаев осуществляют вывод на дисплей, то этот процесс несколько ускорили: в системе заранее создается несколько контекстов, связанных с дисплеем. При выводе в окно или на дисплей новый контекст не создается, а получается из числа уже заготовленных системой. После использования такой контекст должен быть освобожден, а не уничтожен. Получение контекста осуществляется быстрее, чем его создание (так как в системе заранее создано некоторое количество таких контекстов) , но зато он должен быть получен и освобожден в процессе обработки одного сообщения — иначе все заготовленные контексты могут оказаться занятыми другими процессами или потоками, так что работа системы окажется нарушенной.
Контекст устройства описывает так называемые атрибуты контекста и непосредственно характеристики устройства.
Атрибуты контекста устройства независимы от самого устройства. Они характеризуют то изображение, которое будет рисоваться. В число атрибутов входят кисти, перья, шрифты, цвет текста, цвет фона и многое другое.
Информация об устройстве описывает непосредственно возможности самого графического устройства. Функции GDI взаимодействуют с устройством опосредованно — через контекст и через драйвер этого устройства. Для обеспечения универсальности средств вывода от драйверов требуется поддерживать некоторый базовый минимум операций. При необходимости выполнить более сложные операции GDI будет привлекать специальные программные расширения, являющиеся частью самого GDI. В случае использования устройств, способных аппаратно реализовывать дополнительные функции, GDI будет направлять запросы непосредственно драйверу этого устройства, а не использовать собственные расширения.
Рисунок 1. Вывод изображений с использованием контекста устройства в Windows
Как правило вы можете не заботиться о характеристиках устройств, на которых реально будет работать приложение. Однако, при разработке сложных приложений, которые могут широко распространяться, вы должны все–таки позаботиться о некоторых вопросах совместимости — например, при назначении цветов стоит их подбирать так, что бы при переходе на черно–белое оборудование изображение осталось бы различимым. Часто лучшим решением является возможность легкой настройки программы пользователем под его конкретную аппаратуру, либо использование только системных цветов (конечно, если для всех объектов, отображаемых приложением, предусмотрены системные цвета).
Обычно надо позаботиться о нормальном функционировании приложения в следующих случаях:
если приложение осуществляет вывод только в окно, то надо учитывать возможность работы:
с разным разрешением — от 640x400, 640x480 и до часто встречающихся 1024x768, 1280x1024. Было бы очень желательно, что бы даже в режиме 640x400 все диалоги и окна помещались на экране.
с разным числом цветов — от 16 и до более чем 16 миллионов цветов. При этом надо учитывать как количество цветов, которое поддерживается видеокартой, так и количество цветов, которое может воспроизводить дисплей. Чисто монохроматические дисплеи (черный и белый) уже практически не встречаются, а вот дисплеи дешевых переносных компьютеров часто дают только 8–16 градаций серого; причем различимость цветов может быть невелика. Сравнительно редкий случай, когда может встретиться монохроматический дисплей — разработка приложений для работы на серверах.
с разными настройками системной палитры; включая контрастные и энергосберегающие режимы (иногда применяются для переносных компьютеров)
если приложение способно выводить на принтер, то надо иметь в виду, что вместо принтера может оказаться плоттер, который хорошо рисует линии, но совершенно не может выводить растровых изображений, либо АЦПУ, которое способно только печатать текст.
Получение информации об устройстве
Контекст устройства содержит два вида данных — информацию об устройстве и так называемые атрибуты контекста. Информация об устройстве включает в себя описание непосредственно того графического устройства, на котором будет осуществляться вывод и возможности этого устройства по выполнению расширенных функций GDI. Эти данные специфичны для аппаратуры.
Для того, что бы получить информацию об устройстве в GDI предусмотрена функция int GetDeviceCaps (hDC, nIndex);
Эта функция возвращает целое число, являющееся значением указанного аргументом nIndex параметра устройства. В windows.h определено значительное количество символических имен, определяющих возвращаемые функцией GetDeviceCaps данные. Возвращаемое число может представлять собой как непосредственное значение запрашиваемого параметра (например, ширину устройства в миллиметрах), либо битовой последовательностью, в которой отдельные биты являются флагами (см., например, параметр RASTERCAPS). Полный список всех возможных характеристик устройства весьма обширен, поэтому приводить его здесь не будем; при необходимости можно обратиться к формальному описанию функции GetDeviceCaps в документации. Некоторые из них:
DRIVERVERSION | Версия драйвера. 0x0100 обозначает версию 1.0 |
HORZSIZE, VERTSIZE | размер устройства вывода в миллиметрах |
HORZRES, VERTRES | размер устройства вывода в единицах устройства вывода (пикселях) |
LOGPIXELSX, LOGPIXELSY |
число единиц устройства (пикселей), приходящееся на один логический дюйм[1] |
BITSPERPIXEL | число бит на 1 пиксель |
PLANES | число битовых планов |
TECHNOLOGY |
тип устройства, может принимать следующие значения: DT_PLOTTER векторный плоттер DT_RASDISPLAY растровый дисплей DT_RASPRINTER растровый принтер DT_RASCAMERA растровая камера DT_CHARSTREAM поток символов DT_METAFILE метафайл DT_DISPFILE дисплейный файл |
NUMBRUSHES | Число встроенных кистей |
NUMPENS | Число встроенных перьев |
ASPECTX | Относительная ширина пикселя |
ASPECTY | Относительная высота пикселя |
ASPECTXY | Относительная диагональ пикселя |
RASTERCAPS |
Битовая маска, указывающая возможности устройства при работе с растровыми операциями RC_BANDING поддерживает пополосный вывод RC_BITBLT может передавать битмапы RC_BITMAP64 битмапы могут быть больше 64К RC_DI_BITMAP поддерживает независимые от устройства битмапы RC_DIBTODEV поддерживает функцию SetDIBitsToDevice RC_FLOODFILL может выполнять заливку замкнутых контуров RC_GDI20_OUTPUT поддерживает расширения версии 2.0 GDI RC_PALETTE устройство использует палитру RC_SCALING устройство может масштабировать RC_STRETCHBLT устройство поддерживает функцию StretchBlt RC_STRETCHDIB устройство поддерживает функцию StretchDIBits... |
... | ... |
Одной из идей разработки GDI было обеспечение единого программного интерфейса со всеми устройствами, однако реализовать ее в полной мере практически невозможно. Поэтому вам иногда придется определять характеристики устройства, на котором вы осуществляете вывод. Например, если вы собираетесь отобразить на принтере какой–либо битмап, то надо проверить бит RC_BITBLT в параметре RASTERCAPS, так как плоттеры и АЦПУ не могут работать с растровыми изображениями; или вам может понадобиться узнать, какое число цветов может быть отображено на дисплее или цветном принтере и т.д.
Атрибуты контекста устройства
Атрибуты контекста описывают уже не само устройство а те "инструменты" и правила, которыми и по которым будет осуществляться вывод на это устройство. Атрибуты контекста являются независимыми от аппаратуры.
Контекст устройства содержит, помимо информации об устройстве, так называемые "атрибуты" контекста. Так, например, когда мы выводим текст, то применяем тот или иной шрифт. Текущий шрифт — это один из атрибутов контекста устройства. Аналогично перья, кисти, цвета и др. тоже являются атрибутами контекста устройства. Приведем полную таблицу атрибутов:
Название атрибута | Стандартное значение | Установить | Получить |
Mapping mode Система координат |
MM_TEXT | SetMapMode | GetMapMode |
Window origin Начало отсчета в логических координатах |
0,0 |
SetWindowOrg 0 SetWindowOrgEx OffsetWindowOrg 0 OffsetWindowOrgEx |
GetWindowOrg 0 GetWindowOrgEx |
Viewport origin Начало отсчета в координатах устройства |
0,0 |
SetViewportOrg 0 SetViewportOrgEx OffsetViewportOrg 0 OffsetViewportOrgEx |
GetViewportOrg 0 GetViewportOrgEx |
Window extents Масштабные коэффициенты системы координат |
1,1 |
SetWindowExt 0 SetWindowExtEx SetMapMode ScaleWindowExt 0 ScaleWindowExtEx |
GetWindowExt 0 GetWindowExtEx |
Viewport extents Масштабные коэффициенты системы координат |
1,1 |
SetViewportExt 0 SetViewportExtEx SetMapMode ScaleViewportExt 0 ScaleViewportExtEx |
GetViewportExt 0 GetViewportExtEx |
Pen Перо (карандаш) |
BLACK_PEN |
SelectObject SelectPen 2 |
SelectObject SelectPen 2 |
Current pen position Текущая позиция пера |
0,0 |
MoveTo 0 MoveToEx LineTo |
GetCurrentPosition 0 GetCurrentPositionEx |
Brush Кисть |
WHITE_BRUSH |
SelectObject SelectBrush 2 |
SelectObject SelectBrush 2 |
Brush origin Начальная точка кисти |
0,0 (screen) |
SetBrushOrg 0 SetBrushOrgEx |
GetBrushOrg 0 GetBrushOrgEx |
Font Шрифт |
SYSTEM_FONT |
SelectObject SelectFont 2 |
SelectObjectSelectFont 2 |
Bitmap Ассоциируемый битмап |
отсутствует |
SelectObject SelectBitmap 2 |
SelectObject SelectBitmap 2 |
Background mode Режим заполнения фона |
OPAQUE | SetBkMode | GetBkMode |
Background color Цвет фона |
White | SetBkColor | GetBkColor |
Text color Цвет текста |
BLACK | SetTextColor | GetTextColor |
Drawing mode Режим рисования |
R2_COPYPEN | SetROP2 | GetROP2 |
Stretching mode Режим сжатия изображения |
BLACKONWHITE | SetStretchBltMode | GetStretchBltMode |
Polygon filling mode Режим заполнения многоугольников |
ALTERNATE | SetPolyFillMode | GetPolyFillMode |
Text Alignment Привязка текста |
TA_LEFT|TA_TOP | SetTextAlign | GetTextAlign |
Intercharacter spacing Межсимвольный промежуток |
0 | SetTextCharacterExtra | GetTextCharacterExtra |
Text Justification Выравнивание строки |
0,0 | SetTextJustification | SetTextJustification |
Clipping region Область отображения |
отсутствует |
SelectObject SelectClipRgn IntersectClipRect OffsetClipRect ExcludeClipRect |
SelectObject GetClipBox |
Arc direction Направление рисования дуг |
AD_COUNTERCLOCKWISE | SetArcDirection | GetArcDirection |
В случае платформы Win32 | |||
Miter Limit Величина спрямления сопрягаемых линий |
10.0 | SetMiterLimit 1 | GetMiterLimit 1 |
Graphics Mode Режим задания координат |
GM_COMPATIBLE | SetGraphicsMode 1 | GetGraphicsMode 1 |
World Transformation Matrix Матрица преобразования глобальных координат |
1.0,0.0,0.0 0.0,1.0,0.0 |
SetWorldTransform 1 | GetWorldTransform 1 |
В последующих разделах все эти атрибуты будут рассмотрены применительно к изображению тех примитивов, на отображение которых они влияют.
Если вы в процессе рисования изменяли какие–либо атрибуты, то после того, как контекст был освобожден (удален) все ваши изменения теряются, а сразу после получения или создания контекста устройства все его атрибуты устанавливаются в стандартное состояние, приведенное в этой таблице. Из этого правила есть только одно исключение — если вы используете сохранение контекста устройства. Об этом — ниже.
Получение хендла контекста устройства
Как было отмечено выше, существует два метода получения контекста устройства — создание и получение контекста устройства. Разница связана с тем, что создание и, позже, уничтожение контекста устройства занимает некоторое время. Если вы собираетесь осуществлять вывод на принтер, то эти затраты времени ничтожно малы, по сравнению со всем временем печати. Однако, если вы собираетесь только осуществлять рисование в окне (которое может обновляться очень часто), то даже сравнительно быстрая операция создания контекста, повторенная многократно, займет значительное время. Поэтому в Windows существует несколько заранее созданных контекстов, соответствующих дисплею. При выводе в окно контекст создавать не надо, надо воспользоваться одной из функций, возвращающих такой заранее заготовленный контекст устройства.
Более того, в Windows методы, создающие контекст, предназначены для работы с устройством целиком, а методы, возвращающие уже существующий — с окном. Разница заключается в применении системы координат, связанной с контекстом. В первом случае система координат связана с верхним левым углом устройства, а во втором случае — с верхним левым углом внутренней (либо внешней) области окна.
Внимание! получаемые контексты должны быть обязательно использованы и освобождены в процессе обработки одного сообщения, в то время как создаваемые контексты устройства могут существовать продолжительное время.
Существует 7 основных методов получения и освобождения контекста устройства; причем каждый метод создает специфичный контекст устройства, предназначенный для выполнения определенных действий. Надо хорошо представлять, в каком случае каким методом надо пользоваться. Функции BeginPaint, GetDC, GetWindowDC возвращают заранее заготовленный контекст устройства, а функции CreateDC, CreateIC, CreateCompatibleDC и CreateMetaFile создают новый контекст.
1) При обработке сообщения WM_PAINT рекомендуется применять следующий способ:
PAINTSTRUCT ps;
BeginPaint (hWnd, &ps);
...
EndPaint (hWnd, &ps);
Структура PAINTSTRUCT содержит следующие данные:
typedef struct tagPAINTSTRUCT {
HDC hdc; // хендл контекста, он же возвращается функцией BeginPaint
BOOL fErase; // TRUE, если фон неверного прямоугольника надо очищать
RECT rcPaint; // неверный прямоугольник, может быть пустым!
// остальные поля используются Windows:
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[ 16 ];
} PAINTSTRUCT;
Полученный контекст устройства будет соответствовать только неверной области. Система координат остается связана с внутренней областью окна, а неверная область только ограничивает ту зону, в которой осуществляется реальное рисование; рисование вне этой области просто не приводит ни к какому эффекту.
Начиная с Windows 3.x для задания областей, нуждающихся в перерисовке используются не неверные прямоугольники, а неверные области (region), которые могут быть сложной формы. В этом случае прямоугольник rcPaint может быть указан пустым, в то время как неверная область реально существует.
Кроме этого, функция BeginPaint выполняет еще несколько операций:
если fErase равен TRUE, тогда функция BeginPaint вызывает обработку сообщения WM_ERASEBKGND с параметрами wParam= hDC, lParam= 0
неверный прямоугольник маркируется верным. Для этого BeginPaint вызывает функцию ValidateRect.
Если вам надо обязательно рисовать на всем устройстве (окне), а не только в зоне неверного прямоугольника, то перед вызовом функции BeginPaint, вы можете маркировать нужный прямоугольник или область как нуждающуюся в перерисовке.
2) Иногда необходимо получить хендл контекста для всей внутренней области окна. Для этого вы можете воспользоваться следующим способом:
HDC hDC;
hDC = GetDC (hWnd);
...
ReleaseDC (hWnd, hDC);
Этот способ часто применяется для рисования во внутренней области окна при обработке иных сообщений, чем WM_PAINT. Если вы собираетесь использовать его при обработке сообщения WM_PAINT, то помните про:
восстановление фона окна (так как обычно это делает BeginPaint)
удалите неверные прямоугольники после рисования (так, например, вы можете вызвать ValidateRect (hWnd, NULL); для маркирования всего окна верным).
3) Еще реже приходится рисовать во внешней (неклиентной, non–client) части окна, тогда вы можете воспользоваться таким способом:
HDC hDC;
hDC = GetWindowDC (hWnd);
...
ReleaseDC (hWnd, hDC);
Применяя такой контекст устройства вы можете рисовать, например, на иконке, когда ваше приложение минимизировано.
Интересная особенность этой функции — если в качестве хендла окна ей передать NULL, то функция возвратит хендл контекста, соответствующего всему дисплею.
4) В некоторых случаях надо получить доступ ко всему устройству, например принтеру. Для этого вы должны использовать пару функций CreateDC...DeleteDC следующим образом:
HDC hDC;
hDC = CreateDC (lpszDriver, lpszDevice, lpszOutput, lpData);
...
DeleteDC (hDC);
Например, для рисования непосредственно на поверхности дисплея:
hDC = CreateDC ("DISPLAY", NULL, NULL, NULL);
или принтера:
hDC = CreateDC ("IBMGRX", "IBM Graphics", "LPT1:", NULL);
параметры имеют следующие значения:
lpszDriver | — имя драйвера (имя файла без расширения) |
lpszDevice | — имя устройства (если один драйвер поддерживает несколько устройств) |
lpszOutput | — имя устройства вывода |
lpData | — указатель на данные, передаваемые во время инициализации. |
Функция CreateDC применяется сравнительно редко, так как первоначально она была ориентирована на работу с устройствами типа принтера или плоттера. Однако, для применения этой функции надо было анализировать информацию об используемом принтере (ах), содержащуюся в файле win.ini. Начиная с Windows 3.x появились специальная библиотека, реализующая стандартные диалоги и наиболее распространенные действия, включая процесс получения контекста принтера. См., например, функцию ChoosePrinter.
5) Иногда получаемый контекст нужен только для того, что бы узнать характеристики устройства. Тогда создается несколько упрощенный, так называемый информационный контекст:
HDC hDC;
hDC = CreateIC (lpszDriver, lpszDevice, lpszOutput, lpData);
...
DeleteDC (hDC);
параметры функции такие же, как и в предыдущем случае.
В самостоятельную группу надо выделить еще два способа: оба они позволяют получить контекст реально не существующего устройства. Один из них создает контекст устройства, похожего на реально существующее, но представленное только в памяти компьютера, а другой создает устройство, которое запоминает все операции по рисованию как команды, а затем может их воспроизвести на реальном устройстве.
6) Этот способ создает контекст так называемого совместимого устройства, оно реально не существует, но обладает характеристиками реально существующего.
HDC hCompatDC;
HDC hRealDC;
// для получения контекста реального устройства годится любой
// из способов 1..5
hRealDC= GetDC (hWnd);
// по хендлу контекста реально существующего устройства создается
// контекст совместимого устройства.
hCompatDC= CreateCompatibleDC (hRealDC);
// созданный таким образом совместимый контекст описывает устройство
// размером в 1 пиксел. Для нормальной работы с этим устройством
// его надо ассоциировать с битмапом, об этом - позже
// если контекст реального устройства нам больше не нужен, мы можем
// его освободить
ReleaseDC (hWnd, hRealDC);
// .. здесь мы можем использовать созданный совместимый контекст
// для освобождения совместимого контекста применяетсяфункция
DeleteDC (hCompatDC);
Совместимые контексты (compatible DC, memory DC) предназначены для работы с битмапами. Для этого созданный совместимый контекст ассоциируется с конкретным битмапом, после чего появляется возможность выполнять рисование на этом битмапе, либо осуществлять передачу изображения между битмапом и другим контекстом устройства. Подробнее о применении совместимых контекстов устройства см. в разделе «Работа с зависимым от устройства битмапом».
7) Последний способ создает так называемый метафайл. Это устройство только лишь запоминает команды GDI в специальном файле, а затем может воспроизвести заданное изображение на реальном устройстве, "проигрывая" запомненные команды. Контекст метафайла существенно отличается от других контекстов информацией об устройстве, так как ни к какому устройству он не привязан и не на кого не похож. Поэтому такие параметры, как число бит на пиксель, число встроенных шрифтов и пр. не имеет для него никакого смысла.
HDC hDC;
HANDLE hMF;
hDC = CreateMetaFile (lpszFilename);
...
hMF = CloseMetaFile (hDC);
...
DeleteMetaFile (hMF);
или
HDC hEnhDC;
HENHMETAFILE hEnhMF;
hEnhDC = CreateEnhMetaFile (lpszFilename);
...
hEnhMF = CloseEnhMetaFile (hDC);
...
DeleteEnhMetaFile (hEnhMF);
Как использовать полученный хендл метафайла мы рассмотрим позже.
Используя приведенные методы вы можете получать требуемый контекст устройства. Обычно, после получения хендла контекста устройства, производятся требуемые настройки — выбор пера, шрифта и пр. при этом такие настройки приходится делать каждый раз при получении хендла контекста устройства. В некоторых случаях, если настройки отличаются от устанавливаемых по умолчанию и редко меняются, можно ускорить рисование, сохраняя произведенные изменения.
В процессе рисования вы будете постоянно изменять атрибуты контекста — выбирать новые кисти, перья, изменять цвета и режимы рисования и так далее. Все эти изменения действуют только в то время, пока контекст существует. Как только контекст освобождается (или уничтожается, если он был создан), то все изменения, сделанные в его атрибутах, пропадают. Контекст, который вы получаете, практически всегда настроен стандартным образом.
Сохранение контекста устройства
При работе с контекстом устройства часто оказывается неудобно каждый раз создавать контекст и производить его настройку. В некоторых случаях удобно применять так называемое сохранение контекста устройства. На практике такой прием применяется сравнительно редко, так как при сколько–нибудь сложном рисунке количество изменяемых атрибутов контекста устройства достаточно велико, причем их смена может происходить многократно.
Сохранение контекста может выполняться двумя существенно различающимися путями. Так, если вам надо для выполнения какого–то фрагмента рисунка изменить большое число атрибутов контекста, а для продолжения рисования их надо восстановить, то удобно воспользоваться парой функций SaveDC и RestoreDC для сохранения и восстановления контекста.
HDC SaveDC (hDC);
BOOL RestoreDC (hDC, hSavedDC);
Возможно специальное применение функции RestoreDC (hDC, -1) — восстановить в том виде, какой был перед последним вызовом SaveDC.
Однако простым изменением порядка рисования можно в большинстве случаев обойтись без применения этих функций.
Второй способ сохранения контекста основан на описании окна, как окна, использующего сохранение контекста устройства. В этом случае для окна (или для всего класса окон) создается специальный контекст, который сохраняется между операциями его освобождения и получения. Таким образом появляется возможность сохранять все сделанные изменения в атрибутах контекста.
Для этого надо, регистрируя класс окна, указать стиль CS_OWNDC. Этот стиль указывает, что каждое окно этого класса имеет свой собственный контекст устройства. В этом контексте сохраняются все его атрибуты. Далее вы обычным способом получаете и освобождаете контекст устройства, но его атрибуты не надо устанавливать каждый раз заново.
При работе с окнами, имеющими стиль CS_OWNDC удобно настраивать атрибуты контекста при создании окна, например так:
int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{WNDCLASS wc;
// при регистрации класса окна задать стиль CS_OWNDC:
wc.style= CS_OWNDC;...
RegisterClass (&wc);...}
// при обработке сообщений в оконной функции:
LRESULT WINAPI _export WinProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{HDC hDC;
PAINTSTRUCT ps;
switch (wMsg) {
case WM_CREATE:...
hDC= GetDC (hWnd);
// ... установить атрибуты при создании окна
ReleaseDC (hWnd, hDC);...
break;
case WM_PAINT:
hDC= BeginPaint (hWnd, &ps);
// обычные функции получения хендла контекста устройства будут теперь
// возвращать хендл сохраненного контекста.
// здесь используются атрибуты, установленные ранее...
EndPaint (hWnd, &ps); // контекст по-прежнему должен быть освобожден
break;
case ...:...}
...}
Недостатком этого способа считается то, что каждое окно этого класса постоянно удерживает созданный контекст в памяти, что может быстро привести к использованию всех ресурсов компьютера.
В частном случае все окна одного класса могут иметь одинаковые атрибуты. Тогда можно указать стиль не CS_OWNDC, а CS_CLASSDC. Этот стиль указывает на то, что используется только одна сохраненная копия контекста в памяти, которая используется всеми окнами данного класса. В этом случае можно настраивать атрибуты контекста еще в функции WinMain, сразу после создания первого окна этого класса.
Системы координат GDI
Для начала надо уточнить то место, которое занимает система координат GDI в Windows. Как отмечалось при первом знакомстве, в Windows используется одновременно несколько разных систем координат. Среди них надо выделить следующие:
Система координат менеджера окон; в документации никак особо не оговаривается, что применяется именно эта система координат.
Система координат панели диалога; применяется только при разработке собственных диалогов. В тексте часто можно понять, что речь идет именно о ней, если оговаривается, что используются единицы диалога (dialog units). Если не оговаривается, то по контексту — все, связанное с шаблонами и ресурсами описания диалогов использует именно эту систему отсчета.
Система координат GDI; в документации координаты в системе координат GDI часто называют логическими координатами (logical coordinates). Там же может встретиться понятие координаты устройства (device coordinates).
Система координат GDI применяется при осуществлении графического вывода на устройство и, естественно, эта система координат определяется атрибутами контекста устройства. Таким образом для каждого контекста, существующего в настоящий момент, определяется собственная система координат.
Основные понятия
Вообще говоря, для задания любой системы координат необходимо как–то ее привязать к системе координат устройства (речь идет о системах координат, применяемых в Windows, а не о математическом понятии). Так, скажем, система координат менеджера окон отличается от системы координат устройства только лишь смещенным (а возможно и нет) началом отсчета — в верхний левый угол внутренней области окна.
Аналогично, система координат GDI тоже должна быть привязана к системе координат устройства. Точнее, она может быть привязана либо к системе координат устройства, если контекст соответствует всему устройству, либо к системе координат менеджера окон, если контекст соответствует окну.
В любом случае в Windows ту систему координат, по отношению к которой задается система координат GDI, называют системой координат устройства (device coordinates, viewport coordinates). А саму систему координат GDI называют логической системой координат (logical coordinates, window coordinates).
Обратите внимание, что английская терминология в этой области очень путаная, одно и то же понятие может обозначаться разными терминами даже в пределах одного абзаца. Так, термины viewport и device относятся к системе координат устройства, а термины logical и window описывают логическую систему координат. Это несколько странно, так как при выводе в окно система координат окна будет соответствовать координатам устройства, а логические координаты, используемые GDI, почему–то будут обозначаться термином window.
Когда система предоставляет вам контекст устройства, то его система координат совпадает с координатами устройства (окна), но у вас есть возможность эту систему координат самостоятельно изменить во время рисования на контексте.
Фактически логическая система координат определяется формулами, по которым происходит пересчет из логических координат в координаты устройства. При осуществлении вывода на контекст все координаты и размеры считаются заданными в логической системе координат, включая толщину проводимых линий, размеры шрифта, интервалы между символами, координаты точек и прочее.
В этих формулах используются нижние индексы в виде view и win, в соответствии с принятыми названиями атрибутов контекста устройства. Индекс view относится к системе координат устройства, а индекс win — к логической системе координат. То есть xview и yview — координаты какой–либо точки в системе координат устройства, Xview.org и Yview.org— относительное смещение начал отсчета систем координат, выраженное в единицах устройства (viewport origin), Xwin.org и Ywin.org — то же самое смещение, но выраженное в логических единицах (window origin), а Xview.ext, Yview.ext и Xwin.ext, Ywin.ext— масштабные коэффициенты (viewport extents, window extents).
Естественно, что в этих формулах смещение начала отсчета должно задаваться лишь единожды — либо для логической системы координат, либо для системы координат устройства. В каком именно виде — зависит исключительно от удобства. Например, если вы хотите начало отсчета логической системы координат поместить точно в центре окна (листа бумаги и пр.), то фактически вы знаете положение точки начала отсчета в координатах устройства — размеры контекста устройства, деленные пополам — тогда удобнее задать величины Xview.org и Yview.org, а Xwin.org и Ywin.org оставить равными нулю.
Рисунок 2. Система координат устройства и логическая система координат
Для обратного преобразования (из системы координат устройства в логическую систему координат), будут применяться такие же формулы, но с переставленными индексами win и view:
Иногда вам может понадобиться самим пересчитать координаты или размеры объекта из одной системы координат в другую. Вместо того, что бы самостоятельно использовать приведенные формулы, удобнее воспользоваться следующими функциями:
BOOL DPtoLP (hDC, lpPoints, nCount);
BOOL LPtoDP (hDC, lpPoints, nCount);
Функция DPtoLP преобразует координаты точек, заданных массивом lpPoints из nCount объектов типа POINT, заданные в системе координат устройства в логические координаты (DPtoLP — Device Point to Logical Point), то есть из «view» в «win», а функция LPtoDP — выполняет обратное преобразование.
Под координатами устройства может подразумеваться либо непосредственно система координат устройства, если контекст соответствует всему устройству, либо система координат, связанная с окном, если контекст соответствует внутренней или внешней области окна. Так, при выводе в окно, реальное положение какой–либо точки окна на экране может быть вычислено в два этапа — сначала с помощью функции LPtoDP надо пересчитать логические координаты в координаты, связанные с окном, а затем с помощью функции ClientToScreen пересчитать из координат окна в координаты экрана. При работе с устройством в целом, например при выводе на принтер, достаточно одной функции LPtoDP.
При использовании функций DPtoLP и LPtoDP возможно возникновение интересных ошибок. Представим себе, например, что вам надо нарисовать линию шириной 10 пиксель. Так как логическая система координат может не совпадать с координатами устройства, то линия шириной 10 логических единиц вовсе не обязательно будет шириной 10 пиксель. Само собой напрашивается примерно такой фрагмент программы для вычисления ширины линии:
POINT pt;
pt.x = 10; pt.y = 0;
DPtoLP (hdc, &pt, 1); // пересчитаем 10 пиксель (ед. устройства) в логические
// единицы. Далее считаем, что в компоненте pt.x записана нужная нам величина
Ошибка, которая присутствует в этом фрагмента, сразу и не видна. Более того, во многих случаях вы получите вполне приемлемый результат и даже не заподозрите об ошибке — до тех пор, пока у вашей логической системы координат не окажется смещенным начало отсчета по оси x от левой границы контекста. В этом случае вы получите ширину 10 пиксель, преобразованную в логические единицы плюс смещение начала отсчета:
Рисунок 3. Из–за смещения начала отсчета возможно возникновение ошибок.
Что бы избежать подобной ошибки лучше брать не одну точку, а вектор нужной длины:
POINT vector[ 2 ];
vector[0].x = 0; vector[0].y = 0;
vector[1].x = 10; vector[1].y = 0;
DPtoLP (hdc, &vector, 2);
vector[1].x -= vector[0].x;
// Далее считаем, что в компоненте vector[1].x записана нужная нам величина
Выбор системы координат
Для описания используемой системы координат предназначено пять атрибутов контекста устройства. Четыре атрибута описывают смещение начала отсчета и масштабные коэффициенты. Пятый атрибут — собственно выбранная в настоящий момент система координат.
Название атрибута | Значение по умолчанию | Обозначение в формулах | |
Mapping mode | Система координат | MM_TEXT | |
Window origin | Начало отсчета в логических координатах | 0,0 | Xwin.org, Ywin.org |
Viewport origin | Начало отсчета в координатах устройства | 0,0 | Xview.org, Yview.org |
Window extents | Масштабные коэффициенты системы координат | 1,1 | Xwin.ext, Ywin.ext |
Viewport extents | Масштабные коэффициенты системы координат | 1,1 | Xview.ext, Yview.ext |
Стандартная система координат GDI, выбираемая в контекст устройства при его создании совпадает с системой координат самого устройства (или окна). Такая система координат получила название текстовой (MM_TEXT). Вы можете отказаться от этой системы координат и установить некоторую собственную систему, у которой ориентация осей или масштабные коэффициенты отличаются от стандартной. Очевидно, что чаще всего придется устанавливать какие–либо системы координат, базирующиеся на метрической или английской системах мер. Раз так, то Microsoft предоставляет несколько дополнительных систем координат, так что во многих случаях вы можете просто выбрать подходящую вам метрическую (MM_LOMETRIC, MM_HIMETRIC), английскую систему (MM_LOENGLISH, MM_HIENGLISH) или полиграфическую (MM_TWIPS), не заботясь о точном вычислении масштабных коэффициентов. Более того, используя какую–либо из вышеназванных систем вы вообще не можете изменять масштабные коэффициенты, хотя можете перемещать точку начала отсчета.
В тех же случаях, когда вы хотите самостоятельно настраивать масштабные коэффициенты, вы можете воспользоваться системой координат MM_ANISOTROPIC, в которой вы свободно можете менять все коэффициенты, либо MM_ISOTROPIC, в которой GDI позволит вам произвольно назначать масштабные коэффициенты, но при этом сам их скорректирует, так что масштаб по обеим осям окажется равным. То есть если вы нарисуете прямоугольник с равным логическим размером сторон, то на рисунке он будет выглядеть квадратом.
Название | Единица | Ориентация осей |
MM_TEXT | 1 пиксель | |
MM_LOMETRIC | 0.1 мм | |
MM_HIMETRIC | 0.01 мм | |
MM_LOENGLISH | 0.01" | |
MM_HIENGLISH | 0.001" | |
MM_TWIPS |
1/20 полиграфической точки = 1/1440" (предполагается, что полиграфическая точка = 1/72")[2] |
|
MM_ISOTROPIC | x=y цена единицы определяется пользователем | |
MM_ANISOTROPIC | x!=y цена единицы определяется пользователем |
Обратите внимание, что при выборе любой системы координат начало отсчета размещается в верхнем левом углу контекста, даже если ось Y направлена вверх (!). При этом получается, что весь рисунок располагается в области отрицательных значений координаты Y. На практике это значит, что для большинства систем координат (кроме MM_TEXT и MM_TWIPS) вы как правило должны задать новое положение начала отсчета.
Внимание! На 16ти разрядных платформах координаты задаются целым 16ти разрядным числом со знаком, так что минимальное значение -32768, а максимальное +32767.
Для того, что бы определить или изменить текущую систему координат можно воспользоваться функциями GetMapMode, которая возвращает индекс используемой системы координат, или SetMapMode, которая устанавливает новую систему координат:
UINT GetMapMode (hDC);
UINT SetMapMode (hDC, nIndex);
Функции, изменяющие положение точки начала отсчета и масштабные коэффициенты, возвращают информацию о предыдущих или ныне действующих значениях атрибутов различным образом. Существует некоторый «старый» набор функций, изначально ориентированный на 16ти разрядную платформу. Эти функции возвращают обе компоненты атрибута (масштабные коэффициенты по осям X и Y), упакованные в двойное слово; младшее слово содержит компонент X, а старшее — компонент Y. Для получения этих компонент по отдельности можно воспользоваться макросами LOWORD (dw) и HIWORD (dw).
Так как в Win32 API координаты задаются целым числом, то есть 32х разрядным, то упаковать два компонента в одно двойное 32х разрядное слово стало невозможно. В связи с этим GDI предоставляет дополнительные функции, возвращающие необходимую информацию в структуре типа SIZE или POINT. По счастью, необходимые изменения были внесены в Windows API еще во времена Windows 3.1, так что использование большинства функций, типичных для Win32 API возможно и в Windows API.
typedef struct tagSIZE { int cx; int cy; } SIZE; |
typedef struct tagPOINT { int x; int y; } POINT; |
При использовании любой стандартной системы координат вы можете самостоятельно установить положение начала отсчета логической системы координат, задав его либо в логических единицах (window origin), либо в единицах устройства (viewport origin) с помощью функций:
// Реализованы только в Windows API
DWORD GetWindowOrg (hDC); 0
DWORD GetViewportOrg (hDC); 0
DWORD SetWindowOrg (hDC, nX, nY); 0
DWORD SetViewportOrg (hDC, nX, nY); 0
// Реализованы в Windows API (начиная с Windows 3.1) и в Win32 API
BOOL GetWindowOrgEx (hDC, lpPoint);
BOOL GetViewportOrgEx (hDC, lpPoint);
BOOL SetWindowOrgEx (hDC, nX, nY, lpPrevPoint);
BOOL SetViewportOrgEx (hDC, nX, nY, lpPrevPoint);
Для задания масштабных коэффициентов вы можете воспользоваться функциями
// Реализованы только в Windows API
DWORD GetWindowExt (hDC); 0
DWORD GetViewportExt (hDC); 0
DWORD SetWindowExt (hDC, nX, nY); 0
DWORD SetViewportExt (hDC, nX, nY); 0
DWORD ScaleWindowExt (hDC, xMul, xDiv, yMul, yDiv); 0
DWORD ScaleViewportExt (hDC, xMul, xDiv, yMul, yDiv); 0
// Реализованы в Windows API (начиная с Windows 3.1) и в Win32 API
BOOL GetWindowExtEx (hDC, lpSize);
BOOL GetViewportExtEx (hDC, lpSize);
BOOL SetWindowExtEx (hDC, nX, nY, lpPrevSize);
BOOL SetViewportExtEx (hDC, nX, nY, lpPrevSize);
BOOL ScaleWindowExtEx (hDC, xMul, xDiv, yMul, yDiv, lpPrevSize);
BOOL ScaleViewportExtEx (hDC, xMul, xDiv, yMul, yDiv, lpPrevSize);
При использовании функций Scale...Ext... система осуществляет коррекцию масштабных коэффициентов с помощью следующих формул:
Xnew.ext = (Xold.ext * xMul) / xDiv
Ynew.ext = (Yold.ext * yMul) / yDix
С помощью рассмотренных функций вы можете сами сконструировать требуемую систему координат, или выбрать какую–либо заранее описанную. Однако в разных системах координат наложены разные ограничения на изменение атрибутов. Совсем свободно манипулировать с этими атрибутами вы можете только в системе MM_ANISOTROPIC. Она позволяет описать координаты с произвольными значениями атрибутов по обеим осям.
Однако такая полная свобода в выборе масштабных коэффициентов часто является излишней. В некоторых случаях вам надо обеспечить равную величину единицы по обеим осям. Такие системы координат удобны тем, что прямоугольник с равными величинами сторон будет представляться квадратом. Конечно, вы можете сами воспользоваться информацией об устройстве и выбрать нужные значения атрибутов.
Но можно сделать и проще — воспользоваться системой координат MM_ISOTROPIC. При установке атрибутов в такой системе координат GDI сам корректирует их значения, что бы обеспечить равную цену единиц. При этом важно устанавливать сначала масштабные коэффициенты логической системы координат (с помощью функции SetWindowExt или SetWindowExtEx) и только затем коэффициенты системы координат устройства (с помощью функции SetViewportExt или SetViewportExtEx).
Во всех остальных системах координат вы можете только лишь изменять положение начала отсчета, а масштабные коэффициенты останутся неизменными.
Практические примеры
Допустим, что вы хотите сделать так, что бы логический размер окна был как минимум 1000x1000 единиц независимо от его физического размера, чтобы масштаб по обеим осям был одинаковым и при этом поместить начало отсчета координат в центр окна. Для этого вы можете воспользоваться примерно такой схемой:
void Cls_OnPaint (HWND hwnd)
{PAINTSTRUCT ps;
RECT rc;
BeginPaint (hwnd, &ps);
// устанавливаем собственную систему координат
GetClientRect (hwnd, &rc); // rc.left и rc.top всегда равны 0
SetMapMode (ps.hdc, MM_ISOTROPIC);
// задаем масштабные коэффициенты
SetWindowExtEx (ps.hdc, 1000, 1000, (LPSIZE)0L);
SetViewportExtEx (ps.hdc, rc.right, -rc.bottom, (LPSIZE)0L);
// перемещаем начало отсчета в центр контекста
SetViewportOrgEx (ps.hdc, rc.right/2, rc.bottom/2, (LPPOINT)0L);
... // осуществляем рисование в выбранной системе координат
EndPaint (hwnd, &ps);}
В качестве другого примера обратим внимание на систему координат MM_TWIPS. В этой системе координат за единицу принята 1/1440 доля дюйма. Если при подготовке какого–либо полиграфического макета вы применяете эту систему координат для вывода на принтер, то может быть целесообразным при выводе на экран воспользоваться аналогичной системой, но базирующейся на логическом дюйме:
void Cls_OnPaint (HWND hwnd)
{PAINTSTRUCT ps;
BeginPaint (hwnd, &ps);
// устанавливаем собственную систему координат
SetMapMode (ps.hdc, MM_ANISOTROPIC);
SetWindowExtEx (ps.hdc, 1440, 1440, (LPSIZE)0L);
SetViewportExtEx (
ps.hdc,
GetDeviceCaps (ps.hdc, LOGPIXELSX),
GetDeviceCaps (ps.hdc, LOGPIXELSY),
(LPSIZE)0L);
... // осуществляем рисование в выбранной системе координат
EndPaint (hwnd, &ps);}
В других случаях может возникнуть необходимость изменить масштабные коэффициенты, отталкиваясь от какой–либо стандартной системы координат. Ну, к примеру, вам надо отобразить на экране чертеж какого–либо объекта, размеры которого заданы в метрической системе координат, но при этом отобразить его в необходимом масштабе. Например, чертеж микродвигателя удобно увеличить раз в 10, а чертеж автомобиля — уменьшить раз в 50. В то же время удобно сохранить прежнюю единую метрическую систему задания размеров. Для этого удобен следующий прием — установить сначала необходимую метрическую систему координат, затем переключиться в анизотропные (или изотропные) координаты и потом скорректировать масштабные коэффициенты.
void Cls_OnPaint (HWND hwnd)
{PAINTSTRUCT ps;
SIZE sz;
RECT rc;
BeginPaint (hwnd, &ps);
GetClientRect (hwnd, &rc);
// устанавливаем собственную систему координат
SetMapMode (ps.hdc, MM_HIMETRIC);
SetMapMode (ps.hdc, MM_ANISOTROPIC);
// рисовать будем автомобиль — масштаб 50:1
ScaleWindowExtEx (ps.hdc, 50,1, 50,1, &sz);
// перемещаем начало отсчета в нижний левый угол листа
SetViewportOrgEx (ps.hdc, 0, rc.bottom, (LPPOINT)0L);
... // осуществляем рисование в выбранной системе координат
EndPaint (hwnd, &ps);}
Этот–же прием может использоваться для «переворота» осей координат. Например, можно установить метрическую систему, но ось Y направить вниз, как в MM_TEXT.
Глобальные системы координат GDI (Win32 API)
Внимание! В данном разделе рассматриваются дополнительные возможности по преобразованию систем координат, поддерживаемые 32х разрядными подсистемами в Windows NT. Остальные реализации Win32 API и все реализации Windows API не поддерживают этих возможностей.
В Win32 API предусмотрен альтернативный, более медленный, но существенно более мощный механизм для определения собственных систем координат. К сожалению, в документации при описании новых возможностей Win32 API в очередной раз произошла смена терминологии (английской). При рассмотрении глобальных систем координат выделяют четыре понятия:
система координат физического устройства (physical device coordinate space)
система координат устройства (device coordinate space)
логическая система координат (page coordinate space)
глобальная система координат (world coordinate space)
(Русскоязычная терминология приводится с минимальными изменениями по сравнению с предыдущим разделом, англоязычная — в соответствии с документацией).
Система координат физического устройства соответствует координатам и единицам устройства; для того, что бы можно было удобно работать с самыми различными устройством вводится система координат устройства, использующая какие–либо независимые от устройства единицы отсчета — например, дюймы и миллиметры. Логическая система координат соответствует логическим координатам в понимании Windows API и на нее распространяются все рассмотренные в предыдущих разделах преобразования. Следующий уровень абстракции — глобальная система координат — добавляет дополнительный механизм пересчета координат, обеспечивающий возможность поворота, перекоса, отражения и масштабирования координат.
Все эти системы координат 2х мерные, различаются только ориентацией осей, ценой деления и максимальным диапазоном изменения координат. Так координаты физического устройства ограничены, естественно, размерами самого устройства (или окна), координаты устройства могут изменяться в диапазоне 227 единиц как по горизонтали, так и по вертикали, а логические и глобальные координаты —в диапазоне ±231 единиц.
По умолчанию используется механизм, перешедший по наследству из Windows API в Win32 API, соответствующий заданию логических координат, которые GDI последовательно преобразует в координаты устройства и затем в физические координаты. Однако вы можете в любой момент перейти на альтернативный способ, при котором вы будете задавать уже не логические, а глобальные координаты. При этом надо описать специальную матрицу, которая задает необходимые преобразования:
x’ = M11 * x + M21 * y + Dx
y’ = M12 * x + M22 * y + Dy
Полученные в результате такого преобразования координаты x’ и y’ рассматриваются как логические и затем подвергаются преобразованию, соответствующему переходу от логической системы координат к координатам устройства (см. функцию SetMapMode).
Проверить, какой режим используется, или установить нужный вы можете с помощью функций
int GetGraphicsMode (hDC);
int SetGraphicsMode (hDC, nIndex);
Для задания индекса режима можно использовать одно из двух символических имен:
GM_COMPATIBLE — режим, используемый по умолчанию, соответствует обычному преобразованию логических координат в координаты устройства, принятому в Windows API.
GM_ADVANCED — расширенный режим Win32 API. В этом режиме вы можете определять или изменять матрицу преобразования глобальных координат. Точнее говоря, вы можете вызывать функции для задания или изменения матрицы преобразования координат. Если такая матрица уже задана и отличается от стандартной, то даже при переходе в GM_COMPATIBLE она будет использоваться по–прежнему. Для отключения преобразований вы должны установить стандартную матрицу преобразований (M11 и M22 равны 1.0, остальные коэффициенты M21, M12, Dx и Dy равны 0.0) с помощью функции SetWorldTransform, либо, воспользовавшись функцией ModifyWorldTransform установить исходную матрицу.
BOOL GetWorldTransform (hDC, lpxformMatrix);
BOOL SetWorldTransform (hDC, lpxformMatrix);
BOOL ModifyWorldTransform (hDC, lpxformMatrix, dwMode);
BOOL CombineTransform (lpxformResult, lpxformA, lpxformB);
typedef struct tagXFORM {
FLOAT eM11;
FLOAT eM12;
FLOAT eM21;
FLOAT eM22;
FLOAT eDx;
FLOAT eDy;
} XFORM;
Функция GetWorldTransform возвращает текущую матрицу преобразований, SetWorldTransform позволяет задать новую матрицу, а функции ModifyWorldTransform и CombineWorldTransfrom используются для изменения и вычисления коэффициентов матрицы. Считается, что матрица XFORM используется следующим образом:
В этой форме матрица XFORM сделана квадратной, добавлением третьего столбца с неизменяемыми значениями, равно как и вектора сделаны трехкомпонентными добавлением еще одного компонента, равного 1. Векторная форма записи этой матрицы будет полезна при рассмотрении функции ModifyWorldTransform, которая выполняет умножение текущей матрицы преобразований на заданную вами. Такое умножение может выполняться двумя способами (умножение матриц не коммутативно): если параметр dwMode равен MWT_LEFTMULTIPLY, то задаваемая вами матрица используется как левый операнд умножения, а текущая — как правый; а если dwMode равен MWT_RIGHTMULTIPLY, то задаваемая вами матрица будет располагаться справа от текущей. Еще одно возможное значение параметра dwMode — MWT_IDENTITY — устанавливает стандартную матрицу преобразований, при этом параметр lpxformMatrix не используется.
Последняя функция CombineTransform служит для вычисления новой матрицы преобразований lpxformResult по двум заданным матрицам lpxformA, lpxformB, которые рассматриваются как матрицы, задающие два последовательно выполняемых преобразования. Здесь интересно сделать обзор основных простейших преобразований систем координат и задаваемых для них коэффициентов. Это позволит любое сложное преобразование описать как последовательность примитивных действий и построить требуемую матрицу автоматически.
Перемещение (translation). Перемещение осуществляется добавлением постоянных величин к координатам x (коэффициент Dx) и y (коэффициент Dy). При этом коэффициенты M11 и M12 должны быть равны 1, а M12 и M21 равны 0. Формула в матричном виде раскрывается следующим образом:
x’ = x + Dx y’ = y + Dy |
Масштабирование (scaling) и зеркальное отражение (reflection). Обе эти операции выполняются одним способом, для их задания необходимо указать масштабные коэффициенты M11 (масштаб по оси X) и M22 (масштаб по оси Y).
Отрицательные значения коэффициентов соответствуют перевернутому виду. Для получения зеркального отражения задают коэффициенты равными -1.
x’ = x * M11 y’ = y * M22 |
`Поворот (rotation). Для задания коэффициентов необходимо узнать угол поворота a. Если он известен, то коэффициенты M11 и M22 оба будут равны cos a, коэффициент M12 будет равен sin a, а коэффициент M21 = ‑sin a. То есть для задания поворота необходимо вычислить коэффициенты M11 и M12, а коэффициенты M22 и M21 получаются из уже вычисленных: M22 = M11 и M21 = -M12.
x’ = x * M11 - y * M21 = x * cos a - y * sin a y’ = x * M12 + y * M22 = x * sin a + y * cos a |
Сдвиг (shear). Для задания сдвига (описание неперпендикулярных осей координат) необходимо задать два коэффициента M12 и M21, задающих величину сдвига осей. При этом коэффициенты M11 и M22 оба равны 1.
x’ = x + y * M21 y’ = x * M12 + y |
Внимание! Помимо возможности использовать функции для изменения матрицы преобразований режим GM_ADVANCED отличается от GM_COMPATIBLE рисованием прямоугольников и эллипсов — нижняя и правая границы в этом режиме включаются в рисуемый объект и рисованием дуг — они всегда рисуются против часовой стрелки.
В этом вопросе в документации встречаются некоторые неточности. Так, например, обычно утверждается, что для использования функций по заданию или изменению матрицы преобразований глобальных координат необходимо работать в расширенном режиме (GM_ADVANCED), а обратный переход от GM_ADVANCED к GM_COMPATIBLE осуществляется только при стандартной матрице преобразований. Вторая часть утверждений не совсем корректна.
Режим GM_ADVANCED отличается от GM_COMPATIBLE тем, что он позволяет изменять матрицу преобразований плюс включение нижней и правой границ ограничивающего прямоугольника в рисуемый объект, плюс рисование дуг всегда против часовой стрелки плюс некоторые особенности отображения растровых шрифтов. Сама матрица преобразований используется постоянно, вне зависимости от текущего режима, в то время как в режиме GM_COMPATIBLE вы ее просто не можете изменять. Переключение из расширенного режима в совместимый вовсе не запрещается и корректно выполняется системой.
Объекты GDI
При рассмотрении таблицы атрибутов контекста устройства вы наверное заметили, что значительное количество атрибутов изменяются с помощью функции SelectObject. Эти атрибуты представлены специальными структурами данных, описывающими так называемые объекты GDI. Эти объекты описывают некоторые примитивы GDI, используемые при выводе изображений. В качестве примера можно привести перья (pen) и кисти (brush), применяемые при рисовании линий и закраске фона фигур.
Объекты GDI не имеют ничего общего с объектами ООП, они являются объектами с только точки зрения Windows и принадлежат модулю GDI. Фактически такой объект реализован как специальная структура (иногда несколько структур) данных, управление которыми осуществляется системой, а вы можете этими данными манипулировать, используя хендл. Эти структуры данных не являются интерактивными и они не получают никаких сообщений. Так что использование в данном случае термина объект является не слишком удачным, хотя и общепринятым.
Общие правила
Объектов GDI существует достаточно большое количество, но все они имеют сходные правила применения. Перед тем как приступить к их использованию целесообразно рассмотреть основные правила применения объектов GDI.
1) Объекты могут создаваться и уничтожаться в любой момент времени, причем они могут сохраняться и между обработкой разных сообщений. Поэтому объекты часто создаются в функции WinMain или при обработке сообщения WM_CREATE, а уничтожаются, соответственно, либо при обработке сообщения WM_DESTROY, либо перед выходом из функции WinMain.
2) Все созданные объекты обязательно должны быть уничтожены до завершения приложения. Windows сам не уничтожает оставленных приложением объектов, что может привести к быстрому исчерпанию ресурсов. Это связано с тем, что объекты GDI размещаются не в глобальной памяти Windows, а в локальной памяти модуля GDI (USER.EXE или GDI32.EXE). Для этого модуля ограничен максимальный размер локальной кучи в 64К для 16ти разрядных (Windows 3.x, Windows–95) и 4М для 32х разрядных (Windows NT, Windows–98) графических подсистем, причем объекты GDI, созданные каким–либо приложением, с этим приложением не ассоциируются, в следствие чего автоматического уничтожения этих объектов не происходит[3].
3) Перед уничтожением объекта вы должны быть уверены, что он не выбран контекст устройства. Если объект в момент уничтожения используется, то он не будет уничтожен.
4) Объекты GDI кэшируется системой. То есть повторное создание часто используемого объекта осуществляется существенно быстрее, чем в первый раз.
5) Помимо создаваемых в приложении объектов, система может вам предоставить некоторые стандартные, которые соответствуют наиболее часто применяемым объектам. Например — тонкое перо черного цвета или кисть белого цвета и т.д. Стандартные объекты нельзя уничтожать. Вообще–то система должна заметить, что предпринимается попытка уничтожения стандартного объекта и запретить эту операцию. Но ошибки встречаются везде, даже в системе, так что лучше не уповать на ее надежность.
6) Разные объекты имеют хендлы со специфичными названиями HPEN, HBRUSH, HFONT и др. Вы можете применять просто HANDLE или HGDIOBJ вместо всех этих типов. Применение специфичных типов может быть предпочтительным при осуществлении строгой проверки типов. В разных API и реализациях windows.h для разных компиляторов стандартные функции GDI могут использовать несколько различающиеся типы хендлов. Так, например, функция SelectObject, которая может работать с объектами разных типов, обычно декларирована как функция, получающая хендл типа HGDIOBJ. Однако в старых 16ти разрядных версиях windows.h она может быть описана как получающая просто HANDLE. Часто может быть удобнее применять макросы, определенные в windowsx.h, которые осуществляют соответствующие операции с необходимым приведением типов. Например, вместо SelectObject можно использовать макросы SelectPen, SelectBrush, SelectFont и т.д., смотря по типу выбираемого объекта.
7) Стандартные объекты можно получить с помощью функции
HGDIOBJ GetStockObject (nIndex);
Она возвращает хендл стандартного объекта. Нужный объект задается параметром nIndex. Например, это может быть BLACK_PEN, WHITE_BRUSH, SYSTEM_FONT и т.д. Подробнее — см. в описании функции. Либо, вместо этой универсальной функции можно применять макросы, определенные в windowsx.h: GetStockPen, GetStockBrush, GetStockFont и пр. Эти макросы будут возвращать результат соответствующего типа (HPEN, HBRUSH, HFONT, ...). При использовании макросов надо проследить, что бы индекс требуемого объекта соответствовал используемому макросу. Например, вы можете по ошибке использовать макрос GetStockPen для получения хендла стандартного шрифта — так как макрос в итоге обратиться к универсальной функции GetStockObject, то фатальной ошибки не возникнет, просто возвращаемый результат будет приведен к некорректному типу (в примере: HPEN вместо HFONT).
8) Для создания собственных объектов применяются функции, начинающиеся со слова Create... Например, CreatePen или CreateSolidBrush.
9) Полученный объект (стандартный или созданный вами) выбирается в контекст устройства функцией:
HGDIOBJ SelectObject (hDC, hObject);
Эта функция возвращает хендл объекта того–же типа, выбранного ранее в этот контекст. В большинстве случаев может быть удобнее воспользоваться вместо функции SelectObject макросами из windowsx.h, предназначенными для работы с конкретными объектами:
HPEN SelectPen (hDC, hPen);
HBRUSH SelectBrush (hDC, hBrush);
HFONT SelectFont (hDC, hFont);
HBITMAP SelectBitmap (hDC, hBitmap);
10) Для получения информации об объекте применяется функция
int GetObject (hObject, nSize, lpvStruct);
где hObject — хендл объекта GDI, информацию о котором вы запрашиваете, lpvStruct — указатель на структуру данных, которая будет заполняться информацией об объекте; для объектов разных типов определены разные структуры (BITMAP, LOGPEN, LOGBRUSH, LOGFONT и т.д.), nSize — размер этой структуры.
11) Объект уничтожается функцией
BOOL DeleteObject (hObject);
Эта функция удаляет указанный объект, если только это не стандартный объект и если он не выбран в контекст устройства. Вместо одной универсальной функции в windowsx.h можно найти макросы, удаляющие объекты конкретных типов: DeletePen, DeleteBrush, DeleteFont и т.д.
Обычное использование
Сейчас мы рассмотрим обычные схемы использования объектов. В самом типичном случае объект создается непосредственно тогда, когда он используется, и уничтожается сразу после освобождения из контекста устройства. Несмотря на часто выполняемую операцию создания и уничтожения этот метод является предпочтительным, так как до минимума сводится количество одновременно существующих объектов (что снижает требования к объему свободных ресурсов GDI), а затраты времени минимизируются благодаря кэшированию объектов системой.
В качестве примера мы будем использовать только один объект GDI — перо, так как применение всех остальных типов объектов аналогично.
void Cls_OnPaint (HWND hwnd)
{PAINTSTRUCT ps;
HPEN hpenOld, hpenRed, hpenBlue;
BeginPaint (hwnd, &ps);
// создаем перья:
hpenRed = CreatePen (PS_SOLID, 0, RGB (255,0,0));
hpenBlue = CreatePen (PS_SOLID, 0, RGB (0,0,255));
// выбираем его в контекст и запоминаем прежнее:
hpenOld = (HPEN)SelectObject (ps.hdc, (HGDIOBJ)hpenRed);
... // осуществляем рисование красным пером
// выбираем другое перо, причем запоминать предыдущее не надо – оно
// и так известно - это hpenRed
SelectObject (ps.hdc, (HGDIOBJ)hpenBlue);
... // осуществляем рисование синим пером
// освобождаем созданное перо из контекста (то есть выбираем первоначальное)
SelectObject (ps.hdc, (HGDIOBJ)hpenOld);
// и удаляем созданные
DeleteObject ( (HGDIOBJ)hpenRed);
DeleteObject ( (HGDIOBJ)hpenBlue);
EndPaint (hwnd, &ps);}
Этот пример иллюстрирует все основные шаги по работе с создаваемыми объектами GDI. Однако в реальной жизни он используется не слишком часто — как правило при рисовании используется не один объект данного типа, а несколько. Это приводит к появлению большого количества переменных, как–то: hpen1, hpen2, ..., hpen100. Читаемость такого текста сравнительно невелика, да и вероятность запутаться остается высокой. Другое соображение — часто встречающееся приведение типов. В этом случае удобнее использовать макросы из windowsx.h. В третьих — сохранять исходный объект для восстановления его в контексте устройства не обязательно. Если существуют стандартные объекты данного типа, то вместо восстановления исходного можно перед удалением объектов выбрать в контекст любой стандартный того же типа (скажем, стандартных регионов или битмапов не определено — для них рекомендуется сохранять исходный).
В результате часто используются целые комбайны из функций GDI, как в приводимом ниже примере:
void Cls_OnPaint (HWND hwnd)
{PAINTSTRUCT ps;
BeginPaint (hwnd, &ps);
// создаем красное перо и выбираем его в контекст;
// прежнее НЕ запоминаем:
SelectPen (ps.hdc, CreatePen (PS_SOLID, 0, RGB (255,0,0)));
... // осуществляем рисование красным пером
// создаем и выбираем синее перо;
// предыдущее - созданное нами красное - сразу уничтожаем
DeletePen (SelectPen (ps.hdc, CreatePen (PS_SOLID, 0, RGB (0,0,255))));
... // осуществляем рисование синим пером
// освобождаем синее перо из контекста и уничтожаем его
DeletePen (SelectPen (ps.hdc, GetStockPen (BLACK_PEN)));
EndPaint (hwnd, &ps);}
В этом случае экономятся как локальные переменные, так и дополнительно снижаются требования к объему свободных ресурсов GDI, хотя к чтению подобных длинных выражений надо привыкать. Единственное, за чем надо внимательно следить — что бы при выборе нового объекта вместо предыдущего, созданного вами, использовалась функция DeleteObject (или эквивалентный макрос), а при выборе нового объекта вместо стандартного эта функция не использовалась.
Более редкий случай — когда объекты создаются при обработке WM_CREATE и уничтожаются при обработке WM_DESTROY (или при запуске и завершении приложения). Этот способ используется редко, так как требуется описание дополнительных статических переменных и, кроме того, интенсивнее расходуются ресурсы GDI.
LRESULT WINAPI _export WinProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{PAINTSTRUCT ps;
static HPEN hpenRed; // хендл пера должен сохраняться в промежутке
// между обработкой сообщений
switch (uMsg) {case WM_CREATE:
...
hpenRed= CreatePen (PS_SOLID, 0, RGB (255,0,0));
break;
case WM_PAINT:
BeginPaint (hWnd, &ps);
SelectPen (ps.hdc, hpenRed);
... // здесь мы используем выбранный объект
// теоретически, объект сохраняется и после уничтожения контекста -
// так что мы можем не заменять его на стандартный; правда так делать
// не рекомендуется[4]
EndPaint (hWnd, &ps);
break;
case WM_DESTROY:
DeletePen (hpenRed);
...
break;
...
default:
return DefWindowProc (hwnd, uMsg, wParam, lParam);}
return 0L;}
Довольно часто, если объект создается на все время жизни окна, вместо статической переменной используются связанные с окном данные: либо размещенные в структуре окна (см. функции GetWindowLong, SetWindowLong), либо списки свойств окна (Property) (об этом см. методы связывания данных с окном).
Отображение основных графических объектов
В процессе отображения основных графических примитивов (например, прямых линий, прямоугольников и пр.) участвует большое количество атрибутов контекста устройства, влияющих на видимый результат. Условно можно выделить отображение отдельных пикселей, рисование линий и рисование заполненных фигур.
При отображении отдельных пикселей атрибуты контекста устройства практически не используются — вы просто указываете цвет и положение пикселя, который хотите отобразить. На результат влияют только область отображения и характеристики устройства.
При рисовании линий на результат начинает влиять множество других атрибутов, определяющих вид отображаемой линии и то, каким образом осуществляется это отображение.
При рисовании заполненных фигур сам процесс рисования можно представить в виде двух фаз — рисование контура и заполнение внутренней области. Если рисование контура осуществляется также, как и рисование линий, то на заполнение фона начинают влиять дополнительные атрибуты.
Задание цвета
При отображении любых графических объектов необходимо задавать их цвета. Сложности задания цвета начинаются из–за того, что различная аппаратура может воспроизводить только некоторое конечное число цветов. Одновременно вам может понадобиться осуществлять вывод сразу на несколько устройств, возможности которых по отображению цвета существенно различаются. Например — любой текстовой или графический редактор может взаимодействовать как с дисплеем, способным воспроизводить от нескольких цветов до миллионов цветов и принтером, который часто является монохромным.
Для обеспечения универсальности в GDI принято, что цвет обозначается с помощью 24х битового параметра типа COLORREF. Этот параметр реально соответствует одному двойному слову, самый старший байт которого не используется. Младшие три байта задают интенсивности трех основных компонент цвета — красной, зеленой и синей. Каждая компонента задается своим байтом, так что предельный диапазон ее изменения лежит от 0 до 255. Это позволяет задавать до 16 777 216 различных цветов.
Для удобного задания параметра типа COLORRREF используется специальный макрос RGB, который из трех по отдельности указанных компонент формирует одно двойное слово.
COLORREF RGB (byRed, byGreen, byBlue);
Конечно, Windows не всегда может использовать 24х битовый цвет непосредственно, так как аппаратура может поддерживать от 2 цветов (монохроматические устройства) до более чем 16 млн. цветов (режимы TrueColor и HighColor для современных видеоадаптеров). Вместо отображения указанного вами цвета, GDI будет подбирать ближайший цвет, который в состоянии воспроизвести данная аппаратура в данном режиме; такой цвет называется чистый (pure color).
Внимание! на монохромных устройствах существует только два чистых цвета: черный и белый. Поэтому все цвета, имеющие сумму яркостей трех составляющих менее 381, представляются черными, а больше — белыми. Это может привести к неразличимости образа, если, например, вы по черному фону рисуете красную линию, или по белому — светло–циановую.
В некоторых случаях GDI может использовать вместо чистого цвета смесь точек разных цветов, наиболее точно передающих желаемый цвет. Такой цвет называется смешанным (dithered color). Однако такая операция выполняется далеко не всегда. Так при выводе текста цвет символов будет чистым цветом; аналогично линии (за небольшим исключением) рисуются тоже чистым цветом.
Работа с отдельными пикселями
Рисование по отдельным точкам крайне редко используется в Windows, так как скорость осуществления подобной операции является исключительно низкой. Во всех случаях рекомендуется найти какой–либо другой способ отображения, а не рисовать по отдельным точкам.
Собственно, для рисования точек существуют всего две функции:
СOLORREF GetPixel (hDC, nX, nY);
COLORREF SetPixel (hDC, nX, nY, crColor);
Координаты nX и nY являются логическими координатами, определяющими позицию пикселя, цвет которого мы узнаем или изменяем. Независимо от принятой системы координат отображается один пиксель, даже если его размер соответствует нескольким логическим единицам.
Кроме того, задаваемый цвет пикселя автоматически преобразуется к ближайшему чистому цвету, который может быть воспроизведен аппаратурой.
Если используемый пиксель находится вне области рисования, то обе функции возвращают -1, а если пиксель находится внутри области рисования, то возвращаемое значение соответствует текущему цвету пикселя. Для функции SetPixel оно может отличаться от заданного crColor, указывая примененный чистый цвет (если ваша аппаратура не может отобразить требуемый цвет). Этот прием иногда используется для проверки возможностей устройства, хотя эффективнее анализировать возможности устройства с помощью функции GetDeviceCaps.
Рисование линий
В процессе рисования линии участвует сразу несколько атрибутов GDI (помимо атрибутов, выбирающих систему координат). Перед рисованием нужной вам линии вы должны настроить контекст устройства в соответствии с выполняемой операцией, установив нужные значения атрибутов. Коротко назначение этих атрибутов следующее:
Текущее перо (Pen).
Определяет вид проводимой линии — толщину, цвет, стиль (сплошная или прерывистая). Перо является объектом GDI, поэтому при его использовании необходимо соблюдать правила работы с объектами GDI.
Текущая позиция пера (Pen Position).
Определяет точку из которой начнется рисование следующего отрезка линии. После рисования этого отрезка текущая точка перемещается в конец отрезка.
Цвет фона (Background Color).
Заполняет промежутки между штрихами прерывистой линии (а также фон под символами выводимого текста или промежутки между линиями штрихованных кистей — об этом см. в обсуждении рисования заполненных фигур и обсуждении вывода текста).
Режим заполнения фона (Background Mode).
Определяет, надо–ли закрашивать промежутки между штрихами прерывистой линии цветом фона или оставить фон без изменений. Аналогично — при выводе текста.
Режим рисования (Drawing Mode).
На практике рисование на контексте устройства рассматривается как сложный процесс переноса подготовленного вами изображения (например, образа рисуемой линии) на уже существующее в этом месте изображение. В процессе переноса выполняется некоторая операция, в результате которой получается результирующее изображение. Обычно такая операция — простое копирование нового изображения на старое. Однако вы можете задать необходимую операцию, которую надо выполнять в процессе переноса изображения.
Направление рисования дуг эллипсов (Arc Direction).
Влияет только на результат рисования дуги эллипса (функции Arc, Pie и Chord). По умолчанию дуги рисуются против хода часовой стрелки, но вы можете изменить это направление. Пользоваться этой возможностью, вообще говоря, не рекомендуется, так как применение этой функции ограничено — в Win32 API в расширенном режиме задания координат (см. SetGraphicsMode, GM_ADVANCED) дуги рисуются всегда против хода часовой стрелки и изменить это направление нельзя.
Собственно для рисования прямых линий необходимо всего две функции:
DWORD MoveTo (hDC, nX, nY); 0
BOOL MoveToEx (hDC, nX, nY, lpPoint);
BOOL LineTo (hDC, nX, nY);
Функция MoveToEx перемещает текущую точку пера в указанное место, не выполняя рисования. Последующий вызов функции LineTo осуществит рисование от текущей точки до указанной конечной отрезка. Нарисованный отрезок линии не включает в себя последнюю точку! Это сделано для удобства рисования ломанных линий из нескольких отрезков — в этом случае любая точка ломанной будет нарисована только один раз. Если вам необходимо нарисовать отрезок обязательно включая последнюю точку — продлите отрезок на один пиксель дальше (не забывайте, что вы задаете логические координаты, а удлинить отрезок надо на одну единицу устройства!). Если это сделать трудно (например отрезок наклонен под каким–то углом) то можно дополнительно нарисовать крохотный отрезок длиной 1 пиксель в любую сторону.
В случае Windows API часто применялась функция MoveTo, а не MoveToEx; Функция MoveTo возвращала информацию о прежнем положении текущей точки, упакованную в двойное слово. В Win32 API такая упаковка невозможна — каждая координата уже представляет собой двойное слово — поэтому функция MoveTo больше не поддерживается.
Возможен случай, когда надо нарисовать ломаную линию, состоящую из нескольких отрезков. Конечно, это легко сделать с помощью серии вызовов функции LineTo, но иногда может быть проще воспользоваться другой функцией:
BOOL Polyline (hDC, lpPoints, nCount);
Эта функция рисует ломаную линию, начальная, конечная и все точки перегиба которой заданы в массиве структур типа POINT (параметр lpPoints), содержащем nCount точек. Линия рисуется, начиная от первой точки и далее, соединяя последовательно отрезками все точки, вплоть до последней. Ломаная линия может быть незамкнутой, GDI не проводит замыкающего отрезка от последней точки к первой.
Еще одна функция GDI предназначена для рисования дуг:
BOOL Arc (hDC, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);
Для задания рисуемой дуги эллипса вы должны задать сначала сам эллипс, дуга которого будет изображаться, а затем задать начальную и конечную точки дуги. Эллипс задается описывающим его прямоугольником (причем нижняя и правая границы не включаются) — параметры xLeft, yTop, xRight и yBottom, а начальная и конечная точки задаются параметрами xStart, yStart и xEnd, yEnd.
Рисунок 4. Рисование дуг эллипсов
Начальная и конечная точки могут не лежать на эллипсе. В этом случае GDI вычисляет точку пересечения эллипса с отрезком, соединяющим центр эллипса с указанной вами точкой. Дуга будет рисоваться между точками пересечения отрезков, направленных от центра эллипса к начальной и конечной точкам дуги в направлении против хода часовой стрелки (см. раздел «Направление рисования дуг»).
В случае применения Win32 API вы можете использовать еще несколько функций для рисования линий. Так две новых функции предназначены для рисования дуг:
BOOL ArcTo (hDC, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd); 1
BOOL AngleArc (hDC, nX, nY, dwRadius, eStartAngle, eSweepAngle); 1
Функция ArcTo аналогична функции Arc, за исключением того, что она сначала рисует отрезок от текущей точки до начальной точки дуги, затем рисует саму дугу и перемещает текущую точку в конечную точку нарисованной дуги. Функция AngleArc рисует дугу окружности, не эллипса. Для нее надо задать центр окружности (параметры nX, nY), радиус (dwRadius), начальный угол (eStartAngle) и угловую величину рисуемой дуги (eSweepAngle), в градусах. GDI не проверяет угловую величину дуги, она может превышать 3600.
Еще пара новых функций предназначена для рисования ломаных линий:
BOOL PolylineTo (hDC, lpPoints, nCount); 1
BOOL PolyPolyline (hDC, lpPoints, lpuCounts, nPolyCount); 1
Функция PolylineTo отличается от Polyline тем, что начинает рисование с отрезка от текущей точки до первой точки, указанной в массиве, а после прорисовки всех отрезков перемещает текущую точку в последнюю точку массива. Функция PolyPolyline может за одну операцию отобразить несколько ломаных линий. Координаты всех точек всех линий задаются массивом структур POINT (параметр lpPoints), число точек в каждой ломаной линии задается массивом целых чисел lpuCounts, а число ломаных, рисуемых этой функцией — параметром nPolyCount.
Кроме того в Win32 API существуют функции для рисования кривых Безье:
BOOL PolyBezier (hDC, lpPoints, cPoints); 1
BOOL PolyBezierTo (hDC, lpPoints, cPoints); 1
Функция PolyBezier рисует линию состоящую из сегментов кривых Безье. Для задания каждого сегмента требуется указать четыре точки: начальную, две направляющие и конечная. Так как рисуется линия из нескольких сегментов, то конечная точка одного сегмента является в свою очередь начальной точкой другого сегмента. Таким образом кривая будет определяться набором из Npt = 1 + Nsegments*3; здесь Npt — число точек для задания кривой, Nsegments — число сегментов в кривой.
Используя функцию PolyBezier вы должны задать массив точек, определяющих начальную точку, две направляющие и конечную точку для первого сегмента кривой, затем по две направляющих и одной конечной для каждого последующего сегмента. Текущая точка при этом не используется.
Функция PolyBezierTo отличается тем, что текущая точка используется в качестве начальной точки первого сегмента. В этом случае в массиве должно содержаться на одну точку меньше — только по две направляющих и одной конечной для каждого сегмента кривой. И, кроме того, после рисования кривой текущая точка будет перемещена в конечную точку последнего нарисованного сегмента.
Последняя рассматриваемая функция предназначена для рисования целого набора из прямых отрезков и кривых Безье за одну операцию:
BOOL PolyDraw (hDC, lpPoints, lpbyTypes, cCount); 1
Массивы структур POINT (lpPoints) и байтов (lpbyTypes) имеют одинаковое количество элементов; каждый элемент массива lpbyTypes определяет тип рисуемой линии из текущей точки до точки, задаваемой соответствующим элементом массива lpPoints. Допустимы следующие значения для типов линий:
PT_MOVETO | линия не рисуется, текущая точка перемещается в указанную позицию |
PT_LINETO | рисуется отрезок от текущей точки, до указанной; текущая точка перемещается в конечную точку отрезка. |
PT_BEZIERTO | рисуется кривая Безье; для задания кривой надо определить подряд три точки типа PT_LINETO: первые две точки — направляющие, последняя — конечная; текущая точка будет начальная точка кривой, а после рисования текущая точка переместится в конец нарисованного сегмента. |
PT_CLOSEFIGURE | этот флаг может быть объединен со значениями PT_LINETO или PT_BEZIERTO; при его указании фигура будет замкнута проведением отрезка от последней точки нарисованного сегмента до первой точки фигуры (точки типа PT_MOVETO или точки, заданной функцией MoveTo перед вызовом PolyDraw. |
Перо
Для проведения линий используется специальный объект GDI, который определяет вид проводимой линии, ее цвет и толщину. По аналогии с обычным рисованием на бумаге такой объект получил название перо (pen). Перо является объектом GDI, следовательно к нему применяются все правила работы с объектами GDI, рассмотренные ранее.
GDI предоставляет возможность использовать одно из трех стандартных перьев или создавать собственные перья, имеющие нужные свойства. Функция GetStockObject позволяет получить стандартное перо; они отличаются только цветом, проводимая ими линия всегда сплошная, шириной 1 единицу устройства (пиксель). Вместо функции GetStockObject можно использовать макрос GetStockPen из windowsx.h. Как и все стандартные объекты GDI эти перья нельзя уничтожать.
HANDLE GetStockObject (nIndex);
HPEN GetStockPen (nIndex); 2
BLACK_PEN | — черное перо |
WHITE_PEN | — белое перо |
NULL_PEN | — прозрачное перо |
Куда больше возможностей предоставляют функции, создающие перья. Две из них — CreatePen и CreatePenIndirect отличаются только способом передачи параметров. Функция CreatePen получает все характеристики создаваемого пера в виде отдельных параметров, а функция CreatePenIndirect использует структуру LOGPEN, описывающую создаваемое перо. Функционально обе функции тождественны. Эта же структура используется функцией GetObject для получения информации о пере.
HPEN CreatePen (nPenStyle, nWidth, crColor);
HPEN CreatePenIndirect (lpLogPen);
typedef struct tagLOGPEN {
WORD lopnStyle; // стиль пера
POINT lopnWidth; // ширина линии
COLORREF lopnColor; // цвет пера
} LOGPEN;
Стиль пера может быть:
PS_SOLID | сплошная тонкая или толстая линия | |
PS_DASH | штриховая тонкая линия | |
PS_DOT | пунктирная тонкая линия | |
PS_DASHDOT | штрих–пунктирная тонкая линия | |
PS_DASHDOTDOT | штрих–точка–точка тонкая линия | |
PS_NULL | (прозрачный) | |
PS_INSIDEFRAME | сплошная тонкая или толстая линия |
Ширина пера задается в логических единицах, причем в случае функции CreatePenIndirect для задания толщины используется структура типа POINT, в которой используется только поле x, а поле y — нет. Ширина пера задается в логических единицах. Так как логическая единица в общем случае может не совпадать с физической, то для создания тонких перьев (ширина которых равна 1 пикселю или одной строке растра) надо указать требуемую ширину равной 0 — тогда будет создано перо шириной 1 пиксель. Все прерывистые линии (PS_DOT, PS_DASH, PS_DASHDOT, PS_DASHDOTDOT), шириной больше физической 1 воспроизводятся как PS_SOLID.
Для созданного пера будет назначен ближайший чистый цвет, который может быть воспроизведен данной аппаратурой. В реальных условиях он может отличаться от цвета, заданного вами.
Широкая линия проводится таким образом, что бы ее центр соединял указанные точки. Перо применяется не только для рисования линий, но и для рисования контура фигур. Если при этом используется широкая линия, то может быть удобно, что бы вся линия лежала внутри контура фигуры. Для таких целей предназначен стиль линии PS_INSIDEFRAME. Линии стиля PS_INSIDEFRAME отличаются еще одной особенностью: если ее ширина больше или равна 2 единицам устройства, то эта линия может рисоваться не чистым цветом, а смешанным.
При использовании Win32 API вы можете воспользоваться еще одной функцией для создания перьев:
HPEN ExtCreatePen (dwPenStyle, dwWidth, lpLogBrush, dwStyleCount, lpdwStyle); 1
typedef struct tagLOGBRUSH {
UINT lbStyle;
COLORREF lbColor;
LONG lbHatch;
} LOGBRUSH;
Перья, создаваемые этой функцией делятся на два типа: косметические (cosmetic) и геометрические (geometric). Для описания характеристик пера используется структура LOGBRUSH, обычно применяемая для описания кистей (см. раздел “Кисть”).
Косметические перья создаются, если в параметре dwPenStyle установлен стиль PS_COSMETIC. Они соответствуют тонким сплошным или прерывистым линиям (штриховым, штрих–пунктирным и пунктирным); всегда имеют ширину 1 пиксель (параметр dwWidth должен быть задан 1) и рисуются чистым цветом (цвет задается полем lbColor структуры LOGBRUSH; поле lbStyle обязательно должно быть BS_SOLID, а поле lbHatch не используется). Помимо стиля PS_COSMETIC надо задать один из дополнительных стилей:
Стили PS_SOLID, PS_DASH, PS_DOT, PS_DASHDOT, PS_DASHDOTDOT, PS_NULL, PS_INSIDEFARME в основном соответствуют уже рассмотренным стилям обычных перьев. (Для косметических ширина всегда равна 1 пикселю).
Стиль PS_ALTERNATE означает, что точки линии будут рисоваться через одну.
Если используется стиль PS_USERSTYLE, то два последних параметра функции ExtCreatePen задают стиль прерывистой линии: параметр lpdwStyle указывает на массив целых чисел, задающих длину штрихов и промежутков между ними; параметр dwStyleCount число записей в этом массиве. Внимание! длина штрихов и промежутков задается не в логических единицах, а в единицах устройства, непосредственно в процессе рисования линии.
Другой вид перьев — геометрические — создаются, если в параметре dwPenStyle указан стиль PS_GEOMETRIC. Такие перья могут быть произвольной толщины и цвета, включая узорчатые перья (рисунок на которых задается указанной кистью). Кроме того, при создании геометрических кистей есть возможность задать дополнительные стили. Так, в параметре dwPenStyle вы должны определить стиль PS_GEOMETRIC, стиль линии, стиль оформления окончаний и стиль сопряжения линий.
Стиль линии может быть PS_SOLID, PS_DASH, PS_DOT, PS_DASHDOT, PS_DASHDOTDOT, PS_NULL, PS_INSIDEFARME или PS_USERSTYLE. Стиль PS_ALTERNATE для геометрических перьев не поддерживается.
Стиль оформления окончаний играет роль для широких линий и может быть PS_ENDCAP_ROUND (конец линии полукруглый), PS_ENDCAP_SQUARE (в виде угла) и PS_ENDCAP_FLAT (прямой).
Стиль сопряжения линий влияет, если несколько последовательно проводимых отрезков сопрягаются под небольшим углом. Он может быть PS_JOIN_BEVEL (линии стыкуются под углом), PS_JOIN_ROUND (сопряжение скругляется) и PS_JOIN_MITER (сопряжение спрямляется). Если используется перо со стилем PS_JOIN_MITER, то дополнительно можно задать предельную величину спрямления сопрягаемых линий с помощью функции
BOOL SetMiterLimit (hDC, eNewLimit, lpeOldLimit); 1
BOOL GetMiterLimit (hDC, lpeOldLimit); 1
Величина спрямления задается относительно ширины линии.
Ширина геометрической линии может быть любой, она задается параметром dwWidth в логических единицах. Параметр lpLogBrush описывает кисть, которая задает цвет и узор линии; кисть может быть однотонной, смешанной, штрихованной или даже созданной по образцу — зависимому от устройства битмапу. Использование независимых от устройства битмапов в качестве образцов при описании пера недопустимо. О кистях и битмапах см. разделы “Кисть” и “Растровые изображения”.
Как и в случае косметических перьев, параметры dwStyleCount и lpdwStyle определяют стиль штриховки, если задан стиль линии PS_USERSTYLE, иначе они не используются. Однако в отличие от косметического пера, ширина штрихов и промежутков задается в логических единицах.
После использования созданного пера вы должны его уничтожить, причем перед этим он должен быть освобожден из контекста устройства (или весь контекст должен быть освобожден). Перо, как и все объекты GDI, уничтожается с помощью функции:
BOOL DeleteObject (hGDIobj);
BOOL DeletePen (hPen);
Текущая позиция пера
Следующий атрибут контекста устройства, используемый при рисовании линии, называется текущей позицией пера. Этот атрибут используется очень небольшим числом функций, поэтому мы вкратце рассмотрим их всех, даже если они не связаны с рисованием линий.
С первыми тремя функциями мы уже познакомились — они–то и используются для рисования линий.
DWORD MoveTo (hDC, nX, nY); 0
BOOL MoveToEx (hDC, nX, nY, lpPoint);
BOOL LineTo (hDC, nX, nY);
Помимо них с текущей позицией пера имеют дело функции GetCurrentPosition и GetCurrentPositionEx а также, в некоторых случаях, функции TextOut и ExtTextOut.
DWORD GetCurrentPosition (hDC); 0
BOOL GetCurrentPositionEx (hDC, lpPoint);
Эти функции возвращают позицию пера. Как обычно — GetCurrentPosition возвращает ее в двойном слове и применятся только в 16ти разрядном Windows API, тогда как функция GetCurrentPositionEx возвращает результат в структуре типа POINT и может применяться как в Windows API, так и в Win32 API.
Две другие функции — TextOut и ExtTextOut обычно не используют текущую позицию пера, вы должны им отдельно указывать позицию для вывода текста. Однако, иногда может быть удобно перейти в специальный режим привязки текста TA_UPDATECP (см. функцию SetTextAlign), при котором текст будет выводиться от текущей позиции, а после вывода текста текущая позиция переместится в конец выведенного фрагмента.
Цвет фона и режим заполнения фона
Еще один используемый атрибут контекста устройства — цвет фона. Это не тот цвет, которым закрашивается фон окна, хотя он может и совпадать с ним. Фон окна закрашивается не текущим цветом фона, а кистью, назначенной окну при регистрации класса. Цвет фона и режим заполнения фона используются:
функциями для рисования линий — для заполнения промежутков между штрихами прерывистой линии
функциями вывода текста — для заполнения пространства под символами текста.
функциями, применяющими кисть — для заполнения фона между линиями штрихованных кистей.
Рисунок 5. Рисование прерывистых линий в разных режимах заполнения фона.
Для задания цвета фона и для выяснения текущего цвета вы можете воспользоваться функциями
COLORREF SetBkColor (hDC, crColor);
COLORREF GetBkColor (hDC);
Функции возвращают используемый ранее цвет фона. Аналогично перу, GDI будет использовать ближайший чистый цвет в качестве цвета фона.
В некоторых случаях бывает удобно проводить пунктирные и штрих–пунктирные линии так, что бы в промежутках был виден прежний фон (прежнее изображение), а не закрашивать их цветом фона. Для этого вы должны изменить режим заполнения фона. Для это предназначены функции:
int SetBkMode (hDC, nBkMode);
int GetBkMode (hDC);
GDI поддерживает два разных режима заполнения фона; один из них называется OPAQUE — это режим по умолчанию. В режиме OPAQUE промежутки заполняются текущим цветом фона, а в другом режиме, называемом TRANSPARENT, фон в промежутках не изменяется.
Режим рисования
Атрибут контекста устройства, называемый режим рисования, влияет на сам процесс рисования. Дело в том, что рисование на устройстве легко может быть представлено как операции над битовыми последовательностями, содержащими те или иные данные. Пока мы говорили о рисовании линии, подразумевая простейший перенос битового образа рисуемого объекта (линии) на другой битовый образ (уже имеющееся изображение).
Атрибут режим рисования задает правила, по которым GDI переносит информацию из одной битовой последовательности на другую. Так, помимо самого очевидного копирования возможны операции инверсии как уже имеющегося изображения, так и рисуемого, объединение с помощью различных битовых операций И (and), ИЛИ (or), ИСКЛЮЧАЮЩЕЕ ИЛИ (xor).
При рассмотрении процесса переноса изображения мы будем исходить из предположения монохромного устройства вывода, так как анализ процесса рисования на цветном устройстве выглядит громоздко. Говоря о монохромном устройстве мы будем обозначать светлую точку будем битом со значением 1, а для темную точку — 0.
При рисовании мы можем условно рассматривать три разные битовые последовательности:
исходное изображение, содержащее рисуемый объект, в документации называется pen (почему–то считается, что рисуется именно линия текущим карандашом — отсюда название — но, вообще говоря, режим рисования применяется для всех операций вывода графических примитивов — линий, эллипсов, многоугольников, текста и пр.);
контекст устройства, содержащий нарисованный ранее образ (хотя бы просто закрашенный фон), в документации называется destination;
результат — то изображение, которое будет находиться на контексте устройства после рисования;
Применительно к этим трем битовым последовательностям говорят о двоичной растровой операции (binary raster operation, ROP2), так как в формировании результата участвуют две исходных битовых последовательности.
Режим рисования задает операцию над битами обеих последовательностей, выполняемую в процессе переноса изображения. Всего возможно 16 различных режимов рисования:
Перо (Pen) P | 1 1 0 0 | Выполняемая | Режим рисования |
Имеющееся изображение (Destination) D | 1 0 1 0 | операция | |
0 0 0 0 | 0 | 0 R2_BLACK | |
0 0 0 1 | ~ (P|D) | 1 R2_NOTMERGEPEN | |
0 0 1 0 | (~P)&D | 2 R2_MASKNOTPEN | |
0 0 1 1 | ~P | 3 R2_NOTCOPYPEN | |
0 1 0 0 | P& (~D) | 4 R2_MASKPENNOT | |
0 1 0 1 | ~D | 5 R2_NOT | |
Повторное рисование восстанавливает фон | 0 1 1 0 | P^D | 6 R2_XORPEN |
0 1 1 1 | ~ (P&D) | 7 R2_NOTMASKPEN | |
1 0 0 0 | P&D | 8 R2_MASKPEN | |
1 0 0 1 | ~ (P^D) | 9 R2_NOTXORPEN | |
Прежнее изображение не меняется | 1 0 1 0 | D | 10 R2_NOP |
1 0 1 1 | (~P)|D | 11 R2_MERGENOTPEN | |
Режим рисования по умолчанию | 1 1 0 0 | P | 12 R2_COPYPEN |
1 1 0 1 | P| (~D) | 13 R2_MERGEPENNOT | |
1 1 1 0 | P|D | 14 R2_MERGEPEN | |
1 1 1 1 | 1 | 15 R2_WHITE |
Так, например, по умолчанию используется операция копирования исходного изображения на контекст устройства (называемая R2_COPYPEN), довольно часто применяется операция исключающее или (R2_XORPEN) между пером и существующим изображением на контексте.
Если монохроматические переносимое изображение и уже имеющееся рассматривать как битовые последовательности, то возможны 4 случая:
оба изображения, переносимое (pen) и имеющееся (destination), содержат светлые точки
переносимое изображение (pen) содержит светлую точку, а имеющееся (destination) — темную
переносимое изображение (pen) содержит темную точку, а имеющееся (destination) — светлую
оба изображения, переносимое (pen) и имеющееся (destination), содержат темные точки
Говоря в терминах битов может понадобиться комбинировать 1 с 1, 1 с 0, 0 с 1 и 0 с 0. Эти четыре варианта перечислены во втором столбце таблицы в заголовке. В строчках ниже дается ожидаемый результат во всех четырех случаях.
Например, если в результате комбинирования светлой со светлой должна получиться светлая точка (1 и 1 дает 1), а в остальных случаях — темная (1 и 0, 0 и 1, а также 0 и 0 дают 0), то в таблице эта операция будет обозначена как 1 0 0 0 — R2_MASKPEN.
Вы можете установить желаемый режим рисования, или узнать текущий с помощью функций
int SetROP2 (hDC, nIndex);
int GetROP2 (hDC);
В описании этих функций можно также найти символические имена всех 16 режимов и их краткое описание. Однако, следуя приводимому в документации описанию, найти нужную операцию может быть затруднительно (обычно там не приводятся результаты выполнения операций, только названия и обозначения). В этом случае вы можете получать нужный номер растровой операции из этой таблички, даже вы вообще можете обойтись без символических имен, просто рассматривая битовую последовательность в табличке как номер режима. Этим правилом можно пользоваться, если порядок четырех возможных комбинаций соответствует приведенному в таблице и в примере.
Например, вы хотите выбрать такой режим, что бы изображение получало светлую точку только в том случае, когда и рисуемая точка и точка имеющегося фона имеют темный цвет, или когда точка линии — светлая, а фона — темная; во всех остальных случаях должна получиться темная точка.
Рисунок 6. Получение номера растровой операции.
Направление рисования дуг
Для задания дуги, которую вы хотите нарисовать необходимо определить эллипс, дугу которого вы собираетесь нарисовать, а также начальную и конечную точки дуги. Но вот проблема — от начальной до конечной точки можно провести дугу в двух разных направлениях, так что приходится дополнительно определять, в каком именно направлении дуга будет нарисована. Обычно принято, что дуга рисуется против хода часовой стрелки.
Вы можете при необходимости узнать или изменить направление рисования дуг с помощью функций:
int GetArcDirection (hDC);
int SetArcDirection (hDC, nIndex);
Допустимыми являются AD_COUNTERCLOCKWISE — рисование против хода часовой стрелки (принято по умолчанию) и AD_CLOCKWISE — по ходу часовой стрелки.
Внимание! Эти две функции не работают в случае расширенного режима задания координат (см. функцию SetGraphicsMode 1, GM_ADVANCED) — в этом случае дуги всегда рисуются против хода часовой стрелки.
Дополнительные средства для рисования линий
Windows содержит еще одну функцию, которая применяется для рисования линий. Она предназначена для последовательного вычисления координат точек, принадлежащих линии, и выполнения над этими точками какой-либо операции, определенной пользователем. Фактически эта функция для каждой точки рисуемой линии вызывает указанную пользователем процедуру. Любопытно, что сама эта функция никак не взаимодействует с контекстом устройства, она выполняет только математические операции, вычисляя все промежуточные точки линии.
void LineDDA (nXStart, nYStart, nXEnd, nYEnd, lpfnDdaPrc, lParam);
nXStart, nYStart — определяют первую точку линии
nXEnd, nYEnd — определяют последнюю точку линии
lpfnDdaPrc — указатель на процедуру типа LINEDDAPROC
lParam — данные, передаваемые пользователем процедуре LINEDDAPROC
Вызываемая процедура LINEDDAPROC имеет следующий вид:
void CALLBACK LineDDAproc (nX, nY, lParam) {
// ...}
Процедура LineDDAproc получает координаты точки, которую надо нарисовать и данные, переданные пользователем.
Координаты первой и последней точек задаются в произвольных единицах, так как их использование определяется не процедурой LineDDA, а процедурой LineDDAproc, разрабатываемой вами. Какие координаты вам удобнее — такие и используйте. Адрес процедуры lpfnDdaPrc в случае Windows API является адресом, возвращаемом функцией MakeProcInstance, но не адресом самой процедуры (об этом подробнее — в разделе «диспетчер памяти»). Данные, передаваемые пользователем, (lParam) являются двойным словом. В документации утверждается, что это дальний указатель на данные, хотя это некорректное утверждение. Часто параметр в виде двойного слова используется для задания адреса каких–либо данных (особенно, если данные занимают больше двух слов), однако реально там может удерживаться произвольное 32х разрядное число. Более того, в прототипе функции он описан именно как long.
Рисование заполненных фигур
Следующая группа функций предназначена для отображения заполненных фигур. К таким фигурам относятся прямоугольники, прямоугольники со скругленными краями, эллипсы, сектора, дуги, стянутые хордой и многоугольники. Условно можно представить себе рисование заполненных фигур как процесс, состоящий из двух этапов — рисование контура текущим пером и заполнение фона. При рисовании контура справедливы все замечания, сделанные при обсуждении рисования линий. Если вам не надо рисовать контур, то выберите в контекст устройства прозрачный карандаш (функция GetStockObject или макрос GetStockPen, NULL_PEN). При заполнении фона рисуемой фигуры используются как уже рассмотренные атрибуты GDI, так и несколько новых. Естественно, что перед рисованием нужной вам фигуры вы должны установить требуемые значения атрибутов контекста устройства.
Кисть (Brush).
Кисть используется как для закраски внутренней области замкнутых фигур, так и для закраски внутренней области окна. Если фон фигуры заполнять не надо, то установите прозрачную кисть. Фактически кисть представляет собой маленький, 8x8 пиксель битмап, который многократно повторяется при заполнении указанной области. Кисть может быть как однотонной (все точки кисти имеют одинаковый цвет), так и штрихованной или узорчатой. Для штрихованных кистей определяется только цвет штрихов; цвет промежутков между штрихами определяется непосредственно при закраске области.
Цвет фона (Background Color).
При закраске областей штрихованными кистями, кисть определяет только лишь цвет штрихов, а промежутки между штрихами заполняются текущим цветом фона.
Режим заполнения фона (Background Mode).
Определяет, надо–ли закрашивать промежутки между линиями штрихованных кистей цветом фона или оставить фон без изменений.
Режим рисования (Drawing Mode).
Определяет операцию, выполняемую в процессе переноса формируемого изображения на контекст устройства.
Направление рисования эллипсов (Arc Direction).
Влияет на способ задания начальной и конечной точек дуг, образующих сектора или дуги, стянутые хордой.
Режим заполнения многоугольников (Polygon Filling Mode).
При рисовании заполненных многоугольников вы можете задать многоугольник такой формы, что он будет содержать накладывающиеся друг на друга элементы. GDI предлагает два разных алгоритма, вычисляющих конфигурацию закрашиваемой области. С помощью данного атрибута вы можете выбрать более подходящий вам алгоритм.
Большинство перечисленных здесь атрибутов GDI уже рассмотрено в разделе «Рисование линий», так что повторно обсуждаться они не будут. Ниже отдельно вынесено обсуждение двух новых атрибутов: кисть и режим заполнения многоугольников.
Функции для рисования заполненных фигур
Среди всех функций, рисующих заполненные объекты, можно условно выделить группу функций, для выполнения которых вы должны задать описывающий рисуемый объект прямоугольник. К таким функциям относятся функции по рисованию прямоугольника (Rectangle), прямоугольника со скругленными краями (RoundRect), эллипса (Ellipse), а также функции для рисования сектора (Pie) и дуги, стянутой хордой (Chord). Все эти функции требуют задания либо непосредственно рисуемого прямоугольника, либо прямоугольника, в который будет вписан рисуемый объект (прямоугольник со скругленными углами, весь рисуемый эллипс или эллипс, дуга которого используется для рисования сектора или стягивается хордой).
При задании описывающего прямоугольника необходимо учитывать несколько нюансов:
Во–первых, по умолчанию нижняя и правая границы описывающего прямоугольника не включаются в рисуемый объект, однако в случае Win32 API и установленного расширенного режима границы описывающего прямоугольника полностью включаются в рисуемый объект (функция SetGraphicsMode, GM_ADVANCED, дополнительно см. раздел «Глобальные системы координат GDI (Win32 API)»).
Во–вторых, рисуемый объект может выходить за рамки описывающего прямоугольника, если для обведения контура используется перо, ширина которого превышает одну единицу устройства и это перо не стиля PS_INSIDEFRAME. Линия стиля PS_INSIDEFRAME всегда будет находиться внутри описывающего прямоугольника.
В–третьих, ориентация сторон описывающего прямоугольника всегда параллельна осям координат и, соответственно, предусмотренные функции не могут рисовать наклонных прямоугольников и эллипсов, что является существенным неудобством. Единственное возможность — при использовании Win32 API перейти в расширенный режим и наклонить сами оси координат (см. раздел «Глобальные системы координат GDI (Win32 API)»).
Обзор функций мы начнем с рисования прямоугольников и эллипсов. Сделать это можно с помощью трех разных функций.
BOOL Rectangle (hDC, xLeft, yTop, xRight, yBottom);
BOOL Ellipse (hDC, xLeft, yTop, xRight, yBottom);
BOOL RoundRect (hDC, xLeft, yTop, xRight, yBottom, xRound, yRound);
Функция Rectangle рисует прямоугольник с прямыми углами, а функция RoundRect скругляет эти углы, проводя небольшие дуги. Эти функции используют описывающий прямоугольник, координаты которого задаются в качестве параметров, причем для функций Rectangle и RoundRect этот прямоугольник задает непосредственно рисуемый объект, а для функции Ellipse он задает описывающий прямоугольник. Функция RoundRect дополнительно требует задания величины эллипсов, которыми она будет скруглять углы. При этом надо учитывать, что задаются размеры описывающего скругляющий эллипс прямоугольника, а не его радиусы. То есть в каждом углу скругляющая дуга будет занимать только половину от указанных вами значений.
Рисунок 7. Задание величины скругляющих эллипсов для функции RoundRect.
Теперь осталось рассмотреть пару функция для рисования сектора (Pie), то есть дуги и области, ограниченной двумя радиусами и для рисования дуги, стянутой хордой (Chord).
Рисунок 8. Результат выполнения функций Pie (слева) и Chord (справа).
BOOL Pie (hDC, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);
BOOL Chord (hDC, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);
При использовании этих функций надо помнить о направлении рисования дуги (в Windows API вы можете задавать это направление, а в Win32 API в расширенном графическом режиме оно всегда против часовой стрелки), о задании начальной и конечной точек дуги и об особенностях задания ограничивающего прямоугольника.
Еще две функции, использующиеся для рисования ограниченных фигур, позволяют рисовать многоугольники.
BOOL Polygon (hDC, lpPoints, nCount);
BOOL PolyPolygon (hDC, lpPoints, lpCounts, nPolyCount);
Функция Polygon рисует текущим карандашом ломаную линию, имеющую указанное параметром nCount число точек с координатами, заданными массивом структур типа POINT, который задается параметром lpPoints (аналогично функции PolyLine — см. раздел «Рисование линий»). В отличие от PolyLine, функция Polygon замыкает многоугольник и закрашивает его внутреннюю область текущей кистью. Так как многоугольник может быть достаточно сложным, и некоторые его области могут перекрываться, то GDI предусматривает два различных алгоритма вычисления внутренней, закрашиваемой области. Применяемый алгоритм определяется атрибутом контекста устройства, называемым режим заполнения многоугольников (polygon filling mode) — см. ниже.
Функция PolyPolygon позволяет нарисовать несколько многоугольников за одну операцию. При этом указывается массив, содержащий координаты всех точек всех многоугольников последовательно друг за другом, массив, содержащий числа точек в каждом из многоугольников и число рисуемых многоугольников.
Кисть
В Windows существует специальный объект GDI, используемый для закраски внутренних областей фигур; по аналогии с рисованием на бумаге этот объект получил название кисть (brush). Практически кисть представляет собой небольшую (8x8 пикселей) картинку, которая многократно воспроизводится для закраски указанной области.
Кисть является объектом GDI и, соответственно, при работе с ней надо придерживаться общих правил работы с объектами GDI.
Windows содержит несколько стандартных кистей, для выбора которых можно воспользоваться функцией:
HANDLE GetStockObject (nIndex);
HBRUSH GetStockBrush (nIndex); 2
где параметр nIndex может быть:
BLACK_BRUSH | — кисть черного цвета |
DKGRAY_BRUSH | — темно-серая |
GRAY_BRUSH | — серая |
LTGRAY_BRUSH | — светло-серая |
WHITE_BRUSH | — белая |
HOLLOW_BRUSH | — прозрачная кисть |
NULL_BRUSH | — прозрачная кисть (синоним символа HOLLOW_BRUSH) |
Белая кисть (WHITE_BRUSH) обычно используется для закраски фона окна (при описании класса окна поле WNDCLASS.hbrBackground задается обычно равным хендлу белой кисти).
В принципе, вы можете задать любую другую кисть для закраски внутренней области окна, либо изменив регистрацию класса окна, либо, уже после регистрации и создания окна (окон) этого класса, воспользовавшись функцией:
UINT SetClassWord (hWnd, GCW_HBRBACKGROUND, hbrNewBrush); // Windows 3.x
DWORD SetClassLong (hWnd, GCL_HBRBACKGROUND, hbrNewBrush); // Win32 API
которая изменит кисть, применяемую для закраски фона окна, на другую. При этом надо учитывать, что измениться кисть для всех окон, принадлежащих этому классу, так что вам надо принять специальные меры для перерисовки всех окон этого класса после изменения кисти.
Кисти HOLLOW_BRUSH и NULL_BRUSH представляют один и тот же объект — прозрачную кисть. Вы можете использовать данную кисть для закраски фона замкнутых фигур, если они должны быть представлены только контуром, без заполнения внутренней области.
Если вы хотите создать собственную кисть, то можно воспользоваться одной из следующих функций:
HBRUSH CreateSolidBrush (crColor);
HBRUSH CreateHatchBrush (nStyle, crColor);
HBRUSH CreatePatternBrush (hBitmap);
HBRUSH CreateDIBPatternBrush (hGlobDIB, nColorSpec);
HBRUSH CreateBrushIndirect (lpLogBrush);
Функция CreateSolidBrush позволяет создать однотонную кисть. В качестве параметра указывается цвет создаваемой кисти. В том случае, если система не может воспроизвести указанный цвет чистым, то GDI будет использовать смешивание разноцветных пикселей для получения наиболее точного приближения к заданному цвету.
Так, например, в 16ти цветном режиме стандартные кисти LTGRAY_BRUSH, GRAY_BRUSH и BLACK_BRUSH могут быть представлены оттенками серого цвета, а кисть DKGRAY_BRUSH будет представлена смесью точек серого и черного цветов.
Функция CreateHatchBrush создает штрихованную кисть. Вы указываете два параметра — тип штриховки (nStyle) и цвет штриха (crColor), для закраски фона между штрихами в качестве фона используются атрибуты контекста устройства текущий цвет фона и режим заполнения фона.
Рисунок 9. Стили штрихованных кистей
Параметр crColor задает цвет штриховки. GDI будет применять ближайший чистый цвет для рисования линий штриховки. Для задания цвета фона надо воспользоваться функциями
COLORREF SetBkColor (hDC, crColor);
int SetBkMode (hDC, nMode);
С помощью функций CreatePatternBrush и CreateDIBPatternBrush вы можете описать кисть, определяемую образцом (pattern) — рисунком размером 8x8 пикселей. Для этого надо предварительно получить битмап размером 8x8 пикселей (или больше) и передать его соответствующей функции. Кисть будет создана исходя по изображению размером 8x8 пикселей, находящемуся в верхнем–левом углу битмапа. Подробнее о получении битмапов — смотри в разделе “Растровые изображения”. Все время, пока существует кисть, вы должны сохранять исходный битмап, по которому эта кисть построена. Один битмап может применяться для создания многих кистей.
Разница между двумя функциями создания кисти по образцу связана с применением разных видов битмапов — зависимых от устройства (DDB — device depended bitmap) и независимых от устройства (DIB — device independed bitmap). Независимый от устройства битмап содержит как данные об изображении, так и данные о применяемых цветах (палитре). При создании кисти на основе DIB требуется два параметра, один из которых указывает образец (независимый от устройства битмап), а другой указывает правила интерпретации логических цветов данного битмапа.
К этим двум функциям создания кисти по образцу надо сделать еще одно замечание: если битмап является монохромным, то тогда GDI будет представлять его не в виде черно–белого изображения, а использовать текущий цвет текста вместо цвета (1) и текущий цвет фона вместо цвета (0). Таким образом картинка оказывается как–бы негативной — точки, кодированные 1, по умолчанию представляются черными (цвет текста), а 0 — белыми (цвет фона).
Функция CreateBrushIndirect является объединением всех рассмотренных функций: в качестве параметра ей передается указатель на структуру типа LOGBRUSH, которая описывает кисть любого типа.
typedef struct tagLOGBRUSH {
UINT lbStyle;
COLORREF lbColor;
int lbHatch;
} LOGBRUSH;
Поле lbStyle определяет стиль кисти. Оно может принимать одно из следующих значений: BS_SOLID, BS_HATCHED, BS_HOLLOW, BS_NULL, BS_PATTERN и BS_DIBPATTERN (что в какой-то мере соответствует функции, применяемой для создания кисти). Использование других полей структуры LOGBRUSH зависит от стиля кисти:
Стиль | Эквивалентная функция, применяемая для создания кисти | Использование параметра lbColor | Использование параметра lbHatch |
BS_HOLLOW, BS_NULL | не используется | не используется | |
BS_SOLID | CreateSolidBrush | цвет кисти | не используется |
BS_HATCHED | CreateHatchBrush | цвет штриховки | стиль штриховки |
BS_PATTERN | CreatePatternBrush | не используется | хендл DDB битмапа (HBITMAP) |
BS_DIBPATTERN | CreateDIBPatternBrush | способ интерпретации логических цветов | хендл блока памяти с DIB битмапом (HGLOBAL) |
Структура LOGBRUSH может использоваться также для выяснения свойств кисти с помощью функции GetObject.
Привязка кисти (brush alignment)
Существует совершенно особый прием при работе с кистями — так называемая привязка кисти. При закраске внутренней области какой–либо фигуры с помощью кисти GDI многократно воспроизводит ее изображение. Однако при этом возникает вопрос: с какого места начинает воспроизводиться изображение кисти? Считается, что GDI по умолчанию повторяет кисть начиная с верхнего левого угла экрана — то есть от точки с координатами устройства (0,0). Это так называемая начальная точка кисти (brush origin).
Рисунок 10. Пояснения к атрибуту начальная точка кисти (brush origin)
Если кисть представлена каким–либо изображением или штриховкой, то все закрашиваемые фрагменты попадают в одну "фазу". В некоторых случаях надо изменять начало отсчета кисти для того, что бы рисунок или штриховка был согласован с закрашиваемой поверхностью. Для этого применяется прием, называемый выравнивание кисти (brush alignment):
POINT pt;
// brush origin устанавливается в координатах устройства,
// а точка по которой мы будем выравнивать обычно определена
// в логических координатах, поэтому требуется преобразование.
pt.x= 0; pt.y= 0; // выбираем логические координаты новой точки
LPtoDP (hDC, &pt, 1); // переводим их в координаты окна
ClientToScreen (hWnd, &pt); // а теперь в координаты экрана
//-если мы применяем систему координат MM_TEXT(по умолчанию),то
// мы можем не использовать функцию LPtoDP;
// - если контекст соответствует иному устройству, чем дисплей,
// то мы не используем функцию ClientToScreen.
// кисть имеет размер 8x8 пикселей, поэтому координаты начальной точки
// лучше задавать в диапазоне 0..7, то есть остаток от деления на 8
pt.x %= 8; pt.y %= 8;
// теперь нам известны координаты устройства нового brush origin
UnrealizeObject (hNewBrush);
SetBrushOrg (hDC, pt.x, pt.y);
// функция UnrealizeObject разрешает назначить для кисти новую начальную
// точку; это назначение произойдет при выборе кисти в контекст устройства,
// причем начальная точка назначается именно контексту, а не кисти.
SelectObject (hDC, hNewBrush);
При выравнивании кисти надо придерживаться нескольких ограничений:
запрещено применять функцию UnrealizeObject ко всем стандартным кистям;
выравнивать можно только кисть, не выбранную в контекст устройства;
Режим заполнения многоугольников
При рисовании многоугольников возникает необходимость в решении достаточно сложной задачи — определении внутренней области многоугольника, которую необходимо закрасить. Задача может существенно осложниться, если учесть возможность рисования нескольких перекрывающихся многоугольников за одну операцию и возможность задания пересекающихся областей одного многоугольника. Типичными примерами являются рисунки пятиконечной звездочки или домика:
Рисунок 11. Примеры многоугольников с перекрывающимися поверхностями.
В такой ситуации поведение GDI будет определяться текущим режимом заполнения многоугольников (polygon filling mode). Вы можете его изменить или узнать с помощью функций
UINT GetPolyFillMode (hDC);
UINT SetPolyFillMode (hDC, nIndex);
допустимы два значения параметра nIndex: ALTERNATE и WINDING. В режиме ALTERNATE GDI закрашивает на каждой строке развертки отрезок между сторонами с нечетным и четным последовательными номерами. Очень упрощенно — область, которая повторно закрашивается сохранит первоначальный вид. В режиме WINDING применяется более сложный алгоритм, который позволяет вычислить и закрасить все внутреннюю область многоугольника. Например, заполнение многоугольника в виде пятиконечной звездочки в различных режимах заполнения многоугольников выглядит так:
Рисунок 12. Заполнение пятиконечной звездочки в различных режимах заполнения многоугольников.
Прямоугольники и регионы
Исторически сложилось так, что прямоугольник является базовой фигурой при работе с графическими устройствами. Значительная часть примитивов GDI требует задания описывающего прямоугольника, окна опять–же имеют форму прямоугольника (не считая возможности использовать эллиптические окна в Windows–95), области окон, нуждающиеся в перерисовке — неверные области — в ранних версиях Windows описывались прямоугольником, растровые изображения — битмапы — имеют форму прямоугольника и так далее. Естественно, что в Windows были включены специальные средства для выполнения математических операций над прямоугольниками и некоторый вспомогательный набор функций, осуществляющий операции закраски, обводки контура, инверсии цвета и прочего в указанной вами прямоугольной области.
По мере развития Windows многие функции прямоугольников были переданы более сложным объектам — регионам (region), которые могут описывать области сложной формы. При этом развился параллельный набор функций, ориентированных на применение регионов вместо прямоугольников.
Функции, ориентированные на работу с прямоугольниками и с регионами достаточно разнородны, относятся к самым разным подсистемам Windows, не только к GDI, смотря по тому, как будет применяться указанный прямоугольник или регион. Большая часть этих функций рассматривается в этом разделе, хотя в других разделах те же самые функции будут рассматриваться дополнительно, с углубленным обсуждением их использования.
Прямоугольники
Рассмотрение операции над прямоугольниками мы начнем с математических функций. Эти функции рассматривают прямоугольник как некую математическую абстракцию, описываемую структурой типа RECT.
typedef struct tagRECT {
int left;
int top;
int right;
int bottom;
} RECT;
Над этой структурой можно производить некоторые операции, как–то инициализировать нулем, задать ее поля, прибавить ко всем полям определенные величины и так далее. Использование этих функций не всегда целесообразно, так как того же эффекта можно достичь более простыми средствами. Так, функция
void SetRect (lpRect, xLeft, yTop, xRight, yBottom);
позволяет заполнить структуру типа RECT указанными значениями, функция
void SetRectEmpty (lpRect);
обнуляет поля структуры RECT, а функция
void CopyRect (lpRectDst, lpRectSrc);
копирует одну структуру RECT в другую. Рассмотренные функции заменяются на более простые конструкции самым тривиальным образом, причем получаемый код оказывается компактнее и существенно быстрее. Еще две функции осуществляют перемещение прямоугольника по координатной плоскости (OffsetRect) и изменение его размеров (InflateRect):
void OffsetRect (lpRect, nDeltaX, nDeltaY);
void InflateRect (lpRect, nDeltaWidth, nDeltaHeight);
void InsetRect (lpRect, nDeltaWidth, nDelatHeight); 2
Макрос InsetRect соответствует вызову функции:
InflateRect (lpRect, - (nDeltaWidth), - (nDeltaHeight)).
Некоторые функции для работы с прямоугольниками, предоставляемые Windows, все же достаточно удобны, что бы их не заменять собственными:
BOOL IsRectEmpty (lpRect);
проверяет, является ли данный прямоугольник пустым, или нет; Функция
BOOL EqualRect (lpRect, lpRect);
проверяет совпадение прямоугольников (имеют ли они одинаковые размеры и положение); а функция
BOOL PtInRect (lpRect, lpPoint);
проверяет, попадает ли указанная точка в заданный прямоугольник. Еще три функции позволяют выполнить простейшие математические операции над прямоугольниками: вычислить пересечение, объединение и исключение.
BOOL IntersectRect (lpRectDst, lpRectSrc1, lpRectSrc2);
BOOL UnionRect (lpRectDst, lpRectSrc1, lpRectSrc2);
BOOL SubtractRect (lpRectDst, lpRectSrc1, lpRectSrc2);
Рисунок 13. Пересечение, объединение и два варианта исключения прямоугольников.
При использовании функции SubtractRect для вычисления области прямоугольника 1, не входящего в прямоугольник 2 надо быть уверенным, что прямоугольник 2 полностью перекрывает прямоугольник 1 по одной из сторон.
Помимо чисто математических операций над прямоугольниками существуют некоторые функции, предназначенные для рисования прямоугольников. К ним относятся функции для инверсии цвета в прямоугольной области, для закраски прямоугольника указанной кистью и для проведения каемки вокруг прямоугольника.
void InvertRect (hDC, lpRect);
инвертирует цвет указанного прямоугольника, выполняя операцию BITWISE NOT над всеми пикселями прямоугольника. Эта функция удобна для выделения какого-либо прямоугольника, так как повторное выполнение этой операции восстанавливает первоначальный вид прямоугольника.
В некоторых случаях бывает удобно просто закрасить указанной кистью необходимую область. Конечно, это можно сделать с помощью функции Rectangle. Однако этот способ не всегда хорош, так как при рисовании прямоугольника он окружается линией, нарисованной текущим карандашом. Этот карандаш, во–первых всегда представлен чистым цветом, а, во–вторых, прямоугольник не всегда надо ограничивать линией (использование прозрачного карандаша приводит к частой смене карандашей). Для этого Windows содержит две дополнительные функции:
int FillRect (hDC, lpRect, hBrush);
int FrameRect (hDC, lpRect, hBrush);
Функция FillRect закрашивает указанный прямоугольник требуемой кистью, а функция FrameRect проводит вокруг указанного прямоугольника каемку опять же кистью (не карандашом). Ширина проводимой каемки 1 пиксель как по горизонтали, так и по вертикали.
Вместо функции FillRect можно иногда применять функцию PatBlt, которая позволяет закрасить фон текущей кистью. Эта функция поддерживается непосредственно драйверами устройств, так что ее применение дает наиболее быстрый исполняемый код. Подробнее о функции PatBlt см. в разделе «Операции передачи образов»
Частный случай — закраска прямоугольной области не кистью, а конкретным цветом. Очевидный способ — создание однотонной кисти и закраска прямоугольника с помощью функции FillRect или Rectangle — во–первых достаточно громоздок и, во–вторых, не гарантирует закраски именно чистым цветом — кисть может оказаться смешанной из точек разных цветов. Наиболее быстрый способ — использовать функцию ExtTextOut, указав ей пустую строку, ограничивающий прямоугольник и необходимость закраски прямоугольника цветом фона (флаг ETO_OPAQUE).
Прямоугольники часто используются для объявления каких–либо областей внутренней области окна неверными, то есть нуждающимися в перерисовке или, наоборот, верными — после прорисовки указанной области. Для этого предназначены две функции
BOOL InvalidateRect (hWnd, lpRect, fEraseBkgnd);
BOOL ValidateRect (hWnd, lpRect);
Если вместо адреса структуры RECT указать NULL, то система будет подразумевать прямоугольник, совпадающий со всей внутренней областью окна.
Регионы
Такое количество функций, предназначенных для работы с прямоугольниками связано с тем, что прямоугольник можно назвать основным примитивом Windows, так как он используется практически повсеместно. Однако применение прямоугольников не всегда эффективно. Например, если прямоугольники используются для объявления неверной области окна, то объединение, скажем, двух небольших неверных прямоугольников в противоположных углах окна приведет к объявлению всей внутренней области нуждающейся в перерисовке. Часто вместо прямоугольников эффективнее использовать области сложной формы и, соответственно, регионы (region) как объекты, описывающие области сложной формы.
Регион является объектом GDI, на него распространяются все правила применения объектов GDI. В Windows описан набор функций, предназначенный для создания регионов, форма которых соответствует основным примитивам GDI, и, кроме того, функцию CombineRgn, которая позволяет из нескольких регионов простой формы построить один регион сложной формы.
Мы можем создавать прямоугольные регионы, прямоугольные со скругленными углами, эллиптические и регионы в виде многоугольников. Для этого предназначены следующие функции:
HRGN CreateRectRgn (xLeft, yTop, xRight, yBottom);
HRGN CreateRectRgnIndirect (lpRect);
HRGN CreateRoundRectRgn (xLeft, yTop, xRight, yBottom, xRound, yRound);
HRGN CreateEllipticRgn (xLeft, yTop, xRight, yBottom);
HRGN CreateEllipticRgnIndirect (lpRect);
HRGN CreatePolygonRgn (lpPoints, nCount, nPolyFillMode);
HRGN CreatePolyPolygonRgn (lpPoints, lpCounts, nPolyCount, nPolyFillMode);
Аргументы этих функций подобны аргументам функций, осуществляющих рисование аналогичных фигур, поэтому рассматривать их здесь не будем. Параметр nPolyFillMode аналогичен соответствующему атрибуту контекста устройства — режиму заполнения многоугольников.
В результате вызова одной из функций Create...Rgn создается специальный объект, описывающий регион, а нам возвращается хендл этого объекта.
Как и всякий объект GDI регион удаляется с помощью функции DeleteObject.
Одной из самых интересных особенностей регионов является возможность комбинирования нескольких регионов в один, более сложный. Это делается с помощью функции:
int CombineRgn (hrgnDest, hrgnSrc1, hrgnSrc2, nMode);
Данная функция позволяет выполнить определенную параметром nMode операцию над двумя (или одним) исходными регионами и результат записать в третий регион. При этом новый регион не создается, вы должны предварительно создать какой-либо регион и его хендл передать в качестве hrgnDest. В этом регионе будет размещен результат выполнения операции. Такое, на первый взгляд странное, правило позволяет несколько уменьшить количество создаваемых объектов.
Итак, с помощью функции CombineRgn, мы можем выполнять различные операции, задавая номер нужной операции в параметре nMode:
RGN_AND | — получить пересечение двух регионов (точки, входящие в оба региона одновременно) |
RGN_OR | — получить объединение регионов (точки, входящие хотя бы в один из двух регионов) |
RGN_XOR | — получить объединение без перекрывающихся областей |
RGN_DIFF | — получить часть первого региона, не входящую во второй регион |
RGN_COPY | — скопировать первый регион (второй регион не используется) |
При этом функция возвращает информацию о том, какой регион получен:
SIMPLEREGION | — если итоговый регион состоит из не перекрывающихся примитивов |
COMPLEXREGION | — если примитивы, входящие в итоговый регион, перекрываются |
NULLREGION | — итоговый регион пустой (не имеет общих точек) |
ERROR | — возникла ошибка (например, недостаточно памяти) |
В заголовочном файле windowsx.h включено несколько макросов, основанных на функции CombineRgn:
int CopyRgn (hrgnDest, hrgnSrc); 2
int IntersectRgn (hrgnDest, hrgnSrc1, hrgnSrc2); 2
int SubtractRgn (hrgnDest, hrgnSrc1, hrgnSrc2); 2
int UnionRgn (hrgnDest, hrgnSrc1, hrgnSrc2); 2
int XorRgn (hrgnDest, hrgnSrc1, hrgnSrc2); 2
Существует еще одна функция, которая может изменить тип региона, она позволяет заменить указанный вами любой регион на регион прямоугольной формы:
void SetRectRgn (hrgnSrc, lpRect);
Таким образом, применяя функции создания регионов и их комбинируя мы можем описать области очень сложной формы. Теперь нам надо разобраться с основными способами применения регионов.
Во–первых, мы можем применять регионы как абстрактные объекты, и выполнять над ними какие-либо операции, например перемещение, аналогично операциям над прямоугольниками:
int OffsetRgn (hrgnSrc, nDeltaX, nDeltaY);
или проверять совпадение регионов:
BOOL EqualRgn (hrgnSrc1, hrgnSrc2);
Кроме того мы можем проверить принадлежность точки или прямоугольника региону:
BOOL PtInRegion (hrgnSrc, nX, nY);
BOOL RectInRegion (hrgnSrc, lpRect);
И еще одна функция позволяет получить прямоугольник, описанный вокруг указанного региона:
int GetRgnBox (hrgnSrc, lpRect);
Во–вторых, регионы могут отображаться на контексте устройства, например для закраски областей или обведения контура области сложной формы:
BOOL InvertRgn (hDC, hrgnSrc);
BOOL PaintRgn (hDC, hrgnSrc);
BOOL FillRgn (hDC, hrgnSrc, hbrBrush);
BOOL FrameRgn (hDC, hrgnSrc, hbrBrush, nFrameWidth, nFrameHeight);
Функция InvertRgn осуществляет операцию BITWISE NOT над всеми точками, входящими в указанный регион; она аналогична функции InvertRect. Функция PaintRgn закрашивает регион текущей кистью. Она подобна функции FillRgn, которая закрашивает регион указанной вами, а не текущей, кистью. Самая интересная функция — FrameRgn, которая проводит вокруг региона каемку указанной ширины и указанной кистью. То есть эта функция аналогична функции FrameRect, за исключением того, что область может быть сложной формы и вы можете задать ширину каемки, причем как по горизонтали, так и по вертикали.
Рисунок 14. Применение регионов для закраски и областей и обведения области контуром.
В–третьих, еще один из способов применения регионов связан с обработкой сообщения WM_PAINT. Ранее мы говорили о том, что сообщение WM_PAINT генерируется, когда у окна появляется неверный прямоугольник.
Это не совсем точно — вместо неверного прямоугольника обычно используется регион. Аналогично прямоугольникам, у вас есть две функции, одна из которых объявляет неверный регион, а другая указывает, что регион стал верным:
void InvalidateRgn (hWnd, hrgnSrc, fEraseBkgnd);
void ValidateRgn (hWnd, hrgnSrc);
Если сообщение WM_PAINT генерируется в результате появления неверного региона, то полученный с помощью функции BeginPaint контекст устройства может применяться только для рисования внутри неверного региона.
В–четвертых, регион, являясь объектом GDI, может быть выбран в контекст устройства. Регион, выбранный в контекст устройства, определяет область этого контекста, на которой возможно рисование. При этом он является как бы "маской" через которую видно рисуемое изображение.
Рисунок 15. Исходное изображение (слева), регион (в центре) и нарисованное изображение (справа). Светло–серым цветом показан неизменяемый данным рисунком фон.
Для того, что бы выбрать регион в контекст устройства, вы должны воспользоваться функцией
int SelectClipRgn (hDC, hrgnSrc);
Эта функция возвращает целое число, указывающее тип выбранного региона (SIMPLEREGION, COMPLEXREGION, NULLREGION). При выборе региона в контекст устройства он копируется, поэтому вы можете удалить его или использовать иным образом сразу после выбора в контекст устройства.
Кроме функции SelectClipRgn вы можете воспользоваться функцией SelectObject с той же целью. При этом функция SelectObject будет использоваться точно также, как и функция SelectClipRgn, и вернет не хендл предыдущего региона, а тип выбранного вами.
Растровые изображения и метафайлы
В Windows существует возможность хранить изображения в виде картинок, сохраняющих рисунок в виде информации о цветах отдельных точек. Такие изображения иногда называются растровыми, так как информация о цвете точек группируется по строкам растра изображения, или битмапами (bitmap), иногда термин bitmap даже переводят дословно — битовая карта. В ранних версиях Windows битмапы точно соответствовали образу в графической памяти устройства, на которое осуществлялся вывод. При этом информация о передаче цветов соответствовала в точности формату цветовой памяти устройства. Такие битмапы получили название зависимых от устройства битмапов (device–depended bitmap, DDB)
Так, например, для четырехцветных видеоадаптеров CGA каждый пиксель кодировался двумя последовательными битами в видеопамяти — такой–же была организация битмапов, отображаемых на дисплее. А если использовался 16ти–цветный адаптер EGA, в котором для задания каждого пикселя требовалось задать 4 бита лежащих в различных цветовых плоскостях (planes), то и битмап создавался аналогично — каждая строка растра была представлена 4 раза, для каждой цветовой плоскости по разу. Несомненным достоинством таких изображений была простота их отображения на конечном устройстве и высокая скорость вывода.
Но были и недостатки — главный из них — цвет конкретной точки определяется непосредственно настройками аппаратуры и остается независим от самого растрового изображения. То есть попытка отобразить одно и то же изображение при различных настройках (допустим, при различных используемых палитрах), приводила к искажению цветов исходного изображения. Помимо этого при попытке отобразить одно и тоже изображение на разных устройствах требовалось преобразовывать информацию о кодировании цветов — что опять же требовало дополнительных данных о назначении цветов в обоих устройствах.
Все это привело к тому, что был разработан новый стандарт хранения растровых изображений — так называемые независимые от устройства битмапы (device–independed bitmap, DIB). Этот битмап отличается от DDB как фиксированным способом кодирования цвета каждой точки — последовательной группой бит — так и наличием информации о назначении цветов — так называемой палитры (palette) — или иной информации, позволяющей определить точное назначение цветов каждой точки.
Начиная с Windows 3.x все битмапы, представленные в виде файлов или ресурсов приложения, являются независимыми от устройства битмапами (DIB), в то время как после загрузки в память приложения эти битмапы могут быть представлены как в виде независимых от устройства, так и в виде зависимых — смотря по способу загрузки и использования.
Говоря о битмапах надо выделить несколько обсуждаемых аспектов, решаемых для каждого вида битмапов своим способом;
получение битмапа
формирование или коррекция изображения
отображение битмапа
сохранение независимых от устройства битмапов в файлах
Для зависимых и для независимых от устройства битмапов эти задачи решаются разными методами с привлечением разных функций и инструментов. Использовать один вид битмапов в функциях, предназначенных для работы с другим видом битмапов, невозможно. В последующих разделах эти задачи будут обсуждены более подробно, здесь же будет намечена общая схема решения этих задач для каждого вида битмапов. В некоторых случаях битмапы можно применять другими способами — например для создания кистей, передачи изображения через буфер обмена (clipboard) и так далее.
Помимо растровых изображений (зависимых и независимых от устройства битмапов) в Windows предусмотрен еще один способ сохранения изображений — сохранение рисунка в метафайлах.
Обзор зависимых от устройства битмапов (DDB)
Зависимый от устройства битмап (DDB) является объектом GDI и работа с ним осуществляется также, как и обычными объектами GDI — перьями, кистями и прочим. Говоря об идентификации зависимого от устройства битмапа говорят о его хендле — хендле объекта GDI. Более того, так как независимые от устройства битмапы не являются объектами GDI, то они также не имеют специфичных хендлов. Как только в тексте упоминается некоторый хендл битмапа (HBITMAP), то можно однозначно утверждать, что подразумевается зависимый от устройства битмап, DDB.
Более того, зависимые от устройства битмапы в реальной жизни представлены именно как объекты GDI, так как во всех современных версиях Windows изображения хранятся в виде независимых от устройства битмапов. То есть любой сохраняемый на диске (в виде файла или ресурсов приложения) битмап — всегда независимый от устройства, и только после его загрузки в память в виде объекта GDI он станет зависимым от устройства битмапом.
Конечно, независимые от устройства битмапы могут быть загружены в память непосредственно, однако часто для представления в памяти используются именно DDB, так как его отображение выполняется быстрее и он как правило занимает меньше ресурсов.
Получение битмапа;
Для получения хендла битмапа вы можете либо создать новый объект и получить его хендл, либо загрузить уже имеющееся изображение. Так как во всех современных версиях Windows битмапы реально хранятся в виде независимых от устройства, то для загрузки изображения в виде DDB надо осуществить преобразование независимого битмапа в зависимый. Это делается либо автоматически — при загрузке битмапа из ресурсов приложения, либо это надо осуществить непосредственно в вашем приложении.
Формирование или коррекция битмапа;
Для рисования на битмапе создается контекст устройства, ассоциированный с данным битмапом, после чего все функции GDI, применяемые к этому контексту, реально взаимодействуют с битмапом и формируют его изображение. Для выполнения этой задачи предназначен совместимый контекст устройства (compatible DC, memory DC), который предназначен именно для этого.
Отображение битмапа на контексте устройства;
В конце концов все и затевается ради возможности отобразить битмап в окошке или на листе бумаги. GDI не содержит специальных функций для отображения DDB. Вместо этого вы должны ассоциировать битмап с контекстом устройства (как и для рисования), а затем осуществить передачу изображения с одного контекста на другой — для этого в GDI содержится специальный набор функций, называемых функциями передачи растровых изображений или, дословно, функциями для передачи блоков бит (Bit Block Transfer, BLT — произносится «блит»)
Обзор независимых от устройства битмапов (DIB)
Независимый от устройства битмап (DIB) объектом GDI не является. Он загружается в виде одного или нескольких блоков данных (подробнее о работе с блоками памяти см. в разделе «Диспетчер памяти»), его идентифицируют либо указатели на эти данные, либо хендл блока памяти, в котором этот битмап располагается (это зависит от применяемых функций). Если битмап сохраняется на диске (в виде отдельного файла или в виде ресурса приложения), то это непременно DIB.
После загрузки в память DIB может быть представлен как непосредственно в виде независимого от устройства битмапа, так и он может быть превращен в DDB. В том случае, если его представление соответствует DIB, то он реализован в виде нескольких взаимосвязанных структур данных, описывающих информацию о битмапе (заголовок и палитра) и непосредственно изображение. Иногда эти структуры размещаются в разных областях данных (тогда битмап идентифицируется указателями на эти данные), а иногда в одной — в этом случае структура этой области данных примерно соответствует содержимому файла с DIB, но без заголовка файла — такое представление называется упакованный DIB (packed DIB). В последнем случае для идентификации независимого от устройства битмапа может хватить одного хендла блока, содержащего упакованный DIB, однако этот хендл не является хендлом битмапа (HBITMAP).
При работе с DIB следует выбрать один из трех путей:
1) Работать с DIB непосредственно. Для этого предназначен достаточно большой набор функций, реализованный и в Windows API и в Win32 API.
Получение битмапа из файла или ресурса приложения;
Если битмап представлен в виде файла, то его загрузку необходимо выполнить самостоятельно; по счастью эта процедура может быть сведена всего к нескольким строчкам исходного кода. Можно, конечно, воспользоваться функциями LoadImage 1 или LoadBitmap, но это приведет к получению DDB, причем с организацией, соответствующей дисплею и соответствующего текущей выбранной палитре. Это может быть неудобно, особенно при необходимости этот битмап позже вывести на печать — для этого лучше воспользоваться либо самим DIB, либо DDB, созданным под характеристики и под палитру, применяемую на устройстве отображения. Такую операцию можно выполнить, используя вместо функции LoadBitmap пару функций FindResource и LoadResource, что позволит получить непосредственно сам DIB. (Вместо LoadImage надо прочитать файл с битмапом).
Сохранение битмапа в файле;
Эту операцию надо выполнить самостоятельно, разобравшись со структурами, описывающими DIB, и записав их в файл в необходимом порядке.
Отображение битмапа на контексте устройства;
В отличие от DDB для DIB предусмотрены специальные функции, передающие изображение битмапа на указанный контекст устройства, близкие к функциям передачи растровых изображений между контекстами устройств. В отличие от DDB для DIB не требуется предварительно связывать битмап с контекстом устройства, эти функции осуществляют перенос изображения непосредственно из загруженного в память DIB на контекст устройства.
2) Работать с создаваемым промежуточным DDB. При этом DIB считывается и записывается также, как и в случае (1), однако дальше он преобразуется в DDB с которым и осуществляется все необходимая работа. Этот вариант является одним из самых эффективных, хотя и громоздких — он позволяет получить обычный битмап с организацией, наиболее точно соответствующей устройству отображения, что позволяет как сэкономить ресурсы, так и ускорить процесс вывода.
3) Создать ассоциацию DIB с контекстом устройства. Этот путь похож на выбор обычного битмапа в контекст устройства с помощью функции SelectObject, однако осуществляется другими средствами и разным образом в разных API (Windows или Win32).
В случае 16ти разрядной платформы Windows 3.x вы можете использовать специальный DIB драйвер, поставляемый в составе многих компиляторов
В случае Win32 вы можете использовать так называемую DIB–секцию, которая является своеобразным гибридом обычного битмапа (для нее возвращается HBITMAP, что позволяет применять ее как объект GDI) и независимого от устройства — в памяти она представлена как нормальный DIB.
Обзор метафайлов
Альтернативный метод сохранения изображений представлен в виде метафайлов (metafile). Метафайл в строгом смысле рисунка не хранит, он сохраняет только последовательность команд (вызовов функций GDI) формирующих изображение.
Для работы с метафайлами в Windows содержится достаточно полный набор функций, практически исключающий необходимость делать что–либо самостоятельно. Общие операции над метафайлами можно разделить на операции по созданию метафайла, то есть его записи и операции по воспроизведению метафайла на нужном контексте устройства.
Создание метафайлов;
Для создания метафайла создается специальный контекст устройства; все дальнейшие операции по рисованию на этом контексте запоминаются и сохраняются в виде метафайла. При удалении контекста устройства, связанного с метафайлом, создается объект GDI, представляющий данный метафайл. Этот метафайл может быть представлен либо в оперативной памяти, либо в виде файла — смотря по тому, как вы создавали контекст устройства метафайла. Никаких специальных шагов для записи метафайла на диск предпринимать не надо.
Воспроизведение метафайла;
Для воспроизведения метафайла предназначена специальная функция, которая последовательно выполняет все сохраненные в метафайле команды на указанном контексте устройства. Для ее применения необходимо указать хендл метафайла (объекта GDI), который возвращается либо при удалении контекста устройства, либо при вызове специальной функции, загружающей метафайл с диска в оперативную память.
В какой-то степени можно сравнить метафайл с микропрограммой, осуществляющей вывод изображения.
Зависимые от устройства битмапы
Обычный, зависимый от устройства битмап является простым образом видеопамяти. Его организация отражает особенности аппаратуры, на которой он должен воспроизводиться. DDB в приложении представляется как объект GDI; аналогично описанию перьев или кистей DDB описывается с помощью специальной структуры (BITMAP) и доступен посредством хендла этого объекта типа (HBITMAP).
typedef struct tagBITMAP {
int bmType;
int bmWidth;
int bmHeight;
int bmWidthBytes;
BYTE bmPlanes;
BYTE bmBitsPixel;
LPSTR bmBits;
} BITMAP;
Поле bmType должно быть 0, поля bmWidth и bmHeight определяют размеры изображения, bmPlanes и bmBitsPixel используются для указания способа кодирования информации о цвете точки и для указания максимального количества цветов.
Использование двух полей bmPlanes и bmBitsPixel связано с особенностями хранения цветного изображения разными видеоадаптерами. Например, CGA, IBM 8514 или SVGA в некоторых режимах для задания цвета пикселя отводят несколько последовательных бит памяти (CGA — 2 бита, IBM — 8 бит, SVGA — до 32 бит на каждый пиксель). А адаптеры типа EGA или VGA (и, конечно, в некоторых режимах SVGA) содержат несколько так называемых битовых плоскостей, или планов (planes). В каждом плане одному пикселю соответствует только один бит, а цвет задается комбинацией бит в разных планах. Поле bmPlanes структуры BITMAP определяет количество цветовых планов, а поле bmBitsPixel — количество последовательных бит, отведенных для задания цвета пикселя а одном плане. По крайней мере один из этих параметров равен 1 (или оба — для монохромных битмапов).
Изображение в битмапе хранится разделенным на строки растра (scan line). Длина каждой строки округляется в большую сторону до ближайшей четной границы (кратна 2 байтам) и задается полем bmWidthBytes. Для вычисления длины строки надо произведение bmWidth * bmBitsPixel разделить на 8 и округлить в сторону завышения до ближайшего четного числа.
Если битмап содержит несколько цветовых планов, то строки разных цветовых планов одной строки растра размещаются последовательно друг за другом, затем для другой строки и так далее.
Поле bmBits теоретически должно указывать на собственно данные битмапа (массив строк растра). Однако оно используется далеко не всегда — вы его можете задавать, но когда информацию о битмапе вам возвращает система (функция GetObject), то это поле не инициализируется — для получения данных битмапа существует специальная функция — GetBitmapBits.
Создание зависимого от устройства битмапа
Создание зависимых от устройства битмапов — случай сравнительно редкий; обычно битмапы готовятся специальным графическим редактором и позже загружаются в память либо из файла, либо из ресурсов приложения. В связи с этим предлагаемый здесь материал нужен, в основном, для более близкого «фамильярного» знакомства с DDB; на практике использоваться этот материал будет редко. Предположим, что мы хотим создать битмап для использования в качестве кисти. Кисть всегда имеет размер 8x8 пикселей. Кроме того, для упрощения, мы будем предполагать, что используется монохромный битмап. Монохромный — так как изображение этого битмапа мы опишем сами, а описание цветного битмапа непосредственно в приложении — крайне неэффективное решение. Кроме того, монохромные битмапы обрабатываются несколько особым образом, так что есть повод обратить внимание на их особенности. Итак, пусть мы хотим получить следующую картинку:
Рисунок 16. Подготавливаемый рисунок монохромного битмапа (при использовании такого битмапа в качестве кисти мы получим "кирпичную стену").
Обратите внимание на нумерацию байтов и бит в байте — байты (не слова) нумеруются слева–направо, а биты в байте — справа–налево. Подготовим для этой картинки данные битмапа: так как ширина битмапа 8, количество бит на пиксель 1, то в одной строке растра должно быть 8 бит, значит ее длина 2 байта (четное число) или одно слово.
static WORD wBits[]={
0x00FF, 0x00C0, 0x00C0, 0x00C0, 0x00FF, 0x000C, 0x000C, 0x000C};
Далее мы можем создать битмап, содержащий эту картинку. Для этого можно воспользоваться одной из следующих функций:
HBITMAP CreateBitmap (cxWidth, cyHeight, nPlanes, nBitsPixel, lpBits);
HBITMAP CreateBitmapIndirect (lpBitmap);
HBITMAP CreateCompatibleBitmap (hDC, cxWidth, cyHeight);
HBITMAP CreateDiscardableBitmap (hDC, cxWidth, cyHeight);
Функция CreateBitmap позволяет создать битмап с заданными характеристиками. В нашем случае это будет выглядеть так:
HBITMAP hBmp= CreateBitmap (8, 8, 1, 1, wBits);
Функция CreateBitmapIndirect позволяет сначала описать структуру типа BITMAP, а затем создать битмап по этой структуре. В нашем примере эту функцию можно использовать, например, таким образом:
static BITMAP stBmp= {
0, // bmType
8,8, // bmWidth, bmHeight
2, // bmWidthBytes
1,1, // bmPlanes, bmBitsPixel
NULL // bmBits};
HBITMAP hBmp= CreateBitmapIndirect (&stBmp);
SetBitmapBits (hBmp, sizeof (wBits), wBits);
Конечно, мы могли установить адрес образа (wBits) в поле bmBits и обойтись без функции SetBitmapBits но так мы рассмотрим на одну функцию больше.
В некоторых случаях мы будем создавать битмапы, которые должны быть по своим характеристикам совместимы с конкретным контекстом устройства. Для этого предназначена функция CreateCompatibleBitmap, которая создает битмап указанного размера и такой же организации, как указанный контекст устройства. Изображение при этом не задается — битмап содержит набор случайных данных. Позже вы можете воспользоваться, например, функцией SetBitmapBits для задания данных битмапа.
Внимание! При создании совместимого битмапа надо указывать контекст реального устройства, совместимость с которым требуется. Если указать совместимый контекст устройства в качестве прототипа, то будет создан монохромный битмап.
Функция CreateDiscardableBitmap используется крайне редко, так как создаваемый с ее помощью битмап может быть удален из памяти диспетчером при необходимости.
Независимо от способа создания, существует одна особенность в применении монохромных битмапов: при его отображении на цветном устройстве для представления точек битмапа будут использоваться не черный и белый цвета, а текущий цвет фона (для всех бит, равных 0, — обычно это белый цвет) и текущий цвет текста (для всех бит, равных 1, обычно это черный цвет). Таким образом вы можете в определенной мере управлять отображением монохромного битмапа с помощью функций SetTextColor и SetBkColor.
При необходимости вы можете узнать характеристики битмапа, используя функцию GetObject:
BITMAP stBmp;
GetObject (hBmp, sizeof (stBmp), &stBmp);
Однако эта функция не устанавливает поле bmBits структуры BITMAP. Для получения данных битмапа надо воспользоваться функцией:
LPSTR GetBitmapBits (hBitmap, dwMaxSize, lpBuffer);
Еще несколько функций могут использоваться совершенно специфичным способом:
DWORD GetBitmapDimension (hBmp);
BOOL GetBitmapDimensionEx (hBmp, lpSize);
DWORD SetBitmapDimension (hBmp, nWidth, nHeight);
BOOL SetBitmapDimensionEx (hBmp, nX, nY, lpSize);
Эти процедуры используются для задания/получения справочного размера битмапа, в единицах по 0.1 мм. Никакие иные функции GDI не используют эту информацию при работе с битмапами. Практически вы можете использовать эти размеры сами при необходимости передачи битмапов между устройствами с разной разрешающей способностью.
Получение зависимых от устройства битмапов как ресурсов приложения
Создание битмапов непосредственно в приложении — случай сравнительно редкий. Обычно битмапы рисуются с помощью какого–либо графического редактора и затем загружаются из файла (в этом случае загружается не DDB, а DIB), либо битмап добавляется в ресурсы приложения и затем загружается из ресурсов с помощью функции LoadBitmap. В этом случае выполняется загрузка именно в виде зависимого от устройства битмапа, хотя в ресурсах приложения размещается DIB.
Интересный нюанс — так как битмап из ресурсов приложения загружается как зависимый от устройства, то какому устройству будут соответствовать его характеристики? Microsoft считает, что такие битмапы будут отображаться преимущественно на дисплее, и, следовательно, DDB будет строиться под характеристики дисплея. Указать, для какого устройства битмап надо оптимизировать невозможно. В большинстве случаев этот подход вполне приемлем, однако при необходимости осуществлять вывод битмапа не только в окне, но и на иных устройствах (например на принтере или внедрять его в метафайл), лучше получить доступ непосредственно к самому DIB, записанному в ресурсах приложения (об этом — в разделе «Загрузка независимых от устройства битмапов.»).
Подробнее о применении и описании ресурсов приложения — см. раздел «Ресурсы приложения», здесь же будут представлены основные сведения об описании битмапов в качестве ресурсов приложения. Для описания битмапа в файле описания ресурсов принята следующая форма:
nameId | уникальное имя или номер ресурса |
load-opt 0 | режим загрузки ресурса: PRELOAD или LOADONCALL (по умолчанию) |
mem-opt 0 | тип выделяемой памяти: FIXED, MOVEABLE (по умолчанию) или DISCARDABLE |
filename.bmp | имя файла, содержащего битмап |
nameId BITMAP [load-opt] [mem-opt] filename.bmp (некоторые редакторы и компиляторы ресурсов, как, скажем, Borland WorkShop, позволяют описывать битмап непосредственно в файле описания ресурсов в текстовом виде. Тогда вместо filename.bmp используются структурные скобки BEGIN...END или {...} с заключенным между ними данными битмапа в виде списка шестнадцатеричных чисел. Этот способ не гарантирует переносимость ресурсов между разными средами разработки приложений.)
Часто режим загрузки ресурса и тип выделяемой памяти при описании битмапов не указывается — предлагаемые по умолчанию значения (загрузка по требованию и перемещаемый блок памяти) как правило являются оптимальными. Примеры описания битмапов:
red_brick BITMAP rbrick.bmp
1 BITMAP firm.bmp
Считается, что лучше использовать уникальные номера ресурсов, а не имена, так как это обеспечивает более быстрый поиск ресурса в приложении и требует меньше памяти для описания ресурсов приложения.
Для загрузки битмапа из ресурсов приложения используются функции:
HBITMAP LoadBitmap (hInstance, lpszName);
HANDLE LoadImage (hInstance, lpszName, uType, cxDesired, cyDesired, fuLoad); 1
где hInstance — хендл копии приложения, содержащего данный битмап, а lpszName — имя ресурса битмапа. Имя ресурса может быть либо текстом — тогда lpszName это обычная строка, оканчивающаяся символом ‘\0’, либо номером — тогда вместо lpszName может стоять или «#number», или MAKEINTRESOURCE (number). Например, для загрузки битмапов «red_brick» и «1» можно воспользоваться такими вызовами функций:
HBITMAP hbmpRedBrick = LoadBitmap (hInstance, "red_brick");
HBITMAP hbmp1a = LoadBitmap (hInstance, "#1");
HBITMAP hbmp1b = LoadBitmap (hInstance, MAKEINTRESOURCE (1));
Причем последний вариант является самым быстрым и компактным.
Функция LoadImage осуществляет загрузку битмапов, пиктограмм и курсоров. Теоретически она позволяет загружать требуемый ресурс из файла (для этого в fuLoad надо установить флаг LR_LOADFROMFILE и указать hInstance равным NULL). Однако такая операция поддерживается только в случае Windows–95, Windows NT 4.0 и более поздних. Предыдущие реализации Win32 API не поддерживают загрузку изображений из файлов.
Вы можете использовать стандартные битмапы, предоставляемые Windows. Их символические имена начинаются на OBM_... . Для того, что бы вы могли воспользоваться этими идентификаторами, необходимо перед директивой #include <windows.h> определить символ OEMRESOURCE, то есть:
#define OEMRESOURCE
#include <windows.h>
В таблице приведены изображения стандартных битмапов и их идентификаторы, в соответствии с их реализацией в Windows 3.x (Windows API) и Windows NT 3.x (Win32 API); в более поздних версиях (как, например, Windows–95, Windows NT 4.0) внешний вид стандартных битмапов несколько изменен. В таблице заполнены не все клетки просто из соображений построчной группировки схожих битмапов.
OBM_UPARROW | OBM_UPARROWI | OBM_UPARROWD | OBM_OLD_UPARROW | ||||
OBM_DNARROW | OBM_DNARROWI | OBM_DNARROWD | OBM_OLD_DNARROW | ||||
OBM_RGARROW | OBM_RGARROWI | OBM_RGARROWD | OBM_OLD_RGARROW | ||||
OBM_LFARROW | OBM_LFARROWI | OBM_LFARROWD | OBM_OLD_LFARROW | ||||
OBM_REDUCE | OBM_REDUCED | OBM_OLD_REDUCE | |||||
OBM_ZOOM | OBM_ZOOMD | OBM_OLD_ZOOM | |||||
OBM_RESTORE | OBM_RESTORED | OBM_OLD_RESTORE | |||||
OBM_CLOSE | OBM_OLD_CLOSE | ||||||
OBM_MNARROW | OBM_COMBO | OBM_SIZE | OBM_BTSIZE | ||||
OBM_CHECK | OBM_BTNCORNERS | OBM_CHECKBOX |
Работа с зависимым от устройства битмапом
Небольшое замечание: так как битмап является объектом GDI, то вы обязаны удалить его, как только он станет ненужным. Это относится ко всем битмапам, как созданным с помощью функций CreateBitmap, CreateBitmapIndirect, CreateCompatibleBitmap, CreateDiscardableBitmap, так и к загруженным с помощью функции LoadBitmap. Освобождение неиспользуемых битмапов особенно важно, так как это едва–ли не самые большие объекты GDI, занимающие значительные ресурсы.
В GDI практически не содержится функций, использующих зависимые от устройства битмапы непосредственно. Исключение, разве что, функции для создания кисти по образцу, для задания графического образа пункта меню или для передачи изображения в независимый от устройства битмап (подробнее см. в соответствующих разделах):
HBRUSH hbrBrush = CreatePatternBrush (hBmp);
DeleteBitmap (hBmp); // 2
После того, как мы создали кисть, битмап можно удалять, так как его образ скопирован в кисть и больше не используется. Если битмап больше, чем 8x8 пикселей, то для создания кисти будет использован его верхний–левый уголок, размером 8x8.
Все остальные операции по работе с битмапами осуществляются посредством специально создаваемого контекста устройства, ассоциированного с этим битмапом. Для этого был разработан специальный вид контекстов устройства — совместимый контекст (compatible device context, compatible DC, чаще называемый memory device context, memory DC). Такой разнобой в названиях контекста связан, с одной стороны, с названием функции, его создающей — CreateCompatibleDC — создающей контекст устройства, совместимого с другим, реально существующим устройством (см. раздел «Получение хендла контекста устройства»). А, с другой стороны, созданный таким образом контекст устройства не соответствует никакому физическому устройству, его область отображения — некоторое растровое изображение, хранимое в памяти. Отсюда второе название — memory DC.
Совместимость контекста не значит, что его цветовая организация совпадает с организацией реально существующего контекста, а только лишь то, что принципы выполнения операций над этим контекстом будут такими же, как для реального устройства — например, при выводе на совместимый контекст может применяться графический акселератор, если для контекста реального устройства, указанного в качестве прототипа при создании совместимого, такой акселератор используется (конечно, это зависит еще и от возможностей самого акселератора).
Только что созданный совместимый контекст устройства имеет монохромную область отображения размером 1 пиксель. Так как нарисовать что–нибудь осмысленное в такой области нереально, вы должны принять меры к тому, что бы область отображения этого контекста была ассоциирована с хранимым в памяти растровым изображением — зависимым от устройства битмапом. Это делается тривиально — битмап выбирается в совместимый контекст устройства с помощью обычной функции SelectObject. После этого область отображения совместимого контекста будет совпадать с указанным битмапом — как по размерам, так и по цветовой организации.
Внимание! GDI предполагает, что битмап может быть выбран только в совместимый контекст устройства. Если его выбрать в какой–либо контекст реально существующего устройства, то скорее всего такая попытка будет просто проигнорирована, хотя в зависимости от платформы и используемых драйверов устройств, реакция системы может быть и иной.
Общая схема при этом выглядит следующим способом:
HDC hCompatDC;
HBITMAP hBmp;
hCompatDC = CreateCompatibleDC (hDC);
// функция CreateCompatibleDC () создает совместимый
// контекст устройства, соответствующий одному монохромному пикселу
hBmp = LoadBitmap (hInstance, lpszName);
// для получения хендла битмапа мы могли воспользоваться любым
// способом - его загрузкой из ресурсов или созданием
SelectObject (hCompatDC, hBmp);
// теперь совместимый контекст устройства соответствует нашему битмапу.
// ... здесь мы можем выполнять любые операции по рисованию на нашем битмапе
// ... или передавать изображения между разными контекстами устройств.
DeleteDC (hCompatDC);
// после того, как мы выполнили все нужные операции над контекстом
// устройства, мы можем его удалить.
// ... При этом битмап как объект GDI остается и мы можем свободно
// ... применять его хендл. Например, для создания кисти, или для
// ... отображения пункта меню.
DeleteObject (hBmp);
// после того, как битмап стал нам не нужен, мы можем его уничтожить
Очень часто встречается частный случай этой схемы: при создании нового изображения битмап делается совместимым по цветовой организации с тем устройством, на котором он будет отображаться. В этом случае создание битмапа выглядит так:
HDC hCompatDC;
HBITMAP hBmp;
hCompatDC = CreateCompatibleDC (hDC);
hBmp = CreateCompatibleBitmap (hDC, 500, 300)
SelectObject (hCompatDC, hBmp);
PatBlt (hCompatDC, 0,0, 500,300, PATCOPY);
// ... здесь мы можем выполнять любые операции по рисованию на нашем битмапе
// ... или передавать изображения между разными контекстами устройств.
DeleteDC (hCompatDC);
// ... Работаем с битмапом как с объектом GDI
DeleteObject (hBmp);
В этом примере надо отметить два момента: Во–первых, при создании битмапа в качестве прототипа задается обязательно контекст реального устройства (с заданной цветовой организацией), а не совместимого (который соответствует одному монохромному пикселю). Битмап, совместимый с совместимым контекстом устройства будет монохромным! Во–вторых, созданный совместимый битмап содержит произвольные данные, поэтому перед его использованием изображение надо очистить. В этом примере функция PatBlt закрашивает битмап текущей кистью (операция PATCOPY), иногда для начальной закраски используют не текущую кисть (по умолчанию — WHITE_BRUSH может быть не белой), а белый или черный цвета (операции WHITENESS, BLACKNESS). Это зависит от дальнейшего использования: фон битмапа должен совпадать с фоном окна или должен быть конкретного цвета.
Эта схема действительно удобна, если желательно, что бы цветовая организация битмапа соответствовала цветовой организации устройства, на котором он будет отображаться. Как правило это так и есть, кроме сравнительно редких случаев применения монохромных битмапов.
До тех пор, пока битмап остается выбран в совместимый контекст устройства, вы можете применять все функции GDI для рисования и коррекции изображения. Но одна из нужнейших задач — отображение битмапа на нужном устройстве — остается нерешенной. Специальных функций для отображения зависимых от устройства битмапов на контексте устройства в GDI нет, однако предусмотрен более универсальный и мощный механизм, обеспечивающий выполнение этой задачи — механизм передачи растровых изображений между контекстами устройств.
Операции передачи образов
Рассматривая применение битмапов мы обратили внимание на специальный механизм, осуществляющих передачу растровых изображений между различными контекстами устройств. Этот механизм называется операции по обмену блоками бит (bit block transfer, BLT) или тернарными растровыми операциями (ternary raster operation).
Основная идея растровых операций (слово тернарные часто опускают, в отличие от слова бинарные — см. раздел «Режим рисования», стр. 26) заключается в организации обмена данными между двумя контекстами устройств. Эти операции универсальны — они работают с любыми контекстами устройств, поддерживающими обмен растровыми изображениями (например, устройства типа плоттера такими возможностями, естественно, не обладают). Таким образом вы можете осуществить передачу изображения и между битмапом, выбранным в совместимый контекст устройства и реальным устройством на котором хотите это изображение показать, между двумя битмапами или передать имеющееся изображение с реального устройства в битмап или на другое устройство.
GDI содержит 3 функции, осуществляющих такую передачу изображений — PatBlt, BitBlt и StretchBlt (заметьте, что аббревиатура BLT произносится как БЛИТ):
BOOL PatBlt (
hDC, nX, nY, nWidth, nHeight, dwROP);
BOOL BitBlt (
hDestDC, nDestX, nDestY, nDestWidth, nDestHeight,
hSrcDC, nSrcX, nSrcY, dwROP);
BOOL StretchBlt (
hDestDC, nDestX, nDestY, nDestWidth, nDestHeight,
hSrcDC, nSrcX, nSrcY, nSrcWidth, nSrcHeight, dwROP);
Все три функции выполняют сходные операции — они строят результирующее изображение на контексте–приемнике, используя в качестве исходных данных:
изображение, создаваемое на приемнике при закраске фона текущей кистью, выбранной в контекст–приемник (это называется образцом, pattern).
изображение, существующее на контексте–источнике (исходное изображение, source).
изображение, существующее в данный момент на контексте–приемнике (имеющееся изображение, destination).
В процессе выполнения растровой операции эти три исходных изображения (битовых последовательности) комбинируются и получается результирующее изображение. Так как в операции участвуют три исходных последовательности, то операция получила название тернарной (ternary).
Код выполняемой операции задается параметром dwROP — индексом тернарной растровой операции.
В документации по SDK можно найти таблицу, перечисляющую индексы 256 возможных растровых операций, их имена и короткое пояснение к каждой операции. Причем имена присвоены только 15 наиболее употребляемым операциям. Таблица, представленная в документации имеет следующий вид:
Number | Hex ROP | Boolean function | Common Name |
0 | 00000042 | 0 | BLACKNESS |
... | |||
0D | 000D0B25 | PDSnaon | |
... | |||
Поле «Hex ROP» содержит индекс тернарной растровой операции, который вы должны использовать в качестве параметра dwROP. Поле «Boolean function» содержит пояснение к выполняемой операции, а поле «Common name» — имя растровой операции, если оно назначено. Однако разобраться в том, какая конкретно операция выполняется в процессе переноса изображения не так–то просто.
Попробуем пояснить это на примере: операция с индексом 000D0B25 обозначает операцию PDSnaon. Это обозначение содержит в обратной польской записи логические операции над битами, выполняемые в процессе растровой операции. Сначала указаны большими буквами используемые компоненты:
P: образец, (кисть, pattern)
D: существующее изображение (destination)
S: исходное изображение (source),
в общем случае для обозначения компонент операции используются эти три больших буквы, но порядок их перечисления зависит от выполняемой операции. После перечисления компонентов следуют маленькие буквы, указывающие выполняемые операции:
n: инверсия, not; операция использует 1 аргумент
a: пересечение, and; операция использует 2 аргумента
o: объединение, or; операция использует 2 аргумента
x: исключающее ИЛИ, xor; операция использует 2 аргумента
Для того, что бы понять как выполняются эти операции представим, что у нас есть стек, и каждая буква в записи указывает операцию: большая буква выполняет запись в стек, маленькая — операцию над нижними данными в стеке, причем они из стека извлекаются, а результат операции размещается в стеке. Посмотрим на примере:
Рисунок 17. Пример расшифровки обозначения растровой операции.
Таким образом, получаем последовательность операций для формирования результата. Однако использовать такую табличку со списком тернарных растровых операций может быть удобно только при получении справок. А вот если мы можем словами описать нужную последовательность действий, а для нее надо определить индекс растровой операции, то такая таблица совершенно неудобна (одного результата можно достичь, выполняя операции различным образом; даже если вы запишите требуемые действия в рассмотренной форме, нет никакой гарантии, что в таблице такая запись найдется).
Попробуем научиться как-то иначе получать индекс тернарной растровой операции.
Мы уже встречались с бинарными растровыми операциями (ROP2) когда рассматривали рисование линий. Сейчас мы воспользуемся примерно таким же подходом — мы будем исходить из предположения монохромных контекстов устройств (для простоты) и попробуем составить табличку, аналогичную той, что применялась для бинарных растровых операций:
Образец, кисть (pattern) | 1 1 1 1 0 0 0 0 |
Исходное изображение (source) | 1 1 0 0 1 1 0 0 |
Существующее изображение (destination) | 1 0 1 0 1 0 1 0 |
Такая табличка позволяет описать все 256 тернарных операций, поэтому приводить ее целиком не имеет смысла. Однако нам будет удобно использовать подобную запись для определения индекса тернарной операции.
Попробуем, например, найти индекс растровой операции, в результате которой мы получим светлую точку, если:
а) контекст–источник имеет светлую точку
б) контекст–источник и контекст–приемник имеют темные точки
в) только в том случае, когда образец содержит темную точку
Имеется в виду операция ( (а) или (б)) и (в). Составим табличку:
Образец, кисть (pattern) | 1 1 1 1 0 0 0 0 |
Исходное изображение (source) | 1 1 0 0 1 1 0 0 |
Существующее изображение (destination) | 1 0 1 0 1 0 1 0 |
Желаемый результат | 0 0 0 0 1 1 0 1 |
Как и в случае бинарных растровых операций мы можем использовать этот результат как номер операции (и заодно как старшее слово индекса). Этот номер равен 0b00001101 = 0x0D. Это уже рассмотренная нами операция с индексом 0x000D0B25 (PDSnaon).
Разобравшись с растровыми операциями, самое время разобраться с функциями, выполняющими эти операции. Самая простая из трех рассмотренных — функция PatBlt. Она не использует контекст–источник и выполняет операцию только над контекстом–приемником и образцом (фоном, полученным в результате закраски текущей кистью).
BOOL PatBlt (hDC, nX, nY, nWidth, nHeight, dwROP);
Эта функция может использоваться со всеми растровыми операциями, не применяющими контекст–источник. Из именованных растровых операций это:
BLACKNESS | — закрасить все черным |
DSTINVERT | — инвертировать изображение (сделать "негатив") |
PATCOPY | — закрасить кистью |
PATINVERT | — закрасить инвертированной кистью |
WHITENESS | — закрасить все белым |
Эта функция часто используется для начальной закраски областей (операции BLACKNESS, WHITENESS, PATCOPY) и для выделения фрагментов (DSTINVERT).
Следующая функция, которую мы рассмотрим:
BOOL BitBlt (
hDestDC, nDestX, nDestY, nDestWidth, nDestHeight,
hSrcDC, nSrcX, nSrcY, dwROP);
Она осуществляет передачу изображений между двумя контекстами устройств, при этом передается прямоугольный фрагмент, который на контексте-приемнике и на контексте-источнике имеет одинаковые размеры. При использовании этой функции надо быть достаточно осторожным — для задания координат и размеров используется логическая система координат, и логический размер изображения в обеих системах может быть различным.
Отдельно надо рассмотреть случай, когда один из контекстов является цветным, а другой черно–белым — при этом особым образом осуществляется преобразование цветов:
при переходе от монохромного к цветному цвет, закодированный 1, соответствует цвету фона (задаваемому функцией SetBkColor), а цвет 0 — цвету текста (функция SetTextColor).
при переходе от цветного к монохромному считается, что если цвет точки совпадает с цветом фона, то эта точка кодируется цветом 1, иначе 0.
Самая мощная функция, выполняющая растровые операции:
BOOL StretchBlt (
hDestDC, nDestX, nDestY, nDestWidth, nDestHeight,
hSrcDC, nSrcX, nSrcY, nSrcWidth, nSrcHeight, dwROP);
позволяет не только передать изображение между разными контекстами, но и осуществить масштабирование изображения. При масштабировании возможно два случая:
изображение увеличивается, то некоторые строки (столбцы) будут дублироваться;
изображение уменьшается, то некоторые строки (столбцы) будут комбинироваться в одну строку (столбец).
Объединение строк (столбцов) при сжатии может осуществляться различными способами, которые выбираются с помощью функции
UINT SetStretchBltMode (hDC, nMode);
параметр nMode задает режим объединения строк:
BLACKONWHITE | выполняется операция И (AND). В результате получается, что черный цвет имеет "приоритет" над белым — сочетание черного с белым рассматривается как черный |
WHITEONBLACK | выполняется операция ИЛИ (OR). При этом "приоритет" принадлежит белому над черным — сочетание черного с белым дает белый |
COLORONCOLOR | при этом происходит простое исключение строк (столбцов). |
HALFTONE 1 | только в Win32 API; происходит усреднение цвета объединяемых точек. |
Независимые от устройства битмапы
У уже рассмотренных зависимых от устройства битмапов имеется один очень серьезный недостаток — их организация отражает организацию видеопамяти того графического устройства, для которого они были спроектированы. При этом возникают большие сложности с переносом битмапов с одного устройства на другое, особенно при создании битмапов, которые будут сохраняться в виде файлов и позже переносится на другие компьютеры. Достаточно универсальным является только лишь монохромный битмап, который легко может быть отображен на любом цветном устройстве, однако такое ограничение является крайне неудобным для конечного пользователя.
Для цветных битмапов сложности возникают даже при их отображении на однотипных устройствах. Например, SVGA адаптеры часто используют логическую палитру, задающую набор воспроизводимых цветов. При отображении одного и того же цветного битмапа на устройствах, использующих разные палитры, результат будет различным.
Наличие этих сложностей привело к появлению новых видов битмапов, так называемых независимых от устройства битмапов (Device Independed Bitmap, DIB). Такой битмап отличается от обычного тем, что дополнительно содержит данные, определяющие соответствие цветов, используемых битмапом, реальным цветам. Благодаря этому независимый от устройства битмап может быть отображен практически на любом графическом устройстве, поддерживающем операции по обмену битовыми образами, с минимальными искажениями цвета.
На практике, начиная с версий Windows 3.x для хранения изображений (в виде .bmp файлов или ресурсов приложения) используются только независимые от устройства битмапы.
Формат независимого от устройства битмапа
Обычно приходится иметь дело с DIB, когда они представлены либо в виде файлов или ресурсов приложения. Поэтому знакомство с DIB мы начнем с формата файла, содержащего DIB.
Некоторые сложности связаны с наличием нескольких различных видов DIB–файлов. Первоначально (в самых ранних версиях Windows и OS/2 использовался битмап в его простейшем виде, называемом в документации форматом OS/2[5]. В дальнейшем, по мере развития GDI появился формат Windows, который для приложений Windows долгое время являлся фактически стандартом. Этот вид битмапов дожил до платформы Win32, когда к нему было добавлено несколько новых возможностей, правда без изменения заголовка. В дальнейшем развитие Windows битмапов пошло стремительно — практически в каждой новой версии Windows добавляется что–то новое и в заголовках битмапов появляются новые поля. Так появились битмапы 4ой версии (для Windows–95 и Windows NT 4.0) и даже 5ой (для Windows NT 5.0). Скорее всего этот процесс так скоро не остановится.
Утешает в этом два соображения:
Первое: все старые форматы битмапов поддерживаются. Таким образом, если ваше приложение само создает битмап, то он будет корректно обрабатываться и в последующих версиях Windows.
Второе: при загрузке битмапа (а он может быть создан в системе, разработанной позже вашего приложения), можно так построить алгоритм, что анализировать заголовок не потребуется. В этом случае ваше приложение опять–таки может использовать новые форматы битмапов (по крайней мере до тех пор, пока вы не собираетесь самостоятельно анализировать изображение).
Рисунок 18. Структура независимого от устройства битмапа.
Собственно независимый от устройства битмап содержит несколько структур данных, описывающих его характеристики. Эти структуры следуют друг за другом непрерывно, без промежутков. Если говорить о структуре DIB в общем, не вдаваясь в подробности описания этих структур данных, то его формат сохраняется во всех существующих версиях Windows.
Заголовок битмапа содержит данные о его размере (размере всего битмапа в байтах) и расстояние от начала файла до хранимого в нем изображения. В таком виде битмап хранится либо в файле, либо в виде ресурсов приложения. Загрузка битмапа может выполняться двумя разными способами:
В простейшем случае все, кроме заголовка файла, помещается в одну область данных (в случае 16ти разрядных платформ надо учитывать, что размер может быть существенно больше 64К). Битмап, загруженный таким образом, называется упакованным (packed DIB).
В другом случае битмап располагается в двух областях — в первой находится заголовок битмапа и данные о соответствии цветов (палитра, либо заменяющие ее данные), а во второй — собственно изображение.
Первый способ удобен при считывании битмапа с диска или из ресурсов приложения, второй — при создании нового битмапа в приложении, когда размер области данных для хранения изображения может быть заранее неизвестен (его можно узнать из заголовка битмапа). Многие функции GDI, работающие с независимыми от устройства битмапами, требуют задания двух указателей: на информацию о битмапе и на данные изображения. Однако некоторые функции ориентированы на использование упакованного битмапа, и тогда требуют задания хендла глобального блока памяти, содержащего упакованный DIB. С этой точки зрения первый способ (с использованием упакованного битмапа) универсален — вы можете легко вычислить указатель на данные изображения внутри единого блока (например, исходя из данных в заголовке файла).
Загрузка независимых от устройства битмапов
Формат заголовка файла одинаков для всех версий битмапов; он описывается структурой BITMAPFILEHEADER:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;
Поле bfType, должно быть содержать две буквы "BM" (значение 0x4D42).
Поле bfSize указывает полный размер файла, включая этот заголовок. Обратите внимание на то, что размер задается двойным словом, так как может существенно превышать 64K. Например битмап 1280x1024, 24 бита/пиксель имеет размер более 3M. Вообще говоря, это поле может быть не заполнено; хотя и крайне редко, но может даже оказаться, что там указана некорректная величина, вместо правильного размера или 0. По крайней мере для битмапов OS/2 в поле bfSize может оказаться величина, равная размеру заголовка файла плюс заголовок битмапа (26). Во всех случаях лучше исходить не из этой величины, а из реального размера файла.
Поля bfReserved1 и bfReserved2 оба содержат 0. По крайней мере так считает Microsoft. В битмапах OS/2 часто эти поля содержат ненулевые данные.
Поле bfOffBits указывает адрес, с которого в данном файле размещаются собственно данные изображения. Этим полем удобно пользоваться для получения размера заголовка битмапа и данных о его цветах, а заодно для вычисления адреса начала данных изображения.
Так, благодаря наличию поля bfOffBits, можно сформулировать универсальный алгоритм загрузки битмапа в память, не зависящий от версии битмапа и его характеристик. В этом примере мы будем ориентироваться на работу с функциями Windows API, что позволяет сделать более компактный, переносимый код, помимо этого введем дополнительную структуру, описывающую DIB. Она будет удобна по двум причинам — во–первых, после загрузки DIB удобно возвращать два указателя, которые могут понадобиться в дальнейшем, плюс хендл блока памяти, содержащего битмап; все это проще хранить в одной структуре. Во–вторых, эту же структуру мы сможем использовать еще раз, когда рассмотрим загрузку битмапов из ресурсов приложения. Подробнее обо всех указателях и их типах — см. в разделе “Заголовок независимого от устройства битмапа”.
#define STRICT
#include <windows.h>
#include <windowsx.h>
// описываем структуру, содержащую информацию о битмапе
typedef struct _DIB {
HGLOBAL hglbDib; // хендл блока памяти или ресурса
LPBITMAPINFOHEADER lpDibHdr; // указатель на заголовок битмапа
LPSTR lpImage; // указатель на изображение
UINT uDibFlags; // флаг 1-загружен из файла, 2-из ресурса
} FAR* LP_DIB;
#define DIB_FILE 1
#define DIB_RESOURCE 2
#define DIB_SIGNATURE 0x4D42
#ifdef __NT__
#define _memcpy_ (to,from,sz) CopyMemory ((LPVOID) (to), (LPVOID) (from), (sz))
#else
#define _memcpy_ (to,from,sz) hmemcpy ((void huge*) (to), (void huge*) (from), (sz))
#endif
BOOL LoadDIBfromFile (LP_DIB lpDib, LPSTR lpszFileName)
{HFILE hFile;
DWORD dwSize;
BITMAPFILEHEADER bmfh;
// инициализируем возвращаемые данные:
lpDib->hglbDib = NULL;
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)NULL;
lpDib->lpImage = (LPSTR)NULL;
lpDib->uDibFlags = 0;
// открываем файл с битмапом для чтения
hFile = _lopen (lpszFileName, READ);
if (hFile == HFILE_ERROR) return FALSE;
// определяем размер упакованного битмапа
dwSize = _llseek (hFile, 0L, 2); _llseek (hFile, 0L, 0);
if (dwSize >= sizeof (bmhf)) dwSize -= sizeof (bmhf);
// выделяем блок для хранения упакованного битмапа
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)GlobalAllocPtr (GHND, dwSize);
if (lpDib->lpDibHdr != (LPBITMAPINFOHEADER)NULL) {
// считываем заголовок файла
if ( (_lread (hFile, &bmhf, sizeof (bmhf)) == sizeof (bmhf)) &&
(bmhf.bfType == DIB_SIGNATURE)) {
// если заголовок успешно считан, считываем сам битмап
if (_hread (hFile, lpDib->lpDibHdr, dwSize) == dwSize) {
// и устанавливаем нужные поля структуры _DIB:
lpDib->hglbDib = GlobalPtrHandle (lpDib->lpDibHdr);
lpDib->lpImage = (LPSTR) (
(char huge*) (lpDib->lpDibHdr) + bmhf.bfOffBits - sizeof (bmhf));
lpDib->uDibFlags = DIB_FILE;}}
// если где-то возникла ошибка - освобождаем память
if (lpDib->uDibFlags == 0) {
GlobalFreePtr (lpDib->lpDibHdr);
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)NULL;}}
_lclose (hFile);
return lpDib->uDibFlags ? TRUE : FALSE;}
Следует обратить внимание на то, что в этой процедуре основная часть кода выполняет проверки или связана с несколько избыточным описанием структуры _DIB; в частных случаях вся процедура может свестись к выполнению 3х–4х функций.
По сути близкий к этому случай может быть связан с загрузкой независимых от устройства битмапов из ресурсов приложения. При рассмотрении зависимых от устройства битмапов было отмечено, что функция LoadBitmap, загружающая битмап из ресурсов приложения, возвращает зависимый от устройства битмап, предназначенный для воспроизведения на дисплее. Это может быть неудобно, если битмап должен отображаться, скажем, на принтере. По счастью в ресурсы приложения включается непосредственно независимый от устройства битмап, что позволяет получить к нему доступ с помощью функций FindResource и LoadResource. В результате вы получите указатель на блок памяти, содержащий целиком образ файла битмапа, включая структуру BITMAPFILEHEADER. Останется только вычислить адрес начала данных изображения и адрес информации о битмапе:
// включаемые заголовки и описание структуры _DIB - см. в предыдущем примере
BOOL LoadDIBfromResources (LP_DIB lpDib, HINSTANCE hInstance, LPSTR lpszResName)
{LPBITMAPFILEHEADER lpbmfh;
HRSRC hresDib;
// инициализируем возвращаемые данные:
lpDib->hglbDib = NULL;
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)NULL;
lpDib->lpImage = (LPSTR)NULL;
lpDib->uDibFlags = 0;
// ищем нужный ресурс
hresDib = FindResource (hInstance, lpszResName, RT_BITMAP);
if (!hresDib) return FALSE;
// ресурс найден, получаем его хендл
lpDib->hglbDib = LoadResource (hInstance, hresDib);
if (! (lpDib->hglbDib)) return FALSE;
// получаем указатель на загруженный ресурс
lpbmfh = (LPBITMAPFILEHEADER)LockResource (lpDib->hglbDib);
if (lpbmfh != (LPBITMAPFILEHEADER)NULL) {
// заполняем остальные поля структуры _DIB:
lpDib->lpDibHdr = (LPBITMAPINFOHEADER) (lpbmfh + 1);
lpDib->lpImage = (char FAR*) (lpbmfh) + bmhf.bfOffBits;
lpDib->uDibFlags = DIB_RESOURCE;}
if (lpDib->uDibFlags == 0) {
#ifndef __NT__
FreeResource (lpDib->hglbDib);
#endif
lpDib->hglbDib = NULL;}
return lpDib->uDibFlags ? TRUE : FALSE;}
Заканчивая рассмотрение функций для загрузки независимых от устройства битмапов из файла или из ресурса приложения, приведем еще одну функцию, освобождающую выделенные ресурсы. Необходимость в этой функции возникает, как только вводится собственная структура _DIB, в которой содержатся хендлы и указатели на выделяемые ресурсы разного типа (блок памяти или ресурс приложения).
BOOL FreeDIB (LP_DIB lpDib)
{BOOL fResult = FALSE;
switch (lpDib->uDibFlags) {
case DIB_FILE:
if (lpDib->lpDibHdr) GlobalFreePtr (lpDib->lpDibHdr);
fResult = TRUE;
break;
case DIB_RESOURCE:
#ifndef __NT__
if (lpDib->hglbDib) {
UnlockResource (lpDib->hglbDib); // для NT не требуется
FreeResource (lpDib->hglbDib);
// для NT не требуется}
#endif
fResult = TRUE;
break;
default:
break;}
// инициализируем структуру _DIB:
lpDib->hglbDib = NULL;
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)NULL;
lpDib->lpImage = (LPSTR)NULL;
lpDib->uDibFlags = 0;
return fResult;}
Заголовок независимого от устройства битмапа
Непосредственно после заголовка файла битмапа следует информация, описывающая характеристики битмапа — его размеры, количество цветов, используемую палитру, режим сжатия изображения и многое другое. Как уже было отмечено, информация о независимых от устройства битмапах условно делится на две структуры данных: 1) описание самого битмапа и 2) описание используемых битмапом цветов.
В разных версиях Windows были предусмотрены разные форматы описания битмапов, по счастью совместимые снизу–вверх. Имеет смысл обзорно ознакомиться с возможностями битмапов разных версий и изменениями, произошедшими в их описании.
Формат OS/2
В ранних версиях GDI для описания битмапов применялись структуры, совместимые с ранним форматом OS/2. Для описания информации о битмапе применялась структура BITMAPCOREHEADER, а для описания используемых цветов — палитры — массив структур RGBTRIPLE (он необязателен):
typedef struct tagBITMAPCOREHEADER { DWORD bcSize; short bcWidth; short bcHeight; WORD bcPlanes; WORD bcBitCount; } BITMAPCOREHEADER; |
typedef struct tagRGBTRIPLE { BYTE rgbtBlue; BYTE rgbtGreen; BYTE rgbtRed; } RGBTRIPLE; |
Сначала рассмотрим структуру BITMAPCOREHEADER, описывающую битмап:
Поле bcSize содержит размер этой структуры (sizeof (BITMAPCOREHEADER)), его значение должно быть равно 12. Поля bcWidth и bcHeight задают размеры данного битмапа. Так как для задания размеров используется целое число со знаком, то максимальный размер битмапа в этого формата равен 32767x32767 пикселей.
Поле bcPlanes указывает количество цветовых планов (плоскостей), используемых битмапом. Его значение для независимого от устройства битмапа всегда должно быть равно 1. Поле bcBitCount указывает количество бит, используемых для задания цвета пикселя. Возможно одно из следующих значений:
1 — монохромный битмап
4 — 16ти цветный битмап
8 — 256ти цветный битмап
24 — битмап в истинных цветах (TrueColor).
Все остальные значения для полей bcPlanes и bcBitCount являются недопустимыми. Если битмап имеет 2, 16 или 256 цветов, то непосредственно после структуры BITMAPCOREHEADER следует палитра (palette) — таблица определения цветов в виде массива из 2, 16 или 256 записей типа RGBTRIPLE. Считается, что изображение такого битмапа содержит логические номера цветов для каждого пикселя, а соответствие логического номера истинному цвету задается соответствующей записью в палитре. Каждая запись RGBTRIPLE задает интенсивности красной (red), зеленой (green) и синей (blue) компонент цвета пикселя, в виде числа от 0 до 255. Таким образом возможно описание 16 777 216 возможных цветов из которых строится палитра, используемая битмапом.
Последний вариант, когда битмап имеет 24 бита на пиксель, предполагает, что 24х битовый номер цвета пикселя соответствует истинному цвету, то есть записи из трех компонент основных цветов RGB (структура RGBTRIPLE сама имеет размер 24 бита). Понятно, что в этом случае палитра становится не нужна и в заголовок битмапа она не помещается вовсе.
Часто для удобства вместо структур BITMAPCOREHEADER и массива записей RGBTRIPLE используют объединенную структуру BITMAPCOREINFO, которая просто описывает в качестве полей структуру BITMAPCOREHEADER и массив из одной записи RGBTRIPLE.
typedef struct _BITMAPCOREINFO {
BITMAPCOREHEADER bmciHeader;
RGBTRIPLE bmciColors[1];
} BITMAPCOREINFO;
Такая структура несколько упрощает доступ к описанию битмапа по указателю: при использовании BITMAPCOREHEADER и RGBTRIPLE необходимо манипулировать с двумя указателями, а при использовании BITMAPCOREINFO достаточно только одного — указывающего на начало заголовка. Например, вместо такого фрагмента кода:
LPBITMAPCOREHEADER lpbmch = ...; // считаем, что указатель на заголовок нам дан
LPRGBTRIPLE lprgbt;
lprgbt = (LPRGBTRIPLE) (lpbmch + 1); // получаем указатель на палитру
// для доступа к полям заголовка используем, например lpbmch->bcWidth
// для доступа к палитре используем, например lprgbt[i].rgbtRed;
Можно использовать чуть более простой фрагмент, в котором применяется только один указатель:
LPBITMAPCOREINFO lpbmci = ...; // считаем, что указатель на заголовок нам дан
// для доступа к полям заголовка lpbmci->bmciHeader.bcWidth
// для доступа к палитре lpbmci->bmciColors[i].rgbtRed;
Однако использовать структуру BITMAPCOREINFO при загрузке битмапа не слишком удобно, так как ее полный размер может быть различным, смотря по количеству цветов битмапа (причем он может быть либо меньше, либо больше, чем sizeof (BITMAPCOREINFO) и заведомо не равен ему). Его можно вычислить как размер структуры BITMAPCOREHEADER (или значение поля bcSize) плюс размер таблицы определения цветов: нуль, если поле bcBitCount равно 24, или число цветов, умноженное на размер структуры RGBTRIPLE:
UINT uSizeCoreInfo;
LPBITMAPCOREHEADER lpbmch;
uSizeCoreInfo = lpbmch->bcSize + (
lpbmch->bcBitCount==24 ? 0 : (1 << lpbmch->bcBitCount) * sizeof (RGBTRIPLE));
Непосредственно вслед за структурой BITMAPCOREINFO следуют собственно данные изображения. Их можно найти в DIB–файле как по значению поля bfOffBits заголовка файла BITMAPFILEHEADER, так и считывая их непосредственно после таблицы определения цветов. Анализируя заголовок битмапа можно определить и необходимый размер области для хранения изображения. Изображение хранится по строкам развертки, в каждой строке для задания цвета пикселя отводится bcBitCount последовательных бит. Полная длина строки выравнивается в сторону завышения до ближайшей границы, кратной двойному слову (в зависимых от устройства битмапах строка выравнивалась до четного размера, а в случае DIB — кратного четырем). Строки развертки перечисляются снизу–вверх. Для вычисления размера изображения можно воспользоваться таким фрагментом:
DWORD dwSizeImage;
LPBITMAPCOREHEADER lpbmch; // считаем, что указатель на заголовок нам дан
dwSizeImage = ( (lpbmch->bcWidth * lpbmch->bcBitCount + 31) >> 3) & ~3L;
dwSizeImage *= lpbmch->bcHeight;
В этом фрагменте выполняются следующие действия: сначала вычисляется длина строки развертки в битах (lpbmch->bcWidth * lpbmch->bcBitCount), далее нам надо получить эту длину в двойных словах (то есть деленную на 32) и округленную в большую сторону; затем пересчитать из двойных слов в байты — умножить на 4. Этот процесс можно несколько ускорить — пересчет в число двойных слов с округлением в большую сторону легко проделать по формуле (x + 31)/32, или, используя более быстрые операции, (x+31)>>5, так как 32 это 25. Далее надо умножить на 4, то есть ((x+31)>>5)*4 = ((x+31)>>5)<<2), или, в окончательном варианте, ((x+31)>>3)& (~3): так как при умножении на 4 (сдвиге влево на 2 бита), младшие 2 разряда будут обнулены, то заменяя деление с умножением на сдвиг вправо, мы должны сбросить два младших бита в 0.
Формат Windows
Достаточно быстро Microsoft решил расширить возможности битмапов, в связи с чем появилась новые версии структур, описывающих битмап: для описания заголовка BITMAPINFOHEADER и для описания палитры RGBQUAD:
typedef struct tagBITMAPINFOHEADER { DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER; |
typedef struct tagRGBQUAD { BYTE rgbBlue; BYTE rgbGreen; BYTE rgbRed; BYTE rgbReserved; } RGBQUAD; |
Первое поле структуры BITMAPINFOHEADER — biSize совпадает по назначению и размеру с полем bcSize структуры BITMAPCOREHEADER. Это поле содержит размер структуры, описывающей данный заголовок. Таким образом, анализируя это поле, можно легко определить, какая версия заголовка используется. Однако здесь имеется один подводный камень — в некоторых ранних источниках времен Windows 3.x утверждается, что все поля этой структуры, начиная с поля biCompression, могут быть пропущены. Собственно в документации, сопровождающей компиляторы есть только одно косвенное упоминание об этом: там строго предупреждается, что для определения размера заголовка битмапа надо обязательно использовать поле biSize, а не sizeof (BITMAPINFOHEADER). Таким образом размер структуры BITMAPINFOHEADER может изменяться от 16 до 40 байт; но в любом случае он превышает размер структуры BITMAPCOIREHEADER (12 байт), что позволяет различать заголовки в разных форматах.
На практике мне только один раз встретился битмап с неполным заголовком. Следует заметить также, что в результате проверки оказалось, что все графические пакеты, с которыми я имел дело, отказываются воспринимать такой битмап и сообщают о неверном формате файла; аналогично реагируют на подобные битмапы и современные системы (проверено для Windows–95, Windows–98, Windows NT 4.0). Фактически можно с достаточной надежностью предполагать, что заголовок будет всегда полным. Такое допущение не принесет сколько–нибудь заметных ограничений в использовании битмапов, созданных другими приложениями. Однако в некоторых случаях можно учесть эту особенность практически без усложнения исходного текста; например, чтение заголовка можно представить таким образом:
// пусть файл с битмапом уже открыт и его хендл = hFile
union {
BIMAPCOREHEADER bmch;
BITMAPINFOHEADER bmih;
} bmh;
DWORD dwSizeHeader;
memset (&bmh, 0, sizeof (bmh));
if (_lread (hFile, &bmh, sizeof (DWORD)) == sizeof (DWORD)) {
dwSizeHeader = bmh.bmih.biSize - sizeof (DWORD);
if (_lread (hFile, &bmh.bmih.biWidth, dwSizeHeader) == dwSizeHeader) {
// заголовок успешно прочитан, все неопределенные поля обнулены if (bmh.bmih.biSize == sizeof (BITMAPCOREHEADER)) {
// OS/2 битмап, анализируем структуру bmh.bmch
} else {
// Windows битмап, анализируем структуру bmh.bmih}}}
Такой прием позволяет считывать битмапы как формата OS/2, так и формата Windows. С некоторым усложнением он может быть в дальнейшем распространен и на более новые форматы битмапов, появившиеся в Windows–95 и Windows NT 4.0.
Коротко познакомимся с остальными полями структуры BITMAPINFOHEADER: Поля biWidth и biHeight задают размеры битмапа. Похоже, что максимальный размер в 32 767 x 32 767 пикселей показался разработчикам Windows слишком скромным, поэтому для задания размеров используются двойные слова со знаком (до 2 147 483 647 x 2 147 483 647 пикселей). Мне, например, битмап, превышающий 30 тысяч пикселей в ширину или высоту, пока еще не встречался.
Поля biPlanes и biBitCount используются так же, как и в заголовке битмапа OS/2, и имеют такие же значения: biPlanes всегда 1, а biBitCount может быть 1, 4, 8 или 24. Аналогично OS/2, если поле biBitCount имеет значение 24, то таблица определения цветов (палитра) пропущена.
Поле biCompression используется, если битмап представлен в сжатом виде, и в этом случае поле biSizeImage указывает реальный размер изображения в байтах. Если используется несжатый формат битмапа, то допустимо указание 0. Вместо чисел, естественно, используются символы BI_RGB (0), BI_RLE4 (1) или BI_RLE8 (2), в зависимости от используемого алгоритма сжатия (RLE–4 или RLE–8), либо несжатый битмап (BI_RGB). Подробнее об алгоритмах сжатия и анализе сжатых битмапов можно узнать из стандартной документации, например, из сопровождающей компиляторы системы помощи.
Поля biXPelsPerMeter и biYPelsPerMeter указывают на рекомендуемые характеристики устройства, на котором будет отображаться битмап. Они могут использоваться, например, для выбора наиболее адекватного битмапа, если предусмотрено несколько вариантов для разных разрешений. Обычно эти поля задают равными 0. Однако, если создаваемый битмап будет отображаться на каком–либо отличном от дисплея устройстве, то эти поля целесообразно задать соответствующими характеристикам устройства, равно как и размер самого битмапа определять исходя из разрешающей способности устройства. Далее такой битмап может легко обрабатываться программами верстки, которые, обнаружив ненулевое значение этих полей, включат его в макет сразу с такими размерами, как требуется.
При этом возникает небольшой нюанс, связанный с тем, что разрешение устройства возвращается функцией GetDeviceCaps в точках на дюйм, а нам требуется задавать в виде числа точек на метр. Возникает необходимость определить соотношение дюйма и метра. Когда я попробовал иметь дело с величиной 25.4 мм/дюйм, то с удивлением обнаружил, что битмап в макете отображается с некоторой погрешностью. Пришлось экспериментально вычислять значение дюйма, принятое в Microsoft (?!); оказалось, что наиболее точный результат дает величина 25.397 мм/дюйм. Фрагмент программы выглядит примерно так:
LPBITMAPINFOHEADER lpbmih = ...; // получаем указатель на BITMAPINFOHEADER
HDC hDC = ...; // контекст устройства вывода
// при вычислениях можно обойтись длинными целыми вместо плавающей запятой,
// пока разрешающая способность устройства не превышает 4294 точек на дюйм,
// а в ближайшем будущем так и будет.
lpbmih->biXPelsPerMeter = (LONG) (
(GetDeviceCaps (hDC, LOGPIXELSX) * 1000000UL) / 25397UL);
В принципе можно вычислить эти величины и другим способом, например так:
lpbmih->biXPelsPerMeter = (LONG) (
(GetDeviceCaps (hDC, HORZRES) * 1000UL) / GetDeviceCaps (hDC, HORZSIZE));
Какой из способов даст более точный результат и каким лучше пользоваться — на усмотрение разработчика. Первый способ использует так называемый «логический дюйм», который даст на устройствах с низким разрешением несколько завешенный результат, зато различимое изображение (особенно это касается текста); помимо этого для многих устройств часто можно выполнить специальную настройку (с помощью панели управления Windows), которая позволит прецизионно установить точные значения. Второй способ отталкивается от физических характеристик устройства и, если они заданы не совсем точно, результат также будет неточным, зато менее зависимым от настройки операционной системы. Например для различных дисплеев часто применяются одни и те–же драйвера, что приводит к тому, что разные дисплеи с разными электронно–лучевыми трубками и разными физическими размерами считаются совершенно одинаковыми. Может быть первый способ предпочтительнее для дисплеев, а второй — для принтеров, размер бумаги для которых стандартизирован куда жестче.
Поле biClrUsed задает количество цветов, задаваемых таблицей определения цветов. Это число может быть меньше, чем число возможных цветов. Если этого поля нет, или его значение 0, то таблица содержит 2, 16 или 256 записей, смотря по количеству бит, отведенных на один пиксель (biBitCount).
Поле biClrImportant определяет число цветов, которые должны быть по возможности точно переданы при отображении битмапа. Значение 0 предполагает, что все цвета должны передаваться как можно точнее. Этим полем можно воспользоваться, если вы сами разрабатываете палитру битмапа — тогда вы можете некоторые цвета (например, цвета, покрывающие большую часть изображения) объявить важными, перечислить их в палитре первыми и этим несколько сократить цветовые искажения при отображении битмапа на устройствах, использующих палитру.
Информация об используемых битмапом цветах размещается сразу после заголовка битмапа в виде массива от 2 до 256 записей типа RGBQUAD или пропущена вовсе, если битмап представлен в истинных цветах. Структура RGBQUAD отличается от RGBTRIPLE только тем, что она дополнена неиспользуемым байтом до границы двойного слова[6].
Аналогично формату OS/2 вводится дополнительная объединяющая структура BITMAPINFO, по смыслу эквивалентная структуре BITMAPCOREINFO.
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO;
Полный размер структуры BITMAPINFO можно определить исходя из размера заголовка (обязательно надо брать значение поля biSize, а не sizeof (BITMAPIFOHEADER)) и размера палитры, вычисляемого с учетом поля biClrUsed:
UINT uSizeDibInfo;
LPBITMAPINFOHEADER lpbmih;
uSizeDibInfo = lpbmih->biSize + (
lpbmih->biClrUsed ? lpbmih->biClrUsed : (
lpbmih->biBitCount > 8 ? 0 : (1 << lpbmih->biBitCount))
) * sizeof (RGBQUAD);
Теоретически, этот фрагмент кода лишь относительно корректен — поле biClrUsed может отсутствовать в структуре BITMAPINFOHEADER. По идее надо сначала проверить значение поля biSize, и только если поле biClrUsed присутствует в структуре, использовать его значение. Однако этот фрагмент может оказаться совершенно корректным, если осуществлять загрузку заголовка битмапа в специально выделенную для этого структуру BITMAPINFOHEADER, с предварительным обнулением всех полей (примерно так, как в примере на странице 56).
Следует еще раз напомнить, что битмапы с неполным заголовком — современными системами не поддерживаются, так что в принципе не будет ошибки, если посчитать заголовок присутствующим полностью. В то же время битмапы с неполной палитрой — почти типичный случай; например обои Windows–95 часто представлены именно в таком виде, поэтому учитывать возможность задания поля biClrUsed необходимо.
Иногда бывает удобно воспользоваться собственным заменителем структуры BITMAPINFO:
struct {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[ 256 + 3 ];
} bitmapheader;
В этой структуре резервируется достаточное пространство для удержания заголовка битмапа и палитры плюс еще некоторая информация (3 дополнительные записи RGBQUAD = 12 байт), о которой будет рассказано ниже, в разделе «Формат Win32 (Windows NT 3.x)».
Определение размера области данных для хранения изображения осуществляется точно также, как и в случае OS/2, за небольшой оговоркой — полученный размер является максимальным. Если используются сжатые битмапы (biCompression равно BI_RLE4 или BI_RLE8), то реальное изображение может оказаться существенно меньшим. Вообще говоря, определение размера сжатого изображения возможно только после того, как это изображение полностью построено — так как возможность сжатия данных и степень сжатия очень сильно зависят от характера самих данных. Таким образом, при выделении пространства под вновь создаваемые битмапы стоит выделять максимально необходимый объем пространства, а при сохранении в сжатом виде этот размер вам вернет GDI, так как собственно сжатие осуществляется именно им.
DWORD dwSizeImage;
LPBITMAPCOREHEADER lpbmih; // считаем, что указатель на заголовок нам дан
dwSizeImage = ( (lpbmih->biWidth * lpbmih->biBitCount + 31) >> 3) & ~3L;
dwSizeImage *= lpbmih->biHeight;
Формат Win32 (Windows NT 3.x)
При расширении возможностей битмапов, реализованных в Win32 API (ранние версии Windows–95, Windows NT 3.x) удалось обойтись без изменения размера заголовка битмапа; изменения коснулись только способов задания некоторых полей и описания цветов. Всего можно перечислить несколько новшеств:
перечисление строк развертки как снизу–вверх, так и сверху–вниз;
добавление двух новых цветовых форматов: 16 и 32 бита на пиксель (так называемые HiColor);
в случае форматов 16 и 32 бита на пиксель новый способ описания цветов — вместо палитры задаются маски цветов.
Рассмотрим эти новшества подробнее.
Во–первых, для того, что бы разобраться в том, какой порядок перечисления строк развертки используется, надо обратить внимание на поле biHeight. Если строки развертки перечисляются сверху–вниз, то это поле будет представлено отрицательной величиной. В связи с этим при определении размеров изображения необходимо использовать абсолютную величину поля biHeight.
DWORD dwSizeImage;
LPBITMAPCOREHEADER lpbmih; // считаем, что указатель на заголовок нам дан
dwSizeImage = ( (lpbmih->biWidth * lpbmih->biBitCount + 31) >> 3) & ~3L;
dwSizeImage *= abs (lpbmih->biHeight); // 1
Во–вторых, новые цветовые форматы (16 и 32 бита на пиксель) первоначально (Windows NT 3.x) требовали нескольких одновременных изменений в битмапе:
палитра отсутствует, так как изображение сохраняется практически в истинных цветах (даже 16 бит на пиксель дает возможность описать 65 536 разных цветов);
вместо палитры записываются три двойных слова, представляющего соответственно маски красной, зеленой и синей компонент (эти маски позволяют GDI разобраться, какие биты в 16ти или 32х битовом номере цвета передают соответствующую компоненту цвета);
поле biCompression задается равным BI_BITFIELDS, что бы подчеркнуть отсутствие палитры и наличие вместо нее масок цветов. Это значение позволяет старым приложениям распознать неподдерживаемый формат битмапа до того, как возникнет ошибка, связанная с попыткой прочитать палитру.
Все три изменения осуществлялись одновременно и были обязательны для HiColor битмапа. Однако по мере развития в этот формат были внесены некоторые изменения. Так, существенно упрощенный GDI в Windows–95 потребовал задания фиксированных масок цветов, работать как в Windows NT с произвольными масками было чересчур сложно[7].
В Windows–95 разрешено применять следующие маски цветов:
Формат | Красный | Зеленый | Синий |
16 бит/пиксель, 5–5–5 (32 768 цветов): | 0x00007C00L | 0x000003E0L | 0x0000001FL |
16 бит/пиксель, 5–6–5 (65 536 цветов) [8]: |
0x0000F800L | 0x000007E0L | 0x0000001FL |
32 бит/пиксель, 8–8–8 (16 777 216 цветов): | 0x00FF0000L | 0x0000FF00L | 0x000000FFL |
Таким образом для 16ти и 32х битовых битмапов появились стандартные маски цветов, которые будут использоваться по умолчанию, если в самом битмапе эти маски не определены; в этом случае поле biCompression задается равным BI_RGB, а не BI_BITFIELDS. Теперь режим BI_BITFIELDS не обязательно должен устанавливаться для 16ти и 32х битовых битмапов, он используется только в том случае, если заголовок битмапа содержит маски.
Если маски присутствуют, то они перечисляются сразу за заголовком битмапа (BITMAPINFOHEADER) в приведенном в таблице порядке — красный, зеленый и синий цвета.
Кроме того, в случае 16ти, 24х или 32х бит на пиксель и режима BI_RGB появилась возможность задавать палитру (поле biClrUsed должно быть ненулевым — палитра для максимально допустимого числа цветов в этих форматах чересчур громоздка). Смысл включения палитры теперь связан не с необходимостью задавать соответствие номеров цветов реальным цветам, а с возможностью оптимизировать процесс отображения битмапа, задавая рекомендуемую для него палитру. Это реально может иметь место при воспроизведении HiColor или TrueColor битмапов на устройствах, поддерживающих палитру — для повышения качества цветопередачи такому устройству целесообразно назначить палитру, оптимизированную для этого битмапа. Если этого не сделать, то все множество цветов битмапа будет приводится к той палитре, которая уже используется устройством — скорее всего это будет системная палитра.
Еще позже в документации появились указания о том, что возможно одновременное задание масок цветов и палитры — после заголовка сначала размещаются маски цветов и только затем палитра, размер которой определяется полем biClrUsed заголовка битмапа. Помимо этого, также можно найти замечание о том, что маски могут быть заданы для всех режимов, в которых число бит/пиксель равно или превышает 16, включая TrueColor (24 бита/пиксель). На практике эти расширения большинством приложений не поддерживаются.
Для проверки использовался стандартный MS Paint, который сам по возможности не выполняет анализа изображений и всю работу старается передать GDI. Это позволяет использовать его в качестве теста на возможности GDI. Для разных платформ Windows были получены следующие результаты:
Windows 3.11 |
Поддерживает режимы 1, 4, 8, 16, 24, 32 бит/пиксель для BI_RGB; Режим BI_BITFIELDS не поддерживается[9]. |
Windows–95 | Поддерживает режимы 1, 4, 8, 16, 24, 32 бит/пиксель для BI_RGB; |
Windows–98 Windows NT 4.0 |
сверх того, что может Windows–95: Поддерживает режимы 16 и 32 бит/пиксель для BI_BITFIELDS; Для режима 24 бита/пиксель задание масок (и BI_BITFIELDS) не поддерживается. Для режимов 16, 24, 32 возможно задание палитры как в BI_RGB так и в BI_BITFIELDS; |
Однако, при работе в 256ти цветном режиме осталось впечатление, что необязательная для 16ти, 24х и 32х бит/пиксель битмапов палитра просто игнорируется, даже если присутствует. Однако это особенность MS Paint, а не GDI. К сожалению, остальные проверенные приложения (например, Photo Shop 5.0) вообще отказались работать с HiColor форматами (16 и 32 бит/пиксель).
Это значит, что для экспорта изображений приложение должно использовать по возможности старые, проверенные и широко распространенные форматы 1, 4, 8 бит/пиксель с полной палитрой (в случае OS/2 приходилось наблюдать ошибки при чтении битмапов с сокращенной палитрой); либо TrueColor в стандартном варианте — без палитры и без масок цветов. А вот при чтении битмапа целесообразно допускать все эти возможные варианты, что обеспечит совместимость с битмапами, создаваемыми другими приложениями, даже в ближайшем будущем.
Таким образом для битмапов Win32 надо обращать внимание на:
возможно отрицательную высоту битмапа;
режим сжатия BI_BITFILEDS — если он задан, то после заголовка есть 3 двойных слова с масками цветовых компонент; если же задан режим BI_RGB, BI_RLE4 или BI_RLE8, то масок нет (предполагаются стандартные маски 5–5–5 или 8–8–8);
для форматов 1, 4 и 8 бит на пиксель палитра обязательна, а для 16, 24 и 32 бит на пиксель палитра может отсутствовать (то есть нулевое значение biClrUsed интерпретируется либо как максимальный размер палитры, либо как ее отсутствие — смотря по числу бит на пиксель). Для HiColor или TrueColor режимов палитра является лишь рекомендуемой, облегчающей процесс отображения полноцветного битмапа на устройстве, поддерживающем палитры. Именно поэтому в примере на странице 57 при определении размера палитры значение поля biBitCount сравнивалось с 8, а не проверялось строгое равенство 24 битам на пиксель — максимальный размер палитры определен только для 2х, 16ти и 256ти цветных битмапов, а для форматов с 16тью, 24мя и 32мя битами на пиксель для задания палитры необходимо задать поле biClrUsed. По умолчанию в HiColor и TrueColor битмапах палитра отсутствует. Если задан и режим BI_BITFIELDS, и biClrUsed не равен 0, то палитра размещается непосредственно после масок.
Сохранение независимого от устройства битмапа
Для сохранения битмапа необходимо разобраться со всеми необходимыми структурами данных, заполнить их, а затем записать в файл. Задачу можно существенно упростить, если считать, что битмап загружен в виде «Packed DIB», что существенно позволяет сохранение битмапа свести, аналогично чтению, к нескольким функциям.
В ранее приводимых примерах я использовал собственную структуру _DIB, описывающую загруженный в память DIB. Она определена в примере на странице 52, вместе с включением необходимых заголовочных файлов и определением вспомогательных символов. Помимо этого она будет применяться в данном примере, а также с ее помощью будут выполнены операции по преобразованию DIB в DDB (стр. 63) и наоборот — DDB в DIB (стр. 64).
Поскольку формат независимого битмапа в предыдущих разделах был рассмотрен достаточно подробно, то сейчас можно прямо перейти к исходному тексту.
BOOL StoreDIBtoFile (LPSTR lpszFileName, LP_DIB lpDib) {
BITMAPFILEHEADER bmfh; // заголовок файла битмапа
HFILE hf;
BOOL a = FALSE;
DWORD dwSize;
hf = _lcreat (lpszFileName, 0);
if (hf != HFILE_ERROR) {
// заполняем и записываем заголовок файла
bmfh.bfType = DIB_SIGNATURE;
bmfh.bfSize = 0L;
bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
bmfh.bfOffBits =
sizeof (BITMAPFILEHEADER) +
(LPSTR) (lpDib->lpImage) - (LPSTR) (lpDib->lpDibHdr);
// в нашем случае это всегда «Packed DIB», поэтому разница двух указателей
// возвратит расстояние между ними.
if (lpDib->lpDibHdr.biSize == sizeof (BITMAPCOREHEADER)) {
// определяем размер изображения
#define lpbmch ((LPBITMAPCOREHEADER) (lpDib->lpDibHdr))
dwSize = ( (lpbmch->bcWidth * lpbmch->bcBitCount + 31) >> 3) & ~3L;
dwSize *= lpbmch->bcHeight;
#undef lpbmch
// прибавляем размер заголовков и палитры
dwSize += bmfh.bfOffBits;
} else {
// размер изображения можно получить из BITMAPINFOHEADER
dwSize = bmfh.bfOffBits + lpDib->lpDibHdr.biSizeImage;}
_hwrite (hf, (LPVOID)&bmfh, sizeof (bmfh));
// записываем собственно DIB
_hwrite (hf, (LPVOID) (lpDib->lpDibHdr), dwSize - sizeof (BITMAPFILEHEADER));
_lclose (hf);
a = TRUE;}
return a;}
Большая часть трудностей, связанных с анализом информации о битмапе, переносится на функции, осуществляющие загрузку битмапа (LoadDIBfromFile, стр. 52, LoadDIBfromResources, стр. 52) и преобразование из DDB в DIB (ConvertDDBtoDIB, стр. 64)
Отображение независимого от устройства битмапа
Для отображения DIB существует несколько возможных способов. Два из них аналогичны функциям BitBlt и StretchBlt, но используют в качестве исходных данных не контекст устройства, а независимый от устройства битмап.
Эти функции используют указатель на структуру BITMAPINFO (или BITMAPCOREINFO), задающую характеристики битмапа и таблицу определения цветов, а также указатель на данные изображения. Эти указатели могут указывать на разные части одного блока данных, содержащего весь битмап, или вообще на разные блоки данных, если вам так удобнее.
Сначала мы рассмотрим функцию, осуществляющую перенос изображения из битмапа на устройство, не изменяющее размеров изображения.
int SetDIBitsToDevice (
hDC, nDestX, nDestY, nDestWidth, nDestHeight,
nSrcX, nSrcY, nStartScan, nCountScan, lpImage, lpDibInfo, nColorUse);
Параметры hDC, nDestX, nDestY, nDestWidth и nDestHeight указывают устройство, на котором осуществляется отображение битмапа, положение и размер выводимого изображения. Существенно отметить, что в данном случае используется система координат устройства.
Параметры nSrcX и nSrcY задают положение исходного прямоугольного изображения в битмапе. Если вы задаете эти координаты не 0, то помните, что у независимых от устройства битмапов начало отсчета системы координат помещается в левый–нижний угол, что несколько необычно.
Параметр lpDibInfo является указателем на структуру BITMAPINFO (BITMAPCOREINFO), определяющую характеристики битмапа, а параметр lpImage указывает на область памяти, содержащую данные изображения.
Еще два параметра nStartScan и nCountScan указывают на фрагмент битмапа, определенный в области lpImage. nStartScan указывает номер строки развертки, с которой начинается изображение, а nCountScan указывает число строк, отображаемых этой операцией. С помощью этих параметров можно разбить процесс вывода одного большого битмапа на несколько вызовов функции SetDIBitsToDevice, каждый из которых перенесет только небольшую часть строк изображения. Это может существенно сократить требуемое для отображения битмапа количество памяти (полная картинка 1280 x 1024, 24 бита/пиксель занимает более 3M).
Последний параметр nColorUse определяет применение цветов битмапом. Он может быть DIB_RGB_COLORS, если таблица определения цветов содержит записи RGBQUAD; или он может быть DIB_PAL_COLORS, если таблица определения цветов содержит массив 16ти разрядных номеров цветов в текущей палитре.
В Windows требуется, что бы таблица определения цветов содержала записи RGBQUAD[10], если битмап сохранен в виде файла, в виде ресурсов приложения, или если он каким–либо способом передается другому приложению. Таким образом DIB_PAL_COLORS может применяться, только если вы сами создаете и используете DIB, не сохраняя его и никому не передавая, и при этом текущая системная палитра полностью удовлетворяет вашим требованиям, что весьма и весьма редкий случай. Более того, при использовании DIB_PAL_COLORS вы обязаны проследить, что бы количество цветов, определяемых индексами было четным. Это связано с тем, что строки растра в DIB выравниваются на границу двойного слова и, одновременно, должны начинаться на такой границе. Размер заголовка (BITMAPINFOHEADER) кратен 4, одна запись RGBQUAD тоже имеет длину 4 байта; таким образом при использовании DIB_RGB_COLORS строка растра всегда начнется на границе двойного слова. А вот в случае DIB_PAL_COLORS одна запись в таблице определения цветов — 16ти разрядное число, тогда вам необходимо проследить, что бы таблица содержала четное число записей и ее длина была бы кратна 4 байтам.
Функция возвращает число строк развертки, перенесенных данной операцией.
Следующая рассматриваемая нами функция осуществляет перенос изображения с одновременным изменением размеров изображения:
int StretchDIBits (
hDC, nDestX, nDestY, nDestWidth, nDestHeight,
nSrcX, nSrcY, nSrcWidth, nSrcHeight, lpImage, lpDibInfo,
nColorUse, dwROP);
Параметры hDC, nDestX, nDestY, nDestWidth, nDestHeight задают устройство, на котором осуществляется отображение, положение и размеры изображения, как оно должно быть отображено.
Параметры nSrcX, nSrcY, nSrcWidth, nSrcHeight задают положение и размеры исходного изображения в битмапе. Начало отсчета системы координат битмапа находится в левом–нижнем углу битмапа, единицы соответствуют пикселям битмапа.
Параметр lpDibInfo указывает на структуру BITMAPINFO (BITMAPCOREINFO), а lpImage на буфер, содержащий данные изображения.
Параметр nColorUse указывает на способ задания таблицы определения цветов, обычно DIB_RGB_COLORS, а параметр dwROP задает индекс растровой операции, выполняемой при переносе изображения.
Так как при переносе может изменяться размер изображения, то функция использует текущий режим сжатия изображения, заданный функцией SetStretchBltMode.
Функция возвращает число строк развертки, перенесенных данной операцией.
Использование промежуточного DDB при работе с DIB
Надо отметить, что функции, отображающие DIB, заметно уступают в скорости функциям, копирующим обычный битмап из одного контекста в другой. Поэтому, если вы многократно осуществляете отображение одного и того же битмапа, то часто удобнее использовать вместо функций SetDIBitsToDevice и StretchDIBits функции по отображению обычного, зависимого от устройства, битмапа, который должен быть предварительно создан из DIB.
Этот способ имеет еще одно достоинство — он позволят изменить характеристики битмапа: размеры, цветовой формат, реорганизовать палитру.
Основа способа проста:
сначала надо загрузить DIB (обычно в виде «Packed DIB»)
преобразовать DIB в DDB с помощью функции CreateDIBitmap
выполнить требуемые операции над DDB (см. «Работа с зависимым от устройства битмапом», стр. 45)
осуществить обратное преобразование DDB в DIB с помощью функции GetDIBits
Первая операция — загрузка DIB — уже подробно рассмотрена в разделе «Загрузка независимых от устройства битмапов.», на ней останавливаться сейчас не будем. Вторая операция позволяет получить хендл DDB (HBITMAP), который можно использовать для выполнения требуемых операций над битмапом. Так, например, такой битмап может быть выбран в совместимый контекст устройства или использован для создания кисти и т.д.
Следует отметить, что определяя свойства преобразования DIB в DDB надо правильно определить цель такого преобразования. Если вы планируете выполнить редактирование DIB, то важно не испортить хранимое изображение; часто может быть целесообразно назначить цветовое разрешение DDB достаточно высоким, скажем TrueColor, даже если он реально будет отображаться на устройстве, имеющем существенно меньшее цветовое разрешение. Либо воспользоваться DIB–секциями, которые также возвращают HBITMAP. Однако еще чаще встречается другой вариант — преобразование DIB в DDB выполняется для ускорения процесса вывода. Так, например, если битмап применяется для закраски фона окна (в качестве «обоев»), то особую важность приобретает скорость вывода, в то время как качество цветопередачи должно только лишь соответствовать возможностям аппаратуры и применяемому режиму.
Преобразование DIB в DDB
Для этого надо воспользоваться функцией:
HBITMAP CreateDIBitmap (hDC, lpDibInfoHdr, dwInit, lpImage, lpDibInfo, nColorUse);
которая создает вовсе не независимый от устройства битмап, как можно решить, глядя на название. Эта функция создает самый обыкновенный, зависимый от устройства битмап по указанному независимому от устройства.
Параметр hDC задает контекст устройства, с которым будет совместим создаваемый битмап.
Параметр lpDibInfoHdr является указателем на структуру BITMAPINFOHEADER (BITMAPCOREHEADER), а lpDibInfo — на структуру BITMAPINFO (BITMAPCOREINFO). Так как BITMAPINFO начинается с заголовка BITMAPINFOHEADER, то оба указателя обычно одинаковы.
Параметр dwInit указывает, надо ли осуществлять инициализацию изображения обычного битмапа данными изображения DIB. Если надо, то он должен быть равен CBM_INIT, иначе 0. Нулевое значение dwInit применяется только если битмап создается в несколько приемов: сначала создается битмап как объект GDI, а затем на него переносится изображение.
Параметр lpImage указывает на данные изображения DIB, а nColorUse задает тип таблицы определения цветов.
Если вы решили при создании обычного битмапа не инициализировать его изображение, то можете выполнить эту операцию позже, применяя функцию:
int SetDIBits (hDC, hBmp, nStartScan, nCountScan, lpImage, lpDibInfo, nColorUse);
Параметр hDC задает контекст устройства, а hBmp — обычный битмап, который инициализируется этой функцией. Надо следить, что бы данный битмап не был выбран в контекст устройства в момент вызова функции SetDIBits.
Параметры nStartScan и nCountScan задают переносимую за одну операцию полосу независимого от устройства битмапа. С помощью этих параметров можно разбить перенос одного большого битмапа на несколько операций, аналогично функции SetDIBitsToDevice.
Параметр lpImage указывает на буфер данных, lpDibInfo является указателем на структуру BITMAPINFO, содержащую заголовок и таблицу определения цветов, а nColorUse указывает на тип применяемой таблицы определения цветов (обычно DIB_RGB_COLORS).
Функция возвращает число перенесенных этой операцией строк развертки.
Использовать зависимый от устройства битмап вместо DIB имеет смысл при многократном отображении битмапа, например, если битмап отображается в окне: в этом случае перерисовка будет осуществляться при обработке каждого сообщения WM_PAINT. Типичный пример использования зависимого битмапа для отображения приведен ниже. В этом примере предполагается, что:
в созданном окне будет отображаться битмап, находящийся в файле с именем C:\TEST\MY.BMP
используются распаковщики сообщений из windowsx.h
для загрузки битмапа из файла применяется функция LoadDIBfromFile, приведенная на странице 52, а для освобождения занятых им ресурсов — функция FreeDIB (см. страницу 53)
Загрузка битмапа осуществляется однократно при создании окна (при обработке сообщения WM_CREATE, в примере — в функции Cls_OnCreate), тогда же DIB преобразуется в зависимый от устройства и, так как он больше не нужен, уничтожается. Хендл зависимого от устройства битмапа сохраняется в структуре описания окна, в двойном слове со смещением 0 (см. функции GetWindowLong и SetWindowLong).
Далее, при необходимости перерисовать битмап (сообщение WM_PAINT, функция–обработчик Cls_OnPaint) зависимый от устройства битмап отображается с помощью функции BitBlt.
Когда окно закрывается (сообщение WM_DESTROY, функция Cls_OnDestroy) находящийся в памяти зависимый от устройства битмап уничтожается, так как все занятые ресурсы GDI должны быть обязательно освобождены.
// включаемые файлы и описание структур см. в примере на странице 52
HBITMAP ConvertDIBtoDDB (LP_DIB lpDib)
{HDC hdc;
HBITMAP hbmp;
// для преобразования DIB в обычный битмап нужен контекст устройства,
// на котором будет осуществляться отображение (окно, дисплей, принтер)
hdc = GetWindowDC (NULL); // используем контекст всего дисплея
// создаем зависимый битмап и запоминаем его хендл
hbmp = CreateDIBitmap (
hdc, lpDib->lpDibHdr, CBM_INIT, lpDib->lpImage,
(LPBITMAPINFO) (lpDib->lpDibHdr), DIB_RGB_COLORS);
ReleaseDC (NULL, hdc); // освобождаем контекст дисплея
return hbmp;}
BOOL Cls_OnCreate (HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{_DIB dib;
// загружаем битмап из файла
if (LoadDIBfromFile (&dib, "C:\\TEST\\MY.BMP")) {
// создаем зависимый битмап и запоминаем его хендл
// предположим, что при регистрации класса окон мы зарезервировали
// 4 байта в структуре описания окна; там мы сохраним хендл DDB,
// который будет отображаться в окне.
SetWindowLong (hwnd, 0, (LONG)ConvertDIBtoDDB (&dib));
FreeDIB (&dib); // освобождаем память, занятую DIB}
return TRUE;}
void Cls_OnPaint (HWND hwnd)
{PAINTSTRUCT ps;
BITMAP bmp;
HBITMAP hBmp;
BeginPaint (hwnd, &ps);
// получаем хендл битмапа и узнаем его характеристики
hBmp = (HBITMAP)GetWindowLong (hwnd, 0);
GetObject (hBmp, sizeof (bmp), &bmp);
// создаем совместимый контекст и выбираем в него битмап
hCDC = CreateCompatibleDC (ps.hdc);
SelectBitmap (hCDC, hBmp);
// отображаем битмап в окне
BitBlt (ps.hdc, 0,0, bmp.bmWidth,bmp.bmHeight, hCDC, 0,0, SRCCOPY);
// удаляем совместимый контекст (битмап при этом сохранится)
DeleteDC (hCDC);
EndPaint (hwnd, &ps);}
void Cls_OnDestroy (HWND hwnd)
{HBITMAP hBmp;
// получаем хендл битмапа
hBmp = (HBITMAP)GetWindowLong (hwnd, 0);
if (hBmp) {
// если битмап существует, удаляем его
DeleteBitmap (hBmp);
SetWindowLong (hwnd, 0, 0L);}}
Обратное преобразование DDB в DIB
Кроме того Вы можете осуществить обратную операцию, перенеся данные обычного битмапа в независимый от устройства битмап. Это делается с помощью функции:
int GetDIBits (hDC, hBmp, nStartScan, nCountScan, lpImage, lpDib, nColorUse);
Параметры и возвращаемое этой функцией значение такое же, как и для функции SetDIBits.
Параметр lpImage может быть NULL, тогда эта функция не переносит данных битмапа, а только лишь заполняет структуру BITMAPINFO. Эта особенность очень часто используется на практике — при необходимости получить информацию о битмапе структура BITMAPINFOHEADER обнуляется, в нее записывается самая необходимая информация (размер структуры biSize, размеры изображения biWidth и biHeight, информация о формате битмапа biPlanes, biBitCount и biCompression), после чего вызывается функция GetDIBits с нулевым указателем на область данных изображения. GDI при этом просто заполняет структуру BITMAPINFOHEADER остальными данными, в том числе вычисляет размер области, необходимой для хранения данных (поле biSizeImage). В дальнейшем очень просто выделить блок памяти необходимого размера и повторным вызовом функции GetDIBits получить непосредственно само изображение.
// включаемые файлы и описание структур см. в примере на странице 52
BOOL ConvertDDBtoDIB (LP_DIB lpDib, HBITMAP hbmp)
{HDC hdc;
DWORD dwSize;
BITMAPFILEHEADER bmfh;
struct {
BITMAPINFOHEADER bmih;
RGBQUAD palette[ 256 + 3 ];
} bmh;
BITMAP bm;
// инициализируем возвращаемые данные:
lpDib->hglbDib = NULL;
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)NULL;
lpDib->lpImage = (LPSTR)NULL;
lpDib->uDibFlags = 0;
// для преобразования DDB в DIB нужен контекст устройства
hdc = GetWindowDC (NULL); // используем контекст всего дисплея
// получаем информацию об обычном битмапе
GetObject ( (HGDIOBJ)hbmp, sizeof (bm), (LPVOID)&bm);
bmh.bmih.biSize = sizeof (bmih);
bmh.bmih.biWidth = bm.bmWidth;
bmh.bmih.biHeight = bm.bmHeight;
bmh.bmih.biPlanes = (WORD)1;
bmh.bmih.biBitCount = bm.bmPlanes * bm.bmBitsPixel;
// определяем формат битмапа
bmh.bmih.biCompression = ( (bmh.bmih.biBitCount == 16) || (bmh.bmih.biBitCount == 32)) ?
BI_BITFIELDS : BI_RGB;
// обнуляем остальные поля
bmh.bmih.biSizeImage = bmh.bmih.biXPelsPerMeter = bmh.bmih.biYPelsPerMeter =
bmh.bmih.biClrUsed = bmh.bmih.biClrImportant = 0L;
// определяем все остальные данные
GetDIBits (
hdc, hbmp, 0, abs (bmh.bmih.biHeight),
(LPVOID)0L, (LPBITMAPINFO)&bmh, DIB_RGB_COLORS);
// выделяем необходимое пространство
dwSize = sizeof (BITMAPFILEHEADER) +
( (bmh.bmih.biCompression == BI_BITFIELDS) ? sizeof (DWORD)*3 : 0L) +
sizeof (RGBQUAD) * (bmh.bmih.biClrUsed ? bmh.bmih.biClrUsed :
(bmh.bmih.biBitCount <= 8 ? 1<<bmh.bmih.biBitCount : 0));
lpDib->lpDibHdr = (LPBITMAPINFOHEADER)GlobalAllocPtr (GHND, dwSize+bmh.bmih.biSizeImage);
if (lpDib->lpDibHdr != (LPBITMAPINFOHEADER)NULL) {
// уточняем информацию
bmh.bmih.biXPelsPerMeter = (LONG) (
(GetDeviceCaps (hdc, HORZRES) * 1000UL) / GetDeviceCaps (hdc, HORZSIZE));
bmh.bmih.biYPelsPerMeter = (LONG) (
(GetDeviceCaps (hdc, VERTRES) * 1000UL) / GetDeviceCaps (hdc, VERTSIZE));
// копируем заголовок битмапа
_memcpy_ (lpDib->lpDibHdr, &bmh, dwSize);
// получаем изображение
lpDib->lpImage = (LPSTR) (lpDib->lpDibHdr) + dwSize;
GetDIBits (
hdc, hbmp, 0, abs (bmh.bmih.biHeight),
(LPVOID) (lpDib->lpImage), (LPBITMAPINFO) (lpDib->lpDibHdr), DIB_RGB_COLORS);
// и устанавливаем остальные поля структуры _DIB:
lpDib->hglbDib = GlobalPtrHandle (lpDib->lpDibHdr);
lpDib->uDibFlags = DIB_FILE; // выделенное пространство должно быть освобождено}
ReleaseDC (NULL, hdc); // освобождаем контекст дисплея
return lpDib->uDibFlags ? TRUE : FALSE;}
Создание ассоциаций DIB с контекстом устройства
Эти операции надо выполнять различными способами, в зависимости от API:
1) Для Windows API удобно использование так называемого DIB драйвера. В том виде, в каком было формально описано применение DIB драйвера, этот путь практически ничего не дает — слишком много ограничений на использование этого драйвера и на использование получаемого контекста устройства.
2) Для Win32 API можно создать специальную «DIB секцию», которая описывает DIB, но с помощью хендла HBITMAP, применяемого для описания DDB. Это позволяет связать DIB с контекстом устройства и выполнить рисование непосредственно на нем. Этот путь является во многих случаях предпочтительным, особенно с учетом несколько неудачной реализации функции CreateDIBitmap в некоторых версиях Win32 (CreateDIBitmap на некоторых платформах выделяет пространство для хранения битмапа в куче, используемой GDI, в то время как для DIB–секций пространство всегда выделяется в куче, используемой по умолчанию).
DIB драйвер (Windows API)
Как видно из предыдущего материала, самая сложная операция — сохранение измененного независимого от устройства битмапа. При этом необходимо выполнить значительный объем операций и, кроме этого, приходится анализировать заголовки битмапа, различающиеся в разных версиях. Операция загрузки битмапа из файла или из ресурсов осуществляется качественно проще и, что очень удобно, алгоритм не зависит от версии битмапа.
По–видимому это подтолкнуло разработчиков Microsoft включить в состав распространяемых с компилятором компонент специальный DIB–драйвер (DIB.DRV)[11], который позволяет модифицировать существующий DIB, не анализируя его заголовок. Используя этот драйвер вы можете легко получить хендл контекста устройства, связанного с указанным вами упакованным DIB. Далее все операции рисования на этом контексте устройства будут сопровождаться изменениями DIB. После уничтожения созданного контекста устройства в упакованном DIB сохранится измененное изображение.
Во всем этом есть два существенных но:
— созданный таким образом контекст не может быть использован для операций передачи растровых изображений. То есть отобразить данный DIB с помощью операций BitBlt или StretchBlt невозможно — для этого необходимо осуществить отображение битмапа, обращаясь к его упакованному представлению. Однако самая сложная операция — получение измененного изображения в виде упакованного DIB — осуществляется без вашего участия.
— DIB драйвер не является компонентом операционной системы, так что получить его можно только в составе redistribution kit, сопровождавшем компиляторы во времена Windows 3.x; к сожалению в современные компиляторы он не включен и мне пока не попадалось его версий для Win32 API.
Для того, что бы создать контекст устройства, ассоциированный с DIB, Вы должны использовать функцию CreateDC (см. также раздел «Получение хендла контекста устройства», стр. 5):
HDC hDibDC = CreateDC ("DIB", NULL, NULL, lpPackedDib);
// рисуем на контексте, после чего его освобождаем
DeleteDC (hDibDC);
Параметр со значением "DIB" задает имя драйвера (DIB.DRV), два другие должны быть NULL, а последний параметр указывает на данные, передаваемые драйверу при создании контекста устройства. Для DIB.DRV он должен указывать на упакованный DIB, то есть на заголовок битмапа BITMAPINFOHEADER (BITMAPCOREHEADER), сопровождаемый всеми необходимыми данными (палитрой и данными изображения). После использования такого контекста вы должны его уничтожить с помощью функции DeleteDC.
При работе с DIB–драйвером необходимо учитывать, что обрабатываемые битмапы не должны быть сжатыми (BI_RLE4 или BI_RLE8), так как:
а) драйвер не может осуществлять сжатие битмапа заново после каждой операции рисования,
б) необходимый для хранения битмапа объем памяти может увеличиваться в процессе рисования, так как он зависит от реально достигнутой степени сжатия, разной для разных изображений, а размер блока памяти, содержащего упакованный DIB, определяется вами и не может изменяться в процессе рисования на контексте.
DIB–cекция (Win32 API)
Win32 API содержит специальную функцию CreateDIBSection, которая создает хендл DDB битмапа (HBITMAP), ассоциированный не с DDB, а с независимым от устройства битмапом. Таким образом существует возможность выполнения над DIB всех операций, типичных для DDB. Так, например, можно получить HBITMAP, соответствующий DIB и выбрать его в контекст устройства.
HBITMAP CreateDIBSection (hdc, lpbmi, nColorUse, ppvBits, hSection, dwOffset); 1
Параметр hdc задает хендл контекста устройства, информация о цветах и палитре которого используется когда параметр nColorUse равен DIB_PAL_COLORS.
Параметр lpbmi является указателем на структуру BITMAPINFO, содержащую заголовок битмапа и, при необходимости, палитру или маски (для HiColor режимов). BITMAPINFOHEADER, являющийся частью BITMAPINFO содержит информацию об организации битмапа и его размерах.
С помощью параметра nColorUse задается способ использования палитры. Значение DIB_RGB_COLORS указывает, что палитра битмапа содержит таблицу записей RGBQUAD (RGBTRIPLE), а значение DIB_PAL_COLORS указывает, что вместо палитры битмапа размещен массив целых чисел, являющихся индексами цветов в системной палитре.
Параметр ppvBits является указателем на переменную типа LPVOID. В эту переменную будет записан указатель на начало данных изображения.
Два последних параметра: hSection и dwOffset используются, если битмап содержится в проецируемом в память файле. В этом случае hSection является хендлом проецирования, возвращенном функцией CreateFileMapping, а dwOffset — смещение от начала файла до данных изображения. В описании указывается, что функция CreateDIBSection требует, что бы значение dwOffset было кратно 4 (длина строки растра в DIB всегда выравнивается на границу двойного слова). Если в проецируемом файле содержится так называемый «Packed DIB», то есть битмап без заголовка файла, то смещение до начала данных изображения само собой будет кратно 4 байтам[12].
Однако в нормальных файлах битмапов заголовок файла присутствует. Он описывается структурой BITMAPFILEHEADER, которая имеет размер 14 байт. Очевидно, что 14 не кратно 4. И, как следствие, для большинства битмапов суммарный размер заголовка файла, заголовка битмапа и данных о цветах (палитры или масок) не может быть кратен 4 (!). Размер структуры BITMAPINFOHEADER равен 40. Суммарный размер обоих заголовков равен 54 и не кратен 4. Палитра, состоящая из записей RGBAUAD по 4 байта каждая, либо маски цветов — три двойных слова никак не могут выровнять конец заголовка по границе двойного слова. В тоже время, если при вызове функции CreateDIBSection задать величину dwOffset не кратную 4, то функция вернет NULL, хотя код ошибки (возвращаемый функцией GetLastError) не будет установлен. Как результат — обычные битмапы в виде файлов нельзя спроецировать в память и передать функции CreateDIBSection.
В итоге функция CreateDIBSection может легко применяться для создания нового DIB — в этом случае hSection и dwOffset следует задать равными 0. Тогда GDI сам создаст необходимое проецирование и вернет хендл битмапа. При необходимости сохранения DIB в виде файла можно с помощью функции GetObject прочитать информацию о DIB–секции:
DIBSECTION ds; // 1
int GetObject (hbmpDIBSection, sizeof (DIBSECTION), &ds);
Структура DIBSECTION содержит следующую информацию:
typedef struct _DIBSECTION {// 1
BITMAP dsBm;
BITMAPINFOHEADER dsBmih;
DWORD dsBitfields[ 3 ];
HANDLE dshSection;
DWORD dsOffset;
} DIBSECTION;
Поле dsBm содержит структуру BITMAP, описывающую секцию как DDB; В этой структуре можно прочитать поле bmBits (в примере выше это будет ds.dsBm.bmBits), которое является указателем на данные изображения DIB–секции. Этот указатель совпадает с тем, который возвращается в параметре ppvBits при вызове функции CreateDIBSection и может быть использован функциями работы с DIB.
Поле dsBmih описывает секцию как DIB; В основном значения полей этой структуры совпадают с теми, которые были указаны при создании секции. Однако, если вы не вычисляли сами размер данных изображения перед вызовом CreateDIBSection, то GDI сам вычислит нужное значение и возвратит его в поле biSizeImage (в примере выше это будет ds.dsBmih.biSizeImage).
Массив dsBitfields содержит маски цветов; они заполняются в зависимости от числа цветов и установленного режима сжатия. Подробнее о масках см. «Формат Win32 (Windows NT 3.x)», стр.58.
Поля dshSection и dsOffset повторяют значения, указанные при вызове функции CreateDIBSection. Если вы указали нулевые значения, то и эти поля также будут нулевыми, несмотря на то, что система сама создает проецирование.
При использовании структуры DIBSECTION нужно следить за тем, что бы вы не создавали DIB–секцию для битмапов в формате OS/2. Непосредственно GDI эту работу выполнит без затруднений, но при этом функция GetObject возвратит в структуре DIBSECTION не BITMAPINFOHEADER, а BITMAPCOREHEADER, другого размера и с другими полями. Если вам придется все–же работать с битмапами OS/2, то заодно придется описать и собственную структуру, аналогичную DIBSECTION; лучше всего просто превратить ее в union, содержащий вариант для Windows и для OS/2.
Если же вы собираетесь использовать CreateDIBSection для редактирования уже существующего DIB, то стоит воспользоваться одним из двух возможных способов: а) создать временное проецирование и скопировать в него битмап, пропустив заголовок файла или б) загрузить DIB обычным образом, создать пустую DIB–секцию, скопировать в нее изображение (скажем, с помощью SetDIBitsToDevice) и освободить первоначально загруженный DIB.
Внимание! Если вы вызывали CreateDIBSection с нулевыми значениями hSection и dwOffset, то при удалении созданной секции с помощью DeleteObject система сама удалит созданное проецирование (вам оно недоступно, так как GetObject возвращает также нули в полях dshSection и dsOffset). Но если вы сами создали проецирование, то вы обязаны сами его удалить.
При работе с DIB–секциями часто возникает необходимость получить палитру, используемую этой секцией. Типичный случай — сохранение DIB–секции в виде файла: если число цветов меньше или равно 256, то такая секция обязательно содержит палитру. Причем в этом случае нужна палитра не в виде структуры LOGPALETTE или массива записей PALETTENTRY, а в виде массива записей RGBQUAD. Для этого предназначена пара функций:
UINT GetDIBColorTable (hdc, uStartIndex, cEntries, lprgbColors); 1
UINT SetDIBColorTable (hdc, uStartIndex, cEntries, lprgbColors); 1
Обратите внимание — функции используют не хендл DIB–секции, а хендл совместимого контекста устройства, в который должна быть выбрана DIB–секция.
Практические примеры:
1) Создание пустой DIB–секции 1:
struct {// не ‘BITMAPINFO bmi’ - нам надо зарезервировать место под палитру
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[ 256+3 ]; // в BITMAPINFO используется bmiColors[1]
} bmi;
LPVOID lpData;
HDC hdcDisplay;
HDC hdcMem;
HBITMAP hbmpDibSection;
int nFirstCol;
int nColors; // число цветов в системной палитре
PALETTEENTRY pe[ 256 ]; // системная палитра
int i;
hdcDisplay = GetWindowDC ( (HWND)0L);
// создаем DIB–секцию, для чего полностью заполняем bmi, включая маски и палитру
bmi.bmiHeader.biSize = sizeof (BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = GetDeviceCaps (hdcDisplay, HORZRES); // пусть размер DIB
bmi.bmiHeader.biHeight = GetDeviceCaps (hdcDisplay, VERTRES); // совпадает с экраном
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount =
GetDeviceCaps (hdcDisplay, BITSPIXEL) * GetDeviceCaps (hdcDisplay, PLANES);
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biSizeImage = 0L; // а это пусть GDI вычисляет
bmi.bmiHeader.biXPelsPerMeter =
GetDeviceCaps (hdcDisplay, HORZRES)*1000 / GetDeviceCaps (hdcDisplay, HORZSIZE);
bmi.bmiHeader.biYPelsPerMeter =
GetDeviceCaps (hdcDisplay, VERTRES)*1000 / GetDeviceCaps (hdcDisplay, VERTSIZE);
bmi.bmiHeader.biClrUsed = 0;
bmi.bmiHeader.biClrImportant = 0;
// обнулим палитру
ZeroMemory ( (LPVOID)bmi.bmiColors, sizeof (bmi.bmiColors));
// решим, будем–ли мы задавать маски цветов для режимов 16 и 32 bpp
// если задавать, то только стандартные 5–5–5, 5–6–5 или 8–8–8 и указать BI_BITFIELDS
// (даже на Windows NT 4.0 CreateDIBSection работала только со стандартными масками_
// можно и не задавать; битмапы 16 и 32 bpp можно создать и как BI_RGB
// В ДАННОМ ПРИМЕРЕ БУДЕМ ЗАДАВАТЬ РЕЖИМ BI_BITFIELDS
nFirstCol = 0;
switch (bmi.bmiHeader.biBitCount) {
case 16:
bmi.bmiColors[0].rgbGreen = 124; // red: 0x7C00
bmi.bmiColors[1].rgbGreen = 3; // green: 0x03E0
bmi.bmiColors[1].rgbBlue = 224;
bmi.bmiColors[2].rgbBlue = 31; // blue: 0x001F
bmi.bmiHeader.biCompression = BI_BITFIELDS;
nFirstCol = 3;
break;
case 32:
bmi.bmiColors[0].rgbRed = 255; // red: 0x00FF0000
bmi.bmiColors[1].rgbGreen = 255; // green: 0x0000FF00
bmi.bmiColors[2].rgbBlue = 255; // blue: 0x000000FF
bmi.bmiHeader.biCompression = BI_BITFIELDS;
nFirstCol = 3;
break;}
// проверим, нужно–ли назначать битмапу палитру?
nColors = GetDeviceCaps (hdcDisplay, SIZEPALETTE);
// для 16, 24 и 32 nColors будет равен 0
if (nColors) {
GetSystemPaletteEntries (hdcDisplay, 0, nColors, pe);
for (i =0; i < nColors; i++) {
bmi.bmiColors[ i + nFirstCol ].rgbRed = pe[i].peRed;
bmi.bmiColors[ i + nFirstCol ].rgbGreen = pe[i].peGreen;
bmi.bmiColors[ i + nFirstCol ].rgbBlue = pe[i].peBlue;}
bmi.bmiHeader.biClrUsed = nColors;}
// создаем секцию по полученному описанию
hbmpDibSection = CreateDIBSection (
hdcDisplay, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS, &lpData, (HANDLE)0L, 0);
// заполним секцию каким–либо изображением
if (hbmpDibSection) {
hdcMem = CreateCompatibleDC (hdcDisplay);
SelectObject (hdcMem, hbmpDibSection);
// собственно здесь и выполняется редактирование DIB–секции
BitBlt (
hdcMem, 0,0, bmi.bmiHeader.biWidth,bmi.bmiHeader.biHeight,
hdcDisplay, 0,0,
SRCCOPY);
DeleteDC (hdcMem);}
ReleaseDC ( (HWND)0L, hdcDisplay);
// hbmpDibSection оставляем для использования в дальнейшем
2) Сохранение DIB–секции в виде .bmp файла 1:
HBITMAP hbmpDibSection; // этот хендл мы получаем из предыдущего примера
BITMAPFILEHEADER bmfh; // заголовок файла битмапа
DIBSECTION ds; // информация о битмапе
HANDLE hf; // хендл файла в котором будет записан DIB
int nColors; // число цветов в палитре битмапа
RGBQUAD rgbs[ 256 ]; // палитра, заполняется если nColors != 0
HDC hdcDisplay;
HDC hdcMem;
DWORD dwWritten;
// получаем кое-какую информацию о записанном битмапе
GetObject (hbmpDibSection, sizeof (ds), &ds);
// определяем размер палитры
hdcDisplay = GetWindowDC ( (HWND)0L);
hdcMem = CreateCompatibleDC (hdcDisplay);
SelectObject (hdcMem, hbmpDibSection);
ReleaseDC ( (HWND)0L, hdcDisplay);
nColors = ds.dsBmih.biClrUsed ? ds.dsBmih.biClrUsed :
(ds.dsBmih.biBitCount <= 8 ? 1<<ds.dsBmih.biBitCount : 0);
if (nColors) {
// палитра присутствует
nColors = GetDIBColorTable (hdcMem, 0, nColors, rgbs);
ds.dsBmih.biClrUsed = nColors;}
DeleteDC (hdcMem);
// сохраняем в файле
hf = CreateFile (
"TestDIB.bmp", GENERIC_READ, 0,0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hf != INVALID_HANDLE_VALUE) {
// заполняем и записываем заголовок файла
bmfh.bfType = 'MB';
bmfh.bfSize = bmfh.bfReserved1 = bmfh.bfReserved2 = 0L;
bmfh.bfOffBits =
sizeof (BITMAPFILEHEADER) +
ds.dsBmih.biSize +
(ds.dsBmih.biCompression == BI_BITFIELDS ? sizeof (ds.dsBitfields): 0) +
nColors * sizeof (RGBQUAD);
WriteFile (hf, (LPVOID)&bmfh, sizeof (bmfh), &dwWritten, (LPOVERLAPPED)0L);
// записываем полученный от GDI заголовок битмапа
WriteFile (hf, (LPVOID)&ds.dsBmih,ds.dsBmih.biSize,&dwWritten, (LPOVERLAPPED)0L);
// проверяем наличие масок цветов
if (ds.dsBmih.biCompression == BI_BITFIELDS) {
// пишем маски
WriteFile (
hf, (LPVOID) (ds.dsBitfields), sizeof (ds.dsBitfields),
&dwWritten, (LPOVERLAPPED)0L);}
// пишем палитру при ее наличии
if (nColors) {
// палитра присутствует
WriteFile (
hf, (LPVOID) (rgbs), sizeof (RGBQUAD) * nColors,
&dwWritten, (LPOVERLAPPED)0L);}
// записываем полученное от GDI изображение
// строго говоря, надо убедиться, что это не битмап OS/2 по ds.dsBmih.biSize
// и либо использовать ds.dsBmih.biSizeImage, либо вычислять самим, если это
// битмап OS/2
WriteFile (
hf, (LPVOID) (ds.dsBm.bmBits), ds.dsBmih.biSizeImage,
&dwWritten, (LPOVERLAPPED)0L);
// вместо ds.dsBm.bmBits можно воспользоваться lpData из предыдущего примера
CloseHandle (hf);}
// считаем, что больше DIB–секция нам не нужна
DeleteObject (hbmpDibSection);
3) Загрузка DIB–файла с помощью DIB–секции 1:
HWND hwnd; // предположим, что мы будем отображать
// DIB в этом окне
HANDLE hfDib; // данные для проецирования DIB файла
HANDLE hmapDib;
BY_HANDLE_FILE_INFORMATION finfo;
LPBITMAPFILEHEADER lpbmfh;
HANDLE hmapTemp; // packed DIB
LPBITMAPINFO lpbmi;
HBITMAP hbmpDibSection;
HDC hdcMem;
HDC hdcDisplay;
LONG dwOffBits; // смещение до данных изображения
LPVOID lpData;
lpbmi = (LPBITMAPINFO)0L; // инициализируем указатель — дальше проверим
// скопируем временное проецирование для "Packed DIB"
hfDib = CreateFile (
"TestDIB.bmp", GENERIC_READ, 0, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hfDib != INVALID_HANDLE_VALUE) {
hmapDib = CreateFileMapping (hfDib, 0, PAGE_READ|SEC_COMMIT, 0,0, 0);
lpbmfh = (LPBITMAPFILEHEADER)MapViewOfFile (hmapDib, FILE_MAP_READ, 0,0, 0);
// вычислим смещение данных изображения в "Packed DIB" для использования позже
dwOffBits = lpbmfh->bfOffBits - sizeof (BITMAPFILEHEADER);
// создадим временное проецирование
GetFileInformationByHandle (hfDib, &finfo);
finfo.nFileSizeLow -= sizeof (BITMAPFILEHEADER);
hmapTemp = CreateFileMapping (
INVALID_HANDLE_VALUE, 0, PAGE_READWRITE|SEC_COMMIT, 0,finfo.nFileSizeLow, 0);
lpbmi = (LPBITMAPINFO)MapViewOfFile (hmapTemp, FILE_MAP_WRITE, 0,0, 0);
if (lpbmi && lpbmfh) {
// собственно копирование файла...
CopyMemory (
(LPVOID)lpbmi,
(LPVOID) (lpbmfh + 1),
finfo.nFileSizeLow);}
UnmapViewOfFile ( (LPVOID)pbmfh);
CloseHandle (hmapDib);
CloseHandle (hfDib);}
// если проецирование создано и файл скопирован, то отобразим его в окне
if (lpbmi) {
hdcDisplay = GetWindowDC (hwnd);
hbmpDibSection = CreateDIBSection (
hdcDisplay, lpbmi, DIB_RGB_COLORS, &lpData, hmap, dwOffBits);
hdcMem = CreateCompatibleDC (hdcDisplay);
SelectObject (hdcMem, hbmpDibSection);
BitBlt (
hdcDisplay, 0,0, lpbmi->bmiHeader.biWidth, lpbmi->bmiHeader.biHeight,
hdcMem, 0,0,
SRCCOPY);
ReleaseDC (hwnd, hdcDisplay);
DeleteDC (hdcMem);
DeleteObject (hbmpDibSection);
UnmapViewOfFile ( (LPVOID)lpbmi);
CloseHandle (hmapTemp);}
Метафайлы
Сохранять изображения можно не только в виде битмапов. Альтернативный, причем достаточно интересный, метод представлен с помощью так называемых метафайлов (metafile).
Когда мы рассматривали способы получения контекста устройства, то отмечали, что один из перечисленных методов предназначен для создания метафайлов. Это довольно специфичное графическое устройство, которому не соответствует ровным счетом никакого реального устройства. Более того, при работе с метафайлом не создается никакого изображения.
Метафайл можно рассматривать как специфичное записывающее устройство, которое запоминает все функции GDI, которые выполняются над ним. То есть, когда вы пытаетесь, скажем, нарисовать эллипс, метафайл запомнит команду для рисования эллипса в данном месте и данного размера. Позже вы сможете с помощью специальной функции осуществить воспроизведение метафайла на требуемом контексте устройства.
Название метафайл придумано не случайно. Все запоминаемые команды GDI сохраняются в файле, который может быть сохранен на диске и использован позже.
В какой-то степени можно сравнить метафайл с микропрограммой, осуществляющей вывод изображения. Однако это весьма специфичная программа. Для того, что бы оценить область применения метафайлов, надо рассмотреть их основные особенности. Для этого сравним метафайл с битмапом. Конечно эти сравнения весьма относительны, всегда можно придумать такой случай, когда оценки окажутся совершенно неверными:
метафайл компактнее битмапов
время отображения битмапов меньше времени воспроизведения метафайла
при использовании обычных, зависимых от устройства битмапов возможны проблемы с передачей цветов и с переносом на другие устройства, с метафайлом таких проблем меньше
при изменении размера битмапа возможно существенное снижение качества изображения, метафайл менее чувствителен к этому
разработка метафайла отличается некоторыми особенностями, так как не все функции GDI могут быть использованы с метафайлами.
Полезно, кроме того, узнать, каких вещей сделать с помощью метафайла невозможно:
Метафайл не может использовать переменных. Запоминается выполняемая функция GDI не с выражениями, использованными для вычисления аргументов, а с их численными значениями. При записи метафайла сохраняется список вызываемых функций GDI с использованными числовыми значениями аргументов. Чему они соответствуют, выяснится при воспроизведении метафайла.
Метафайл не сохраняет никаких GDI функций, начинающихся со слова Get... более того, метафайл не выполняет никаких действий, связанных с возвращением информации в программу — так как во время воспроизведения метафайла многих переменных может не быть, или они могут быть по другим адресам.
Метафайл не использует функций, ссылающихся на него как на реальный контекст устройства: CreateCompatibleDC, ReleaseDC, DeleteDC, CreateCompatibleBitmap, CreateDiscardableBitmap, PlayMetaFile (имеется в виду что на одном метафайле нельзя воспроизвести другой[13]).
С метафайлами не работают также функции: GrayString, DrawIcon, SetBrushOrg, FillRect, FrameRect.
Однако не все ограничения метафайла являются непреодолимыми, многие из них легко обходятся. Например, мы хотим с помощью метафайла выводить изображение, занимающее все окно. При создании метафайла мы предположим, что изображение размещается в прямоугольнике, скажем, 100 x 100 единиц. Таким образом созданный метафайл будет воспроизводить изображение в области 100 x 100, независимо от размеров окна; но перед воспроизведением метафайла мы можем выбрать собственную систему координат такой, что бы размер внутренней области окна в этой системе координат был равен 100 x 100, и тогда изображение займет все окно.
Из всего сказанного вытекает, что использовать метафайл как обычное графическое устройство не совсем удобно. Требуется специально проектировать процесс рисования на метафайле, что бы им было удобно пользоваться.
А как можно работать с метафайлом? Сначала метафайл должен быть создан — это делается с помощью функции:
HDC CreateMetaFile (lpszFileName);
HDC CreateEnhMetaFile (hdcRef, lpszFileName, lprectBound, lpszDescription); 1
Данная функция создает контекст устройства, связанный с метафайлом. Параметр lpszFileName указывает имя файла, в который будет происходить запись команд. Здесь надо ввести дополнительное понятие. Метафайлы разделяют на метафайлы, использующие диск (файл) для записи команд (disk-based), и метафайлы, использующие блок памяти (memory-based) для хранения набора команд. Если вы указали имя создаваемого файла, то создается метафайл, использующий диск. Однако в качестве lpszFileName может быть указан NULL, тогда будет создан метафайл, использующий память.
Функция CreateEnhMetaFile создает метафайл в формате Win32, обладающий несколько большими возможностями, чем обычный. В частности в заголовке метафайла будет сохранена информация о размерах записанного (параметр lprectBound) в нем изображения и о разрешающей способности устройства (параметр hdcRef), для которого метафайл создан. Помимо этого в заголовок включается небольшая строка, поясняющая название сохраненного рисунка и название приложения, осуществившего запись этого рисунка (параметр lpszDescription). При вызове функции CreateEnhMetaFile допускается задавать нулевые значения hdcRef (по умолчанию будут использованы характеристики дисплея) и lprectBound (размер изображения будет вычисляться в процессе записи рисунка).
Создаваемые этими функциями метафайлы — разные объекты, так что для работы с метафайлами Win32 необходимо использовать свой набор функций, а для работы с метафайлами Windows 3.x — свой. В именах функций, работающих с метафайлами Win32 как правило присутствует текст ...EnhMetaFile.
В процессе записи метафайла Win32 можно задать текст комментария:
BOOL GdiComment (hdcEnhMF, cbSize, lpData);
После получения хендла контекста устройства можно осуществить запись метафайла, используя обычные функции GDI. После того, как метафайл сформирован, Вы должны закрыть его:
HMETAFILE CloseMetaFile (hdcMetaFile);
HENHMETAFILE CloseEnhMetaFile (hdcEnhMetaFile); 1
При этом уничтожается связанный с метафайлом контекст устройства и возвращается хендл метафайла, который может использоваться для его воспроизведения. Этот метафайл является объектом GDI, так что надо очень внимательно следить за удалением этого объекта. В отличие от большинства объектов GDI вместо функции DeleteObject должна быть использована функция:
BOOL DeleteMetaFile (hMF);
BOOL DeleteEnhMetaFile (hEnhMF); 1
Где hMF (hEnhMF) — хендл метафайла (а не контекста устройства). При этом метафайл как объект GDI уничтожается, а файл, содержащий его, остается на диске. Если вы хотите удалить и этот файл тоже, то либо воспользуйтесь функцией для удаления файлов стандартной библиотеки времени выполнения, например unlink, либо функцией Win32 API DeleteFile.
Если вы уже записали метафайл на диск, то вы можете легко создать объект GDI, соответствующий этому метафайлу с помощью функции:
HMETAFILE GetMetaFile (lpszFileName);
HENHMETAFILE GetEnhMetaFile (lpszFileName); 1
Еще один способ получения хендла метафайла связан с использованием глобальных блоков памяти, содержащих данные метафайла. Вы можете сами загрузить метафайл в глобальный блок памяти или включить метафайл в виде ресурса приложения и загрузить его из ресурсов (для каждого ресурса создается отдельный глобальный блок памяти). Получив таким образом блок памяти с метафайлом вы можете создать метафайл, ссылающийся на этот блок с помощью функций:
HMETAFILE SetMetaFileBits (HGLOBAL hGMemMetaFile);
HMETAFILE SetMetaFileBitsBetter (HGLOBAL hGMemMetaFile);
HENHMETAFILE SetEnhMetaFileBits (cbBuffer, lpData); 1
HENHMETAFILE SetWinMetaFileBits (cbBuffer, lpData, hdcRef, lpMETAFILEPICT); 1
Операции SetMetaFileBits и SetMetaFileBitsBetter возвращают хендл метафайла, созданного из данных, содержащихся в глобальном блоке памяти. Этот метафайл, естественно, является метафайлом, использующим память. Причем исходный блок после создания метафайла нельзя уничтожать или использовать, так как Windows будет использовать его непосредственно.
Функция SetMetaFileBitsBetter отличается от SetMetaFileBits тем, что делает "хозяином" глобального блока, не ваше приложение, а GDI. Таким образом этот объект может использоваться буфером обмена или средствами OLE, так как он не уничтожается при завершении работы вашего приложения. Однако при этом уже вы сами обязаны проследить за тем, что бы в итоге освободить все занятые ресурсы.
Операция SetEnhMetaFileBits создает метафайл из блока данных, заданного его размером и указателем. Функция SetWinMetaFileBits создает метафайл Win32 из данных, подготовленных для метафайла Windows 3.x.
Возможна и обратная операция — преобразовать метафайл в блок глобальной памяти. Это делается с помощью функции:
HGLOBAL GetMetaFileBits (hMF);
UINT GetEnhMetaFileBits (hEnhMF, cbBuffer, lpBuffer); 1
UINT GetWinMetaFileBits (hEnhMF, cbBuffer, lpBuffer, int nMapMode, hdcRef); 1
После вызова функции GetMetaFileBits нельзя использовать hMF, он считается уничтоженным, но зато вы можете использовать возвращенный хендл блока глобальной памяти. Так вы можете узнать размер метафайла, сохранить его на диске и т.д.
Функция GetEnhMetaFileBits в отличие от GetMetaFileBits не уничтожает метафайла, так что вы сами должны будете позаботиться об его уничтожении. Функция GetWinMetaFileBits кроме того записывает полученные данные в старом формате.
В том случае, когда надо метафайл, использующий память, превратить в метафайл, использующий диск, надо воспользоваться функцией:
HMETAFILE CopyMetaFile (hMF, lpszFileName);
HENHMETAFILE CopyEnhMEtaFile (hEnhMF, lpszFileName); 1
которая скопирует данные метафайла из памяти в дисковый файл с заданным именем. После копирования можно закрыть исходный метафайл с помощью функции DeleteMetaFile (DeleteEnhMetaFile 1).
Для воспроизведения метафайла предназначена функция
BOOL PlayMetaFile (hDC, hMF);
BOOL PlayEnhMetaFile (hDC, hEnhMF, lpRect); 1
которая проигрывает указанный параметром hMF метафайл на заданном контексте устройства hDC.
В некоторых случаях, когда воспроизведение метафайла занимает много времени, или если надо отдельные записи метафайла корректировать перед воспроизведением, удобно воспользоваться другой функцией для воспроизведения метафайла:
BOOL EnumMetaFile (hDC, hMF, lpfnMFEnumProc, lParam);
BOOL EnumEnhMetaFile (hDC, hEnhMF, lpfnEnhMFEnumProc, lParam, lpRect); 1
Эта функция перебирает все записи метафайла hMF (hEnhMF) и для каждой из них вызывает функцию, заданную указателем lpfnMFEnumProc (lpfnEnhMFEnumProc). Параметр hDC указывает контекст устройства, на котором должен воспроизводиться метафайл, а lParam произвольные данные, которые вы решите передать в функцию lpfnMFEnumProc (lpfnEnhMFEnumProc). В случае Windows API параметр lpfnMFEnumProc должен быть не указателем на саму функцию, а указателем, возвращенным функцией MakeProcInstance; при этом после перебора всех записей метафайла (то есть после возврата из EnumMetaFile) вы должны уничтожить созданный указатель с помощью функции FreeProcInstance; в случае Win32 API надо передавать непосредственно указатель на требуемую функцию.
Функция, которая вызывается для обработки каждой записи метафайла должна иметь следующий вид:
int CALLBACK _export MFEnumProc (
HDC hDC, LPHANDLETABLE lpHTable, LPMETARECORD lpMR, int cHandles, LONG lParam){...}
Или, в случае Win32 API:
int EnhMFEnumProc (// 1
HDC hDC, HANDLETABLE FAR* lpHTable, ENHMETARECORD *lpMR, int cHandles, LONG lParam){...}
Когда Windows вызывает эту процедуру, параметр hDC указывает хендл контекста устройства, на котором воспроизводиться метафайл. Параметр lpHTable является указателем на таблицу хендлов объектов GDI, созданных при воспроизведении метафайла. Причем параметр cHandles указывает число объектов в этой таблице. Параметр lpMR является указателем на структуру, описывающую ту запись метафайла, которая должна воспроизводиться, а параметр lParam содержит те данные, которые вы передали через одноименный параметр функции EnumMetaFile.
Функцию MFEnumProc вы должны написать сами. В простейшем случае она может состоять из единственного вызова функции:
void PlayMetaFileRecord (hDC, lpHTable, lpMR, cHandles);
void PlayEnhMetaFileRecord (hDC, lpHTable, lpMR, cHandles); 1
Которая осуществит воспроизведение данной записи метафайла. Однако до, после или вместо этого вы можете проанализировать запись метафайла и выполнить нужные действия. Вы можете даже изменять запись, причем эти изменения сохраняются в метафайле.
Итак, что содержится в записи?
typedef struct tagMETARECORD {
DWORD rdSize;
UINT rdFunction;
UINT rdParm[ 1 ];
} METARECORD;
typedef struct tagENHMETARECORD {// 1
DWORD iType;
DWORD nSize;
DWORD dParm[ 1 ];
} ENHMETARECORD;
Поле rdSize указывает полный размер записи метафайла, включая заголовок, в словах (то есть реально его надо умножать на 2, что бы получить размер в байтах).
Поле rdFunction задает номер выполняемой функции GDI. Эти номера вы можете посмотреть в windows.h, они начинаются с префикса META_ (например META_RECTANGLE). Младший байт этого слова содержит номер функции GDI, а старший — размер передаваемых этой функции аргументов, выраженный в словах.
Массив rdParm является массивом данных, передаваемых функции GDI. Данные перечислены в обратном порядке, по сравнению с тем, как они размещены в описании функции. Если при этом функция содержит указатель на что–либо (например TextOut содержит указатель на текст), то этот объект целиком включается в запись на месте соответствующего аргумента.
В случае Win32 поля несколько иные: iType задает «тип записи», то есть номер функции, которые имеют префикс EMR_, вместо META_ (например EMR_SETMAPMODE), поле nSize задает размер непосредственно в байтах, а dParm — параметры записи.
Вы можете легко проанализировать запись и выполнить требуемые действия.
[1] Логический дюйм часто равен физическому. При выводе на экран разрешающая способность может быть чересчур низкой, по сравнению с разрешающей способностью принтера. При этом становится целесообразным несколько увеличивать реальное изображение, что бы сохранить приемлемое восприятие изображения. Так, например, шрифт размером 8 пунктов на принтере читается совершенно свободно, но при отображении на экране в реальном масштабе буквы часто становятся плохо различимыми. Логический дюйм это либо нормальный дюйм — для устройств с высокой разрешающей способностью, либо больше — для устройств с низкой разрешающей способностью.
[2] По крайней мере так считает Microsoft. Вообще говоря, существует несколько различных полиграфических систем с применением разных единиц отсчета, в которых размер точки может несколько варьироваться.
[3] Исключение — Windows NT. В этой системе для каждого запущенного приложения загружается своя копия системных модулей, включая GDI32.EXE; Этот механизм не настолько громоздок, как кажется, за счет специальных механизмов защиты страниц «копирование при записи». Достоинство — высокая защищенность, так как приложение может навредить только себе, даже если оно разрушит или не освободит системные объекты. В этом случае при завершении работы приложения все ресурсы, созданные им, автоматически освобождаются. Однако очень надеяться на эту особенность не стоит — если ресурсы не освобождать вовремя, то размер файла подкачки страниц может существенно увеличиться, в связи с чем возникнет вопрос о наличии свободного дискового пространства.
[4] По–видимому, некоторые подобные рекомендации сделаны для обеспечения возможной совместимости с другими системами в будущем. Так, например, при работе в X–Windows (в основном UNIX–машины), при удалении контекста удаляются все объекты, выбранные в него. Если оставаться в рамках Windows или Win32 API, то выбранные в контекст устройства объекты GDI вовсе не обязательно заменять на стандартные перед освобождением контекста.
[5] По крайней мере современные версии OS/2 часто применяют битмапы в ином формате — попытка их проанализировать, используя подобное описание приводит к ошибке, так что использованное название «битмап OS/2» выглядит по меньшей мере сильно устаревшим. Ключом в определении версии битмапа является размер его заголовка. Так, например, размер заголовка битмапа OS/2 (версия 1.2 — формат, поддерживаемый также и Microsoft Windows) равен 12 байтам, размер заголовка Windows битмапа равен 40 байтам (не считая так называемых 4ой и 5ой версий битмапов); более современный формат битмапов OS/2 (версия 2.0) равен 64 байтам.
[6] Иногда возникает непроизвольная ошибка при попытке создать палитру (как объект GDI): для создания палитры необходимо в функцию CreatePalette передать массив структур типа PALETTENTRY, который имеет схожий с RGBQUAD формат. Естественное желание — просто использовать палитру битмапа в качестве массива структур PALETEENTRY. Неприятность связана с тем, что компоненты красного, синего и зеленого цветов в этих структурах перечислены в разном порядке; таким образом необходимо все–таки создавать собственный массив структур PALETENTRY для создания палитры.
[7] В случае Windows NT требуется только чтобы биты для каждой компоненты образовывали непрерывную группу, а число бит для задания каждой компоненты и порядок задания этих групп не фиксированы.
[8] Формат 5–6–5 является альтернативным, для его задания обязательно задание масок цветов; в то время как для задания формата 5–5–5 маски можно опустить — он считается стандартным.
[9] Очень может быть, что это зависит от видеоадаптера и драйверов — на тестовой машине был установлен Diamond Stealth 64 PCI с чипом S3 Vision968. Стандартные драйвера для Windows 3.x вообще не поддерживают режимов 16 и 32 бит/пиксель.
[10] Или RGBTRIPLE, если битмап соответствует формату OS/2
[11] Если вы будете его использовать, то вы должны включить файл драйвера DIB.DRV в установочный комплект вашего приложения — в стандартной системе этот драйвер не содержится; обычно он включен в redistribution kit, сопровождающий компиляторы.
[12] Разве что кроме форматов OS/2, использующих для задания палитры записи RGBTRIPLE по 3 байта каждая; тогда необходимо следить, что бы размер палитры обеспечивал выравнивание начала изображения на границу, кратную 4 байтам.
[13] На практике воспроизведение одного метафайла на другом выполняется успешно. Однако, коль скоро Microsoft советует так не делать, то лучше и не пытаться — вполне может оказаться так, что последующие версии Windows перестанут выполнять эту операцию.