WS_EX_LAYERED и OpenGL

Автор: Сапронов Андрей Юрьевич
Источник: RSDN Magazine #1-2004
Опубликовано: 16.09.2004
Версия текста: 1.0
WS_EX_LAYERED
SetLayeredWindowAttributes
OpenGL
DIB Section
Правильный PIXELFORMATDESCRIPTOR
WM_PAINT
WM_SIZE
Заключение

Код к статье

Начиная с Windows 2000, компания Microsoft включила в состав операционной системы поддержку окон, созданных с расширенным стилем WS_EX_LAYERED. В результате стало возможным очень просто создавать окна с различной степенью прозрачности, объявлять прозрачные (для данного окна) цвета и так далее. Все это достигается при помощи двух функций WinAPI: SetLayeredWindowAttributes и UpdateLayeredWindow.

Однако когда автор попробовал работать с OpenGL-контекстом, результата, в смысле прозрачности окон, удалось добиться далеко не сразу. Эта статья и расскажет о том, как работать с OpenGL в полупрозрачных окнах.

WS_EX_LAYERED

Напомню, чтобы использовать “слоистые” окна (Layered Windows), они должны быть созданы при помощи функции CreateWindowEx, которая отличается от CreateWindow наличием еще одного (первого параметра), задающего расширенный стиль окна, в нашем случае он должен быть WS_EX_LAYERED.

Кроме того, вы можете вызвать функцию SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED) для установки требуемого стиля уже существующего окна.

При использовании констант и функций, имеющих отношение к “слоистым” окнам, в вашем приложении необходимо показать, что вы пишете код именно для Windows 2000 (и выше), например, можно так: define _WIN32_WINNT 0x0500.

SetLayeredWindowAttributes

В примере я буду использовать функцию SetLayeredWindowAttributes, так как она проще в использовании и, в отличие от UpdateLayeredWindow, не усложняет суть дела.

Функция очень простая. Описание функции приведено ниже:

WINUSERAPI BOOL WINAPI
  SetLayeredWindowAttributes(HWND hwnd,
    COLORREF crKey, BYTE bAlpha, DWORD dwFlags);

hWnd – это хэндл окна, для которого хотим применить эту функцию. crKey – цвет, который мы хотим сделать прозрачным. bAlpha – степень прозрачности окна. При bAlpha, равном нулю, окно делается полностью прозрачным. При bAlpha, равном 255 (0xff), окно становится совершенно непрозрачным. dwFlags может принимать два значения – LWA_COLORKEY и LWA_ALPHA, которые могут использоваться совместно при помощи операции «логического ИЛИ». В случае, если используется значение LWA_COLORKEY, то всё окно делается абсолютно прозрачным, а прозрачность цвета, совпадающего с указанным в crKey, определяется параметром bAlpha. Например, если указать

SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 0x0, LWA_COLORKEY)

то прозрачным окажутся только те части окна, которые закрашены в чёрный цвет.

Наоборот, если будет указано

SetLayeredWindowAttributes(hWnd, RGB(0xff, 0xff, 0xff), 0x0, LWA_COLORKEY)

то прозрачными окажутся только те части, которые закрашены белым цветом.

Если флаг LWA_COLORKEY не установлен, то прозрачным делается всё окно, независимо от того, какими цветами оно раскрашено. Таким образом, можно создать окно с полностью прозрачными частями (crKey) или все окно будет полупрозрачным с определенной степенью (bAlphа=0…255). Инициализация и использование может выглядеть так:

      // для использования функции SetLayeredWindowAttributes и
      // связанных с ней констант
      #define _WIN32_WINNT 0x0500	

#include <windows.h>

// …const TCHAR szAppName[] = TEXT("LayeredWnds");	// Название приложенияconst TCHAR szAppTitle[] = TEXT("LayeredWnds");	// Заголовок главного окна // …int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
// …
  HWND hWnd = CreateWindowEx(WS_EX_LAYERED, szAppName, szAppTitle, 
WS_POPUP | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 200, 200, 
NULL, NULL, hInstance, NULL);

// пока не будет вызвана SetLayeredWindowAttributes или UpdateLayeredWindow// окно никак иначе отобразить не удастся
  SetLayeredWindowAttributes(hWnd, 0x0, 100, LWA_COLORKEY);
  UpdateWindow(hWnd);
}

Ниже приведён возможный код обработки сообщений WM_PAINT:

HDC hdc; PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);

// текущее положение окна (клиентской части)
RECT r; GetClientRect(hWnd, &r);
// создаем шрифт
HFONT font = CreateFont(90, 30, 0, 0, 150, 0, 0, 0,
  ANSI_CHARSET, OUT_DEVICE_PRECIS, 
  CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH, _T("Arial"));
// выбираем шрифт
SelectObject(hdc, font);
// и удаляем
verify(DeleteFont(font)); ..// verify – это макрос (см. примеры на CD)// пишем в соответсвующем режиме
SetBkMode(hdc, TRANSPARENT);
DrawText(hdc, _T("RSDN"), 4, &r, DT_VCENTER | DT_CENTER | DT_SINGLELINE );

// конец  рисованию
EndPaint(hWnd, &ps);

В зависимости от параметров SetLayeredWindowAttributes в данном случае можно получить следующее:


При работе с SetLayeredWindowAttributes можно пользоваться не только средствами GDI, но и библиотекой GDI+, которая может значительно упростит работу при выводе графических примитивов.

OpenGL

А теперь, собствено то, что навело меня на мысль о написании этой статьи. В своей работе я использую OpenGL. Однако в отличие от GDI+ при использовании SetLayeredWindowAttributes в приложении с OpenGL окно как было непрозрачным, так и осталось. Причина этого кроется в механизме вывода OpenGL контекста.

ПРИМЕЧАНИЕ

В дальнейшем я буду полагать, что читатель знаком с OpenGL и в состоянии написать минимальное WinAPI-приложение с использованием OpenGL. См. например: Баяковский Ю., Игнатенко А., Фролов А. “Графическая библиотека OpenGL. Методическое пособие”.

Схема работы такого приложения выглядит примерно так (механизм двойной буферизации включён):


Стандартное OpenGL приложение

где Off-screen Buffer может являться как DIB-секцией (DIB-section, см. ниже), так и аппаратно реализованной частью графического ускорителя. Однако вы не сможете получить прямой доступ к этому буферу, а можете только вывести его функцией SwapBuffers в соответствующую часть окна. Инициализация подобного приложения заканчивается созданием OpenGL контекста HGLRC.

DIB Section

Изменим процесс инициализации сообразно нашим целям. Схема работы приложения станет несколько иной:


OpenGL посредством GDI

В качестве bitmap будет выступать DIB-секция (DIB section) – bitmap (по сути просто область памяти) в который приложение может напрямую записывать все, что захочет. Его можно создать функцией CreateDIBSection:

HDC pdcDIB;                      // контекст устройства в памяти
HBITMAP hbmpDIB;                 // и его текущий битмапvoid *pBitsDIB(NULL);            // содержимое битмапаint cxDIB(200); int cyDIB(300);  // его размеры (например для окна 200х300)
BITMAPINFOHEADER BIH;            // и заголовок// …// создаем DIB section// создаем структуру BITMAPINFOHEADER, описывающую наш DIBint iSize = sizeof(BITMAPINFOHEADER);  // размер
memset(&BIH, 0, iSize);

BIH.biSize = iSize;        // размер структуры
BIH.biWidth = cxDIB;       // геометрия
BIH.biHeight = cyDIB;      // битмапа
BIH.biPlanes = 1;          // один план
BIH.biBitCount = 24;       // 24 bits per pixel
BIH.biCompression = BI_RGB;// без сжатия// создаем новый DC в памяти
pdcDIB = CreateCompatibleDC(NULL);

// создаем DIB-секцию
hbmpDIB = CreateDIBSection(
  pdcDIB,                  // контекст устройства
  (BITMAPINFO*)&BIH,       // информация о битмапе
  DIB_RGB_COLORS,          // параметры цвета
  &pBitsDIB,               // местоположение буфера (память выделяет система)
  NULL,                    // не привязываемся к отображаемым в память файлам
  0);

// выберем новый битмап (DIB section) для контекста устройства в памяти
SelectObject(pdcDIB, hbmpDIB);

Правильный PIXELFORMATDESCRIPTOR

Теперь опишем сам OpenGL-контекст. Необходимо заставить OpenGL выводить графику в подготовленный нами bitmap. Для этого используется функция SetPixelFormat и структура PIXELFORMATDESCRIPTOR:

        // создаем контекст OpenGL
        // главное отличие (после расположения буффера) - 
        // флаг PFD_DRAW_TO_BITMAP, т.е. показываем, что надо
        // рисовать "в картинку"
DWORD dwFlags=PFD_SUPPORT_OPENGL | PFD_DRAW_TO_BITMAP;

// заполним соответсвующим образом PIXELFORMATDESCRIPTOR
PIXELFORMATDESCRIPTOR pfd ;
memset(&pfd, 0x0, sizeof(PIXELFORMATDESCRIPTOR)); // сначала нулями

pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); 	   // размер
pfd.nVersion = 1;                             // номер версии
pfd.dwFlags =  dwFlags ;                      // флаги (см. выше)
pfd.iPixelType = PFD_TYPE_RGBA ;              // тип пикселя
pfd.cColorBits = 24 ;                         // 24 бита напиксель
pfd.cDepthBits = 32 ;                         // 32-битный буффер глубины
pfd.iLayerType = PFD_MAIN_PLANE ;             // тип слоя// выберем для нашего контекста созданный формат пикселяint nPixelFormat = ChoosePixelFormat(pdcDIB, &pfd);
// и установим его
BOOL bResult = SetPixelFormat(pdcDIB, nPixelFormat, &pfd);
// собственно, контекст для рендеринга готов
m_hrc = wglCreateContext(pdcDIB);

WM_PAINT

В отличие от программ, в которых используется классическая двойная буферизация, нет необходимости вызывать SwapBuffers() из функции отрисовки сцены, достаточно вызвать лишь glFlush, например:

        void display()
{
  // очищаем буффер цвета и глубины
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
  auxSolidTeapot(0.5); // рисуем… чайник
  glFlush();           // используем glFlush вместо SwapBuffers
}

После вызова glFlush результат работы OpenGL-функций окажется в DIB-секции и связанном с ней контексте устройства в памяти (pdcDIB). После этого необходимо содержимое этого контекста перенести в контекст, связанным с нашим окном. Это можно сделать при помощи BitBlt или SetDIBitsToDevice. С учетом всего этого обработчик сообщения WM_PAINT может иметь следующий вид:

PAINTSTRUCT ps;
HDC hDC = BeginPaint(hWnd, &ps);
display();                                              // OpenGL -> DIB
BitBlt(hDC, 0, 0, cxDIB, cyDIB, pdcDIB, 0, 0, SRCCOPY); // DIB -> hDC// можно и так:// BITMAPINFO bmp_info;// memset(&bmp_info, 0x0, sizeof(bmp_info));// bmp_info.bmiHeader=BIH;// SetDIBitsToDevice(hDC, 0, 0, cxDIB, cxDIB, 0, 0, 0, cyDIB, bmp_cnt, &bmp_info,// DIB_RGB_COLORS);
EndPaint(hWnd, &ps);  

WM_SIZE

При изменении размеров окна необходимо в дополнение к действиям, обычным для данной ситуации (glViewport и изменения соотвествующих матриц), произвести повторную инициализацию DIB-секции. Этот процесс аналогичен ее созданию, меняются только размеры окна (переменные cxDIB и cуDIB, см. выше).

Заключение

Если все сделано правильно, то после вызова функции display окно будет имет вид:


Вот, собственно, и все. Таким способом можно создавать очень красивые окна или элементы управления. Однако не забывайте, что скорость у такого рода приложений на порядок меньше, чем у “чистого” OpenGL.

ПРИМЕЧАНИЕ

Схемы в данной статье сделаны по мотивам рисунков статьи Dale Rogerson, ”OpenGL VI: Rendering on DIBs with PFD_DRAW_TO_BITMAP”, april 18, 1995 из MSDN.


Эта статья опубликована в журнале RSDN Magazine #1-2004. Информацию о журнале можно найти здесь