Предыдущая заметка была посвящена созданию круглого окна в Windows. Усложним этот проект, добавив в круглое окно две области.
В нашем проекте круглое окно было создано методом обрезки содержимого. Cоздадим круглое окно другим способом, а именно используем многослойное окно (layered window). Многослойное окно в Win32 создаётся при помощи функции
с заданием расширенного стиля окна как . В функции в файле RndWnd.cpp заменим код с функцией на:hWnd = CreateWindowEx(WS_EX_LAYERED, szWindowClass, szTitle, WS_POPUP, 10, 10, MAIN_WIN_SIZE, MAIN_WIN_SIZE, NULL, NULL, hInstance, NULL);
Далее необходимо установить цвет прозрачности для многослойного окна. Все области многослойного окна, окрашенные в этот цвет, при выводе окна на экран будут прозрачными. Зададим константу, определяющую цвет прозрачности. Пусть это будет черный цвет:
#define TRANSPARENT_COLOR RGB(0,0,0)
Установка цвета прозрачности для многослойного окна выполняется при помощи функции
. Добавим в функции после кода, создающего окно, следующую строку:SetLayeredWindowAttributes(hWnd, TRANSPARENT_COLOR, 0, LWA_COLORKEY);
Теперь, если начать рисовать в окне, то все, что будет окрашено черным цветом, при отображении окна станет прозрачным. Именно прорисовкой мы сейчас и займемся.
При необходимости обновления изображения в окне система посылает этому окну сообщение
. Для подготовки к обновлению изображения в обработчике сообщения WM_PAINT необходимо вызвать функцию . Данная функция возвращает дескриптор контекста устройства (device context) для дисплея. Этот дескриптор будет использоваться для операций прорисовки. По завершению обновления изображения необходимо в обработчике WM_PAINT вызвать функцию . Вызов этих функций уже был добавлен мастером при создании проекта. Можно начинать рисовать, но отрисовка изображения непосредственно в контексте дисплея может привести к мерцанию изображения. Поэтому лучше использовать буферизацию, т.е. создать совместимый с контекстом дисплея контекст в памяти (буфер), отрисовать в нем изображение, а затем одной операцией перенести изображение из буфера в контекст дисплея. Создадим контекст в памяти, добавив в обработчик WM_PAINT следующий код:
HDC hCompDC = NULL;
RECT ClRect;
HBITMAP hCompBitmap = NULL;
HBITMAP hOldBitmap = NULL;
hCompDC = CreateCompatibleDC(hDC);
GetClientRect(hWnd, &ClRect);
hCompBitmap = CreateCompatibleBitmap(hDC, ClRect.right, ClRect.bottom);
hOldBitmap = (HBITMAP)SelectObject(hCompDC, hCompBitmap);
- контекст устройства в памяти. После завершения операций необходимо освободить все задействованные ресурсы. Код освобождающий ресурсы для контекста в памяти выглядит так:
SelectObject(hCompDC, hOldBitmap);
DeleteObject(hCompBitmap);
DeleteDC(hCompDC);
Сделаем фон окна прозрачным. Для этого нам необходимо окрасить его в цвет прозрачности, в данном случае черный. Окрасить прямоугольную область можно с помощью функции
, передав ей в качестве параметра предварительно созданную кисть. Кисть можно создать, используя функцию .
HBRUSH hTransBrush = CreateSolidBrush(TRANSPARENT_COLOR);
FillRect(hCompDC, &ClRect, hTransBrush);
DeleteObject(hTransBrush);
Далее для удобства создадим функцию
, где и будем выполнять дальнейшую прорисовку окна.
void OnPaint(HDC hDC, RECT& ClRect)
{
}
Добавим вызов этой функции в обработчик WM_PAINT.
OnPaint(hCompDC, ClRect);
Созданное изображение из контекста в памяти необходимо перенести в контекст дисплея. Делается это при помощи функции
.BitBlt(hDC, 0, 0, ClRect.right, ClRect.bottom, hCompDC, 0, 0, SRCCOPY);
Код в обработчике WM_PAINT выполняет подготовительные и вспомогательные функции. Основная прорисовка будет выполняться в функции OnPaint. Для прорисовки используем графическую библиотеку GDI+. Для подключения GDI+ к проекту включим ее заголовок в файл stdafx.h:
#include
using namespace Gdiplus;
Также укажем компоновщику, что нужно добавить библиотеку
при компоновке. Это делается в свойствах проекта в разделе . Кроме того, нужно добавить код для инициализации и деинициализации библиотеки GDI+ соответственно при запуске и завершении работы приложения. Добавим этот код в функцию входа в приложение .
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
// Инициализация GDI+
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
...
// Деинициализация GDI+
GdiplusShutdown(gdiplusToken);
Теперь библиотека GDI+ подключена и инициализирована. Можно ее использовать. Создадим в окне две области в виде контура (Path). Для этого добавим в файл RndWnd.cpp два глобальных указателя на объект типа
и функцию , в которой инициализируем эти указатели. Также в функции CreateAreas для каждой области нарисуем контур в виде дуги, у которой начало и конец соединены прямой линией.
GraphicsPath* g_pRightPath = NULL; // Правая область главного окна
GraphicsPath* g_pLeftPath = NULL; // Левая область главного окна
...
// Создание областей главного окна
void CreateAreas()
{
// Вычислить параметры областей
REAL AreaOffset = 4.0;
RectF AreaRect(AreaOffset, AreaOffset,
MAIN_WIN_SIZE - 2*AreaOffset, MAIN_WIN_SIZE - 2*AreaOffset);
REAL OffsetAngle = 180 * asin((AreaOffset/2) / (AreaRect.Width/2)) / 3.14159265f;
// Создание правой области
g_pRightPath = new GraphicsPath();
g_pRightPath->AddArc(AreaRect, OffsetAngle - 90, 180 - 2*OffsetAngle);
g_pRightPath->CloseFigure();
// Создание левой области
g_pLeftPath = new GraphicsPath();
g_pLeftPath->AddArc(AreaRect, 90 + OffsetAngle, 180 - 2*OffsetAngle);
g_pLeftPath->CloseFigure();
}
Так как для объектов GraphicsPath мы выделили память, то необходимо будет её освободить при завершении работы программы. Для этого в конец функции _tWinMain вставим следующий код.
// Освободить ресурсы
if (g_pRightPath != NULL)
delete g_pRightPath;
if (g_pLeftPath != NULL)
delete g_pLeftPath;
Функцию CreateAreas мы создали, но она ниоткуда ещё не вызывается. Неплохой вариант вызвать эту функцию в момент, когда главное окно создано, но ещё не выведено на экран, т.е. в обработчике сообщения WM_CREATE. Вставим код обработчика сообщения WM_CREATE в оконную функцию
.
// Создание главного окна
case WM_CREATE:
// Создать области главного окна
CreateAreas();
return 0;
Теперь в функции OnPaint можно залить контуры областей цветом. Но усложним себе задачу. Будем подсвечивать область, когда внутри ее находится курсор. Для этого в файле RndWnd.cpp объявим тип перечисление
со списком констант, определяющих какая область в данный момент подсвечена, а также определим глобальную переменную этого типа.
enum AREA_STATE
{
NONE,
LEFT,
RIGHT
};
...
AREA_STATE g_AreaState = NONE; // Состояние областей главного окна
Всё, наконец добрались до кода в функции
. Рисуем в ней фон окна в виде круга, что и сделает наше окно круглым. Далее заливаем цветом контуры областей в зависимости от значения переменной .
Graphics graph(hDC);
// Прорисовка фона областей
SolidBrush brush(Color(150, 200, 200, 255));
graph.FillEllipse(&brush, 0, 0, ClRect.right, ClRect.bottom);
// Прорисовка правой области
SolidBrush RightPathBrush(Color(200, 0, 0));
if (g_AreaState == RIGHT)
RightPathBrush.SetColor(Color(255, 0, 0));
graph.FillPath(&RightPathBrush, g_pRightPath);
// Прорисовка левой области
SolidBrush LeftPathBrush(Color(0, 200, 0));
if (g_AreaState == LEFT)
LeftPathBrush.SetColor(Color(0, 255, 0));
graph.FillPath(&LeftPathBrush, g_pLeftPath);
Теперь осталось вставить код, меняющий значение переменной
в зависимости от положения курсора. Для этого в обработчике , если мышь не захвачена окном, т.е. нет операции перемещения окна, определяем в какой области находится курсор. Это можно сделать при помощи метода класса . Определив область, в которой находится курсор, задаем переменной g_AreaState соответствующее значение или значение , если курсор находится вне областей. Казалось бы всё, но тут есть один нюанс. При быстром перемещении курсора из области за границы окна в момент, когда курсор будет находится в окне, но вне области, сообщение WM_MOUSEMOVE может не прийти. Получится, что область останется подсвеченной, хотя крсор будет вне её. Для решения этой проблемы нам необходимо получить уведомление, когда курсор покинет окно, чтобы установить значение переменной g_AreaState в NONE. Такое уведомление существует - это сообщение , но оно посылается только в ответ на предварительный вызов функции . То есть каждый раз, когда мы захотим узнать, когда курсор вышел за пределы окна, нужно вызвать функцию TrackMouseEvent. Еще необходимо не забыть, что изменив значение переменной g_AreaState, также требуется обновить изображение на экране, вызвав последовательно функции и . Добавим необходимый код в обработчики WM_MOUSEMOVE и WM_MOUSELEAVE.
// Перемещение мыши
case WM_MOUSEMOVE:
{
// Получить позицию мыши
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
if (GetCapture() == hWnd)
{// Мышь захвачена
// Переместить окно
RECT WndRect;
::GetWindowRect(hWnd, &WndRect);
int iOffset_xPos = xPos - iMouseCapture_xPos;
int iOffset_yPos = yPos - iMouseCapture_yPos;
::SetWindowPos(hWnd, NULL, WndRect.left + iOffset_xPos,
WndRect.top + iOffset_yPos, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
else
{// Мышь не захвачена
if (g_pRightPath->IsVisible(xPos, yPos))
{// Мышь в правой области
if (g_AreaState != RIGHT)
{
g_AreaState = RIGHT;
// Обновить изображение
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
// Включить отслеживание выхода мыши за пределы окна
TRACKMOUSEEVENT trkmsev;
trkmsev.cbSize = sizeof(trkmsev);
trkmsev.dwFlags = TME_LEAVE;
trkmsev.hwndTrack = hWnd;
TrackMouseEvent(&trkmsev);
}
}
else if (g_pLeftPath->IsVisible(xPos, yPos))
{// Мышь в левой области
if (g_AreaState != LEFT)
{
g_AreaState = LEFT;
// Обновить изображение
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
// Включить отслеживание выхода мыши за пределы окна
TRACKMOUSEEVENT trkmsev;
trkmsev.cbSize = sizeof(trkmsev);
trkmsev.dwFlags = TME_LEAVE;
trkmsev.hwndTrack = hWnd;
TrackMouseEvent(&trkmsev);
}
}
else
{// Мышь вне областей
if (g_AreaState != NONE)
{
g_AreaState = NONE;
// Обновить изображение
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
// Отменить отслеживание выхода мыши за пределы окна
TRACKMOUSEEVENT trkmsev;
trkmsev.cbSize = sizeof(trkmsev);
trkmsev.dwFlags = TME_LEAVE | TME_CANCEL;
trkmsev.hwndTrack = hWnd;
TrackMouseEvent(&trkmsev);
}
}
}
}
return 0;
...
// Выход мыши за пределы окна
case WM_MOUSELEAVE:
if (g_AreaState != NONE)
{
g_AreaState = NONE;
// Обновить изображение
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
}
return 0;
Полный исходный код примера можно загрузить здесь