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 перестанут выполнять эту операцию.


Информация о работе «Основы графического вывода»
Раздел: Информатика, программирование
Количество знаков с пробелами: 259922
Количество таблиц: 29
Количество изображений: 12

Похожие работы

Скачать
32076
1
25

... 0 - в момент, когда х=0. Это говорит о том, что plot «не знает» о том, что неопределенность sin(x)/x=0/0 устранима и дает 1. Это недостаток практически всех систем для численных вычислений.   Графическая функция fplot MATLAB имеет средства для построения графиков и таких функций, как sin(x)/x, которые имеют устранимые неопределенности. Это делается, с помощью другой графической команды – ...

Скачать
18931
1
4

... 0 в структуре определения предела последовательности. Наглядное выявление данных зависимостей позволяет глубже осознать понятие предельного процесса, осуществляя при этом комплексный блок математических действий. Углубление содержания понятия предела числовой последовательности xn происходит за счет нахождения минимального номера N()ε, начиная с которого выполняется неравенство xn − A ...

Скачать
73983
0
4

... композиций. Была выдвинута ГИПОТЕЗА о том, что при использовании новых информационных технологий (в частности возможностей компьютерной графики и дизайна) на интегрированных занятиях "ИЗО-информатика" у детей старшего дошкольного возраста способствует формированию графических понятий, так как ребёнок вынужден мысленно оперировать ими при работе на компьютере, и создаёт базу для развития ...

Скачать
235892
25
6

... работе в графическом режиме предназ­начается для обучения студентов младших курсов Санкт-Петербургской государственной Академии аэрокосмического приборостроения навыкам программирования, а именно работе в графическом режиме языка Turbo-Pascal . Для работы с настоящей программой необходимо знание стандарта языка, интегрированной среды и элементарным навыкам работы с персональным компьютером . ...

0 комментариев


Наверх