электроника & программирование музыкальные устройства на микроконтроллерах

К оглавлению

Создание нестандартных окон в Windows

Создание круглого окна c двумя областями

Предыдущая заметка была посвящена созданию круглого окна в Windows. Усложним этот проект, добавив в круглое окно две области.

Создание окна

В нашем проекте круглое окно было создано методом обрезки содержимого. Cоздадим круглое окно другим способом, а именно используем многослойное окно (layered window). Многослойное окно в Win32 создаётся при помощи функции CreateWindowEx с заданием расширенного стиля окна как WS_EX_LAYERED. В функции InitInstance в файле RndWnd.cpp заменим код с функцией CreateWindow на:

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. Добавим в функции InitInstance после кода, создающего окно, следующую строку:

SetLayeredWindowAttributes(hWnd, TRANSPARENT_COLOR, 0, LWA_COLORKEY);

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

Обработчик WM_PAINT

При необходимости обновления изображения в окне система посылает этому окну сообщение WM_PAINT. Для подготовки к обновлению изображения в обработчике сообщения WM_PAINT необходимо вызвать функцию BeginPaint. Данная функция возвращает дескриптор контекста устройства (device context) для дисплея. Этот дескриптор будет использоваться для операций прорисовки. По завершению обновления изображения необходимо в обработчике WM_PAINT вызвать функцию EndPaint. Вызов этих функций уже был добавлен мастером при создании проекта. Можно начинать рисовать, но отрисовка изображения непосредственно в контексте дисплея может привести к мерцанию изображения. Поэтому лучше использовать буферизацию, т.е. создать совместимый с контекстом дисплея контекст в памяти (буфер), отрисовать в нем изображение, а затем одной операцией перенести изображение из буфера в контекст дисплея. Создадим контекст в памяти, добавив в обработчик 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);

hCompDC - контекст устройства в памяти. После завершения операций необходимо освободить все задействованные ресурсы. Код освобождающий ресурсы для контекста в памяти выглядит так:

 SelectObject(hCompDC, hOldBitmap);
 DeleteObject(hCompBitmap);
 DeleteDC(hCompDC);

Сделаем фон окна прозрачным. Для этого нам необходимо окрасить его в цвет прозрачности, в данном случае черный. Окрасить прямоугольную область можно с помощью функции FillRect, передав ей в качестве параметра предварительно созданную кисть. Кисть можно создать, используя функцию CreateSolidBrush.

 HBRUSH hTransBrush = CreateSolidBrush(TRANSPARENT_COLOR);
 FillRect(hCompDC, &ClRect, hTransBrush);
 DeleteObject(hTransBrush);

Далее для удобства создадим функцию OnPaint, где и будем выполнять дальнейшую прорисовку окна.

 void OnPaint(HDC hDC, RECT& ClRect)
 {
 }

Добавим вызов этой функции в обработчик WM_PAINT.

 OnPaint(hCompDC, ClRect);

Созданное изображение из контекста в памяти необходимо перенести в контекст дисплея. Делается это при помощи функции BitBlt.

 BitBlt(hDC, 0, 0, ClRect.right, ClRect.bottom, hCompDC, 0, 0, SRCCOPY);

Прорисовка содержимого окна в OnPaint

Код в обработчике WM_PAINT выполняет подготовительные и вспомогательные функции. Основная прорисовка будет выполняться в функции OnPaint. Для прорисовки используем графическую библиотеку GDI+. Для подключения GDI+ к проекту включим ее заголовок в файл stdafx.h:

 #include
 using namespace Gdiplus;

Также укажем компоновщику, что нужно добавить библиотеку gdiplus.lib при компоновке. Это делается в свойствах проекта в разделе Компоновщик->Ввод->Дополнительные зависимости. Кроме того, нужно добавить код для инициализации и деинициализации библиотеки GDI+ соответственно при запуске и завершении работы приложения. Добавим этот код в функцию входа в приложение _tWinMain.

 GdiplusStartupInput gdiplusStartupInput;
 ULONG_PTR gdiplusToken;
 
 // Инициализация GDI+
 GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
 ...
 // Деинициализация GDI+
 GdiplusShutdown(gdiplusToken);

Теперь библиотека GDI+ подключена и инициализирована. Можно ее использовать. Создадим в окне две области в виде контура (Path). Для этого добавим в файл RndWnd.cpp два глобальных указателя на объект типа GraphicsPath и функцию CreateAreas, в которой инициализируем эти указатели. Также в функции 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 в оконную функцию WndProc.

 // Создание главного окна
 case WM_CREATE:
   // Создать области главного окна
   CreateAreas();
   return 0;

Теперь в функции OnPaint можно залить контуры областей цветом. Но усложним себе задачу. Будем подсвечивать область, когда внутри ее находится курсор. Для этого в файле RndWnd.cpp объявим тип перечисление AREA_STATE со списком констант, определяющих какая область в данный момент подсвечена, а также определим глобальную переменную g_AreaState этого типа.

 enum AREA_STATE
 {
   NONE,
   LEFT,
   RIGHT
 };
 ...
 AREA_STATE g_AreaState = NONE; // Состояние областей главного окна

Всё, наконец добрались до кода в функции OnPaint. Рисуем в ней фон окна в виде круга, что и сделает наше окно круглым. Далее заливаем цветом контуры областей в зависимости от значения переменной g_AreaState.

 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, если мышь не захвачена окном, т.е. нет операции перемещения окна, определяем в какой области находится курсор. Это можно сделать при помощи метода IsVisible класса GraphicsPath. Определив область, в которой находится курсор, задаем переменной g_AreaState соответствующее значение или значение NONE, если курсор находится вне областей. Казалось бы всё, но тут есть один нюанс. При быстром перемещении курсора из области за границы окна в момент, когда курсор будет находится в окне, но вне области, сообщение WM_MOUSEMOVE может не прийти. Получится, что область останется подсвеченной, хотя крсор будет вне её. Для решения этой проблемы нам необходимо получить уведомление, когда курсор покинет окно, чтобы установить значение переменной g_AreaState в NONE. Такое уведомление существует - это сообщение WM_MOUSELEAVE, но оно посылается только в ответ на предварительный вызов функции TrackMouseEvent. То есть каждый раз, когда мы захотим узнать, когда курсор вышел за пределы окна, нужно вызвать функцию TrackMouseEvent. Еще необходимо не забыть, что изменив значение переменной g_AreaState, также требуется обновить изображение на экране, вызвав последовательно функции InvalidateRect и UpdateWindow. Добавим необходимый код в обработчики 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;

Исходный код примера

Полный исходный код примера можно загрузить здесь