ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 45 от 20 мая 2001 г.

Всем привет!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Прозрачность - это просто

Автор: Виталий Брусенцев
Демонстрационный проект (57 Kb, Visual C++ 6.0)
Демонстрационная программа (45 Kb, Windows 98 и выше, режим экрана HiColor и выше)
Терминология

Прежде чем начать, убедимся, что понимаем друг друга:

Введение

Зачем нужны растровые изображения с прозрачностью или полупрозрачностью отдельных участков? Это важные элементы графического интерфейса Windows, которые вы можете наблюдать каждый раз, когда включаете компьютер. Иконки на рабочем столе имеют прозрачные участки, что позволяет видеть "сквозь" них. Когда вы, работая в папке Windows 98, перетаскиваете какой-нибудь объект, его значок становится полупрозрачным, позволяя видеть, что в данный момент находится под ним. И, наконец, нельзя забывать о таких "графикоемких" программах, как игры. Трудно себе представить, чтобы в тщательно спроектированной плоской космической "стрелялке" все корабли имели прямоугольную форму. А при отрисовке взрывов желательно делать их полупрозрачными, приближая картинку к реальности.

Вообще - то на эту тему писали довольно много. Для понимания основных механизмов получения эффектов прозрачности рекомендую прочитать статью Рона Джери "Bitmaps with transparency" (ее можно найти в MSDN в разделе Technical Articles->Multimedia->GDI). Также рекомендую изучить находящиеся там статьи Дейла Роджерсона ("Sprites Make the World Go Round") и Германа Родента ("Animation in Win32").

К сожалению, все эти статьи разделяют общий недостаток - почтенный возраст. Цель данной статьи - показать, что с появлением Windows 95, а затем Windows 98 и Windows 2000 жить программистам стало намного проще (и интереснее!). Все приводимые примеры написаны, для удобства, с использованием библиотеки MFC, но принципы остаются общими.

Windows 95 и списки изображений

С выходом Windows 95 в распоряжении программистов оказалась удобная библиотека Common Controls. В ее составе были не только новые (теперь уже известные всем) элементы управления, но и невизуальный компонент - список изображения (Image List control). Его основное предназначение - содержать набор картинок одинакового размера. Это удобно для применения в разнообразных элементах - например, в панелях инструментов (toolbar).

Нас же больше интересует другая интересная возможность - хранить в списках изображений информацию о прозрачности. Это достигается одним из двух способов:

Нужно понимать, что в обоих случаях список изображений будет содержать маску прозрачности, просто при первом способе она будет создана за вас. Какой способ избрать - дело вкусов каждого программиста. Я обычно нахожу в палитре какой-нибудь ненужный цвет и назначаю его прозрачным. В большинстве случаев не везет ярко-сиреневому цвету (RGB 255,0,255).

Создание списков прозрачных изображений

Создать список изображения и загрузить в него растр с прозрачностью можно так:

    CBitmap m_Bmp;
    m_Bmp.LoadBitmap(IDB_BITMAP1); 
    CImageList imgList;    
    imgList.Create(cx, cy, ILC_COLOR24|ILC_MASK, 1, 0);
    imgList.Add(&m_Bmp, RGB(255,255,255));

В приводимых примерах будет предполагаться, что растровые изображения находятся в ресурсах программы и имеют глубину цвета 24 бита (16 млн. цветов). При создании списка необходимо указать размеры загружаемого растра (cx, cy), его цветовой формат (ILC_COLOR24, 16 миллионов оттенков) и признак наличия маски (ILC_MASK). Последние два параметра Create() определяют число хранимых в списке изображений и величину приращения списка при нехватке места. Макрос RGB удобен для указания цвета в 24-битовом диапазоне, в данном случае - цвета прозрачности.

В принципе, загрузить картинку можно и одним вызовом, но этот метод не поддерживает полноцветные изображения (как в нашем примере):

    CImageList imgList;
    imgList.Create(IDB_BITMAP1, cx, 0, RGB(255,255,255));

По умолчанию, такой растр ограничен 16 цветами.

Рисование с помощью Image List

Для чего списки изображений действительно полезны, так это для облегчения работы по рисованию. Вы вспомните навскидку число и порядок параметров у BitBlt()? А ее неудобство из-за необходимости подготовить дополнительный контекст устройства в памяти? Все это способно смутить не только новичка, но и более опытного программиста.

Ситуация еще более усложняется, когда мы имеем дело с прозрачностью. Необходимо выводить картинку несколько раз с различными параметрами растровых операций (Raster operation, ROP). Каждый раз, когда мы собираемся вывести на экран небольшое изображение космического корабля, необходимо проделать уйму работы.

К счастью, программисты Microsoft уже сделали ее за нас. Можно нарисовать растр, содержащийся в списке изображений, просто вызвав функцию ImageList_Draw(). С использованием MFC этот вызов выглядит, например, так:

    imgList.Draw(pDC, 0, m_drawPoint, ILD_TRANSPARENT);

Здесь pDC - указатель на контекст устройства (CDC), 0 - порядковый номер выводимого из списка изображения, m_drawPoint - координаты начала области вывода. Флаг ILD_TRANSPARENT указывает, что вывод нужно осуществлять с учетом маски прозрачности.

Для самых любознательных сообщу, что реализация эффекта прозрачности при этом достигается методом, который Рон Джери называет Black Source Method, т.е., "метод черного источника". Он позволяет выводить изображение с прозрачными участками за два вызова BitBlt() вместо трех, но требует предварительно заменить пикселы, являющиеся прозрачными, черным цветом. Поэтому, загружая растр в список изображений, вы меняете его.

В результате вызова получится примерно вот что.

Отметим, что такой метод работает по принципу "все или ничего": если цвет пиксела изображения отличается от прозрачного хоть на один бит, он считается непрозрачным, что и заметно на примере области тени. Разница может быть незаметной для человеческого глаза, но не для компьютера. Зачастую спрайты рисуют в графических редакторах, сглаживающих картинку. При выводе такого изображения образуется ореол из пикселов, близких по цвету к прозрачному цвету фона, но все же не прозрачных (эффект гало). Поэтому рекомендуется рисовать такие растры на фоне, близком по цвету к фону выводимого изображения.

Windows 98 и библиотека msimg32.dll

Windows 98 принесла новый простой способ вывода прозрачных изображений. Входящая в ее состав библиотека msimg32.dll содержит новые функции для получения соблазнительных графических эффектов. Для ее использования нужно подключить к проекту при сборке файл msimg32.lib.

Теперь растр с прозрачностью можно вывести за один прием с помощью функции TransparentBlt, указав прозрачный цвет в последнем параметре функции:

    CDC memDC;
    memDC.CreateCompatibleDC(pDC);
    CBitmap *temp = memDC.SelectObject(m_Bmp);    
    TransparentBlt(pDC->m_hDC, x, y, dstX, dstY, memDC.m_hDC,
                   x1, y1, srcX, srcY, RGB(255,255,255));
    memDC.SelectObject(temp);

"Ничего себе - за один прием!" скажете вы и ...будете правы. Вновь появляется необходимость в совместимом контексте устройства, в котором нужно выбрать выводимый растр. Вновь - длинный (и не интуитивный) список параметров. Но зато появились возможности по управлению выводом. В данном примере x, y, x1, y1 - координаты начальной точки растра в приемнике и источнике соответственно. Параметры dstX и dstY - размеры области вывода, а srcX и srcY - ширина и высота прямоугольника, отображаемого из растра. Что если задать для них разные значения? Результат приведен здесь.

Как видим, эта функция содержит возможности по сжатию/растяжению растровых изображений. Только не переусердствуйте и не передайте в качестве размеров отрицательные значения - зеркального отображения TransparentBlt() создавать не умеет.

Добавим, что функция TransparentBlt() при выводе опирается на возможности DirectX данного устройства, что может дать прирост производительности по сравнению с традиционными методами.

СОВЕТ
Не забудьте после использования контекста устройства вновь выбрать в нем начальный растр (temp в нашем примере). В противном случае произойдет утечка графических ресурсов системы.
AlphaBlend(): "Полу" - не обязательно 1/2

Все вышеописанные методы работают только с одной моделью прозрачности, называемой в компьютерной графике Chroma Key. Это означает, что прозрачным назначается определенный цвет. Другая, более развитая, модель называется Alpha Blending. При ее использовании для описания характеристик пикселов кроме цветовых компонент (R, G, B) применяется прозрачность (Alpha). Степень прозрачности определяется обратной величиной этого параметра.

Для поддержки этого режима Windows 98, а затем и Windows 2000 и Windows ME предоставляют функцию AlphaBlend():

BOOL AlphaBlend(
  HDC hdcDest,                 // handle to destination DC
  int nXOriginDest,            // x-coord of upper-left corner
  int nYOriginDest,            // y-coord of upper-left corner
  int nWidthDest,              // destination width
  int nHeightDest,             // destination height
  HDC hdcSrc,                  // handle to source DC
  int nXOriginSrc,             // x-coord of upper-left corner
  int nYOriginSrc,             // y-coord of upper-left corner
  int nWidthSrc,               // source width
  int nHeightSrc,              // source height
  BLENDFUNCTION blendFunction  // alpha-blending function
);

Назначение ее параметров, в-общем, ясно из прототипа. Особый интерес представляет последний параметр - BLENDFUNCTION. Он представляет собой структуру (всего из четырех байт), определяющую режим вывода.

typedefstruct _BLENDFUNCTION {
  BYTE     BlendOp;
  BYTE     BlendFlags;
  BYTE     SourceConstantAlpha;
  BYTE     AlphaFormat;
}BLENDFUNCTION, *PBLENDFUNCTION, *LPBLENDFUNCTION;

Что это такое? Дело в том, что AlphaBlend() может работать в двух разных режимах.

Простой режим (общая прозрачность)

Первый (и наиболее простой в использовании) режим работы AlphaBlend() предполагает, что значение Alpha задано для всей картинки. В таком случае, оно применяется ко всем пикселам без исключения.

Формат BLENDFUNCTION в этом случае:

     BLENDFUNCTION blend;
     blend.BlendOp = AC_SRC_OVER;
     blend.BlendFlags = 0;
     blend.AlphaFormat = 0;
     blend.SourceConstantAlpha = 180;

Для поля BlendOp в данный момент определено только одно допустимое значение - AC_SRC_OVER.
Поле BlendFlags должно содержать 0.
Плохо документированный параметр AlphaFormat определяет взаимодействие пикселов источника и приемника, о чем мы еще поговорим далее.
Параметр SourceConstantAlpha определяет степень непрозрачности. Задав для этого поля 0, вы не увидите свой растр вообще. Максимальное значение, умещающееся в тип BYTE, равно 255. При этом выводимый растр полностью перекроет область назначения. Но зачем вам, в таком случае, AlphaBlend()? И это значение используется, в-основном, для второго режима.

Режим с альфа-каналом

Он требует некоторой дополнительной подготовки. В этом режиме растр, подготовленный для вывода, должен содержать информацию о степени прозрачности каждого пиксела. Это достигается, например, применением формата 32 бита на пиксел (по одному байту на каждый цветовой компонент и одному - на альфа-канал).

Сложности возникнут вот с чем. Мне неизвестен ни один графический пакет, позволяющий сохранять растры с альфа-каналом в формате, пригодном для AlphaBlend(). При создании такого растра программно мы можем сохранить его на диск (в формате Windows Bitmap 32-bit). Он прочитывается популярными программами типа ACDSee, но функции LoadBitmap() и LoadImage() отказываются его загружать. При попытке поместить его в ресурс rc.exe у меня вывалился с сообщением Internal error...

Но унывать не стоит. Мы можем подготовить растр в памяти, скомбинировав, например, два растра - с изображением объекта и его тени. К счастью, функция AlphaBlend() может работать с растрами, созданными с помощью CreateDIBSection():

HBITMAP CreateDIBSection(
  HDC hdc,                 // handle to DC
  CONST BITMAPINFO *pbmi,  // bitmap data
  UINT iUsage,             // data type indicator
  VOID **ppvBits,          // bit values
  HANDLE hSection,         // handle to file mapping object
  DWORD dwOffset           // offset to bitmap bit values
);

Здесь hdc - контекст, совместимый с устройством вывода, pbmi - указатель на структуру BITMAPINFO с информацией о размерах и цветовой глубине создаваемого растра. Параметр iUsage определяет, что будет содержаться в буфере растра - индексы в палитре цветов (DIB_PAL_COLORS) или прямые их значения (DIB_RGB_COLORS). Очевидно, что второе - для режима TrueColor палитра не нужна.
Указатель на созданный буфер при успешном вызове будет возвращен в параметре ppvBits.
В параметрах hSection и dwOffset при работе с растром в памяти необходимо указывать 0.

ПРИМЕЧАНИЕ
Все вышесказанное не означает, что спрайты с альфа-каналом нельзя хранить в ресурсах программы. Вы можете включить их как custom resource, загрузить с помощью LoadResource() и заполнить полученными данными буфер, созданный с помощью CreateDIBSection(). Но не забывайте, что это значительно увеличит размер вашего исполняемого модуля. Кроме того, можно рассмотреть вариант подгрузки предварительно рассчитанных в подобной программе растров из внешних файлов - средствами библиотеки исполнения или используя параметры hSection и dwOffset при вызове CreateDIBSection().

Использование CreateDIBSection() облегчает доступ к битам изображения: в противном случае такие картинки можно было бы вывести только в режиме экрана True Color 32 bit. Для обращения с таким форматом данных идеально подходит структура RGBQUAD:

typedefstruct tagRGBQUAD {
  BYTE    rgbBlue; 
  BYTE    rgbGreen; 
  BYTE    rgbRed; 
  BYTE    rgbReserved; 
} RGBQUAD;

Альфа-канал можно хранить в поле rgbReserved, хотя оно для этого и не предназначалось :) Кроме того, остается еще одна (недокументированная) возможность - воспользоваться функцией AlphaDIBBlend(), которую мы рассматривать не будем.

Результат приведен здесь.

Детали работы с битами растров мы опустим (все это можно найти в прилагаемом проекте). Отмечу только, что для вывода использовался такой формат BLENDFUNCTION:

    blend.BlendOp = AC_SRC_OVER;
    blend.BlendFlags = 0;
    blend.AlphaFormat = AC_SRC_NO_PREMULT_ALPHA;
    blend.SourceConstantAlpha = 255;

Параметр AC_SRC_NO_PREMULT_ALPHA не описан в MSDN за январь 2000 года и найден экспериментально (и подглядыванием в wingdi.h :) При его задании используется альфа-канал растра источника и не используется альфа-канал приемника (возможно и такое).

СОВЕТ
При использовании альфа-канала у вас все равно остается возможность без пересчета битов растра изменять прозрачность всей картинки - SourceConstantAlpha работает и в этом случае.

И в завершение напомню, что AlphaBlend() также требует включения в проект при сборке библиотеки импорта msimg32.lib, которая отсутствует в Windows 95.

/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /

Как создать многострочный тултип?

Автор: Александр Шаргин

Начиная с версии 4.70 библиотеки Comctl32.dll тултипы поддерживают многострочный режим работы. По умолчанию он выключен, и всё, что требуется от нас - активизировать его. Для этого предназначено сообщение TTM_SETMAXTIPWIDTH, которое позволяет задать ширину тултипа (в пикселях). По умолчанию ширина установлена в -1, что соответствует однострочному режиму работы. В этом режиме тултип игнорирует все пары '\r\n' в тексте подсказки, выдавая его в одну строку. Задание любого положительного значения ширины переводит тултип в многострочный режим работы.

В многострочном режиме тултип корректно обрабатывает комбинации '\r\n', переходя на новую строку. Кроме того, он старается вписать текст в заданную ширину, разбивая его на строки самостоятельно. Переход на новую строку возможен только между словами, поэтому если в тексте подсказки есть длинные слова, заданная ширина может быть превышена. Если вы не хотите, чтобы тултип разбивал текст на строки по своему усмотрению, задайте значение ширины, заведомо превышающее ширину экрана. Например:


SendMessage(hTip, TTM_SETMAXTIPWIDTH, 0, (LPARAM)0xFFFFFF);

В MFC аналогичного эффекта можно добиться, используя фукцию CToolTipCtrl::SetMaxTipWidth. Единственный параметр, который она получает - новое значение ширины тултипа. Например:

// m_tt - объект класса CToolTipCtrl
m_tt.SetMaxTipWidth(0xFFFFFF);

Проблемы возникают только в том случае, когда вы используете встроенную поддержку тултипов класса CWnd. В этом случае тултип создаётся и уничтожается в недрах MFC, причём документированного способа добраться до него не существует. Выйти из положения можно, воспользовавшись недокументированным: MFC сохраняет указатель на созданный ею объект класса CToolTipCtrl в структуре _AFX_THREAD_STATE, и можно получить к нему доступ, используя выражение AfxGetThreadState()->m_pToolTip.

Вторая проблема состоит в том, что MFC сама следит за временем жизни тултипа, и мы не можем точно знать, когда он уничтожается и создаётся заново. Поэтому ширину тултипа необходимо заново задавать всякий раз, когда тултип "собирается" появиться на экране. Удобнее всего делать это в ответ на уведомление TTN_NEEDTEXT, например:


BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFFFFFF, OnToolTipText)
END_MESSAGE_MAP()

BOOL CMainFrame::OnToolTipText(...)
{
    CToolTipCtrl* ptt = AfxGetThreadState()->m_pToolTip;

    ptt->SetMaxTipWidth(0xFFFFFF);

    return CFrameWnd::OnToolTipText(...);
}

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / /

Здравствуйте!

Благодарю Вас за прекрасную и очень полезную рассылку! Полагаю, что мою благодарность разделяют очень многие ее читатели.

В номере 36 от 11.03.2001 г. рассылки "Программирование на VC++" Сергей Петухов задал вопрос о русификации элементов окна предварительного просмотра перед печатью. В номере 38 от 25.03.2001 г. на него дал обстоятельный ответ Александр Шаргин. Предложенный в ответе метод решения проблемы имеет огромное неоспоримое достоинство - он работоспособен! Однако, не лишен и некоторых очевидных недостатков, связанных с необходимостью ручного редактирования информации ресурсных файлов в каждом из проектов, ориентированных на русскоязычного пользователя. Такой метод пригоден скорее для случаев, когда нужно изменить именно вид стандартного интерфейса MFC в соответствии с требованиями конкретного приложения, а не его язык.

MSDN предлагает иной путь решения. Кратко он освещен в статье "Localization of MFC Components" (MSDN/Visual C++ Documentation/Reference/MFC Library and Templates/MFC Library/MFC Technical notes/TN057). Чуть подробнее последовательность необходимых действий описан в статье "HOWTO: #include the Localized MFC Resources in an EXE or DLL" (ID: Q198536) Knowledge Base.

Суть заключается в том, что все языкозависимые ресурсы MFC размещены в каталогах MFC\INCLUDE\L.* и при статическом присоединении ресурсов MFC к проекту, должны использоваться именно они. Для подключения нужного подкаталога достаточно указать его в командной строке компилятора ресурсов с ключом /I (пример из MSDN /IC:\PROGRAM FILES\DEVSTUDIO\VC\MFC\INCLUDE\L.DEU).

Все довольно просто. Создаем новый подкаталог MFC\INCLUDE\L.RUS, записываем в него по образу других языков семь файлов *.rc и русифицируем их в текстовом редакторе (в редакторе ресурсов VS это сделать не удастся, т.к. файлы защищены от изменения специальной директивой). Затем указываем на вкладке свойств проекта Resources (поле Additional resource include directories) наш новый русскоязычный каталог (например, C:\PROGRAM FILES\DEVSTUDIO\VC\MFC\INCLUDE\L.RUS). В статье из KB рекомендуется еще и удалить все директивы _AFXDLL препроцессора, которые видны на той же вкладке, но в моих приложениях, со статическим присоединением ресурсов MFC, таковые не попадались. Затем открываем меню "View" и выбираем пункт "Resource Includes" и в окне "Compile-time directives" изменяем код языка и номер кодовой страницы. Данные о кодах можно найти в файле "winnt.h". Там они представлены в шестнадцатеричном формате и их следует привести к основанию 10. Для русского языка нужные строки будут выглядеть так:

LANUGAGE 25, 1
#pragma code_page(1251)

После этого проект можно собирать.

В статье из KB сказано, что в том же меню "View" - "Resource Includes" можно указать измененные маршруты заголовочных файлов, например, #include "l.rus/afxprint.rc" вместо #include "afxprint.rc". Вероятно, если это сделать, отпадет необходимость в указании маршрута каталога с русифицированными ресурсами на вкладке "Resources" свойств проекта. Кроме того, в той же статье рекомендуется с помощью редактора ресурсов удалить из раздела "String Table" все неспецифичные для конкретного приложения строки, сгенерериванные AppWizard. Полагаю, это можно не делать. В крайнем случае, на необходимость такой операции укажет компилятор.

Таким образом, нужно один раз перевести на русский язык содержимое нескольких файлов ресурсов, а затем лишь указывать в проектах нужный язык и маршрут каталога с русифицированными файлами.
- Андрей Шуклин

Большое спасибо, Андрей.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter   jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.

Предыдущие выпуски     Статистика рассылки