Работа с регионами Windows GDI. Создание непрямоугольных окон.

Авторы: Сапронов Андрей Юрьевич
duan@id.ru
Опубликовано: 12.02.2003
Версия текста: 1.1

В статье рассматривается создание непрямоугольных окон с применением регионов Windows GDI. Помимо непосредственного описания создания непрямоугольных окон, описано использование WinApi функций по работе с регионами, а также соответсвующих им методов класса CRgn библиотеки MFC (далее будут рассматриваться функции WinApi, и если нет специальных оговорок, то всё, что касается этой функции, относится и к одноименному методу класса CRgn). Особое внимание уделено редко описывемым функциям GetRegionData, ExtCreateRegion (CRgn::CreateFromData для MFC). В контексте этих функций показана работа со структурой XFORM. Статья не преследует целей по формальному описанию функций. Частью данной статьи является демонстрационная программа (WinApi и MFC версия), содержащая полный исходный код, фрагменты которого приводятся.

Графический объект регион (в некоторых источниках region переводят как область) представляет (определяет) собой плоскую произвольную область. Также, предопеределен ряд простых форм региона: эллиптические, прямоугольные, прямоугольные с закругленными краями, полигональные. Однако, простота геометрическая не подразумевает под собой простоту внутренней реализации региона. Непосредственно при програмировании можно идти двумя путями: использование функций WinAPI или библиотеки MFC. При работе на WinAPI для манипулирования регионами используется хэндл HRGN и соотвествующая группа функций. MFC предоставляет для работы класс CRgn. Windows позволяет создавать регионы любой формы и степени сложности. Однако за это приходится платить относительно большой ценой – памятью, а в случае применения региона для создания окон – процессорным временем при перерисовке окна.

Применение регионов достаточно разнообразно: создание непрямоугольных окон; определениt принадлежности произвольной точки этому региону (можно узнать, кликнул ли пользователь по региону); как графические объекты регионы можно закрашивать, делать из них рамку…; создание маски для ограничения рисования (clipping).

Для начала, можно рассмотреть наиболее простые и понятные функции для работы с регионами, а также базовые функции по работе с окнами. В принципе, последних всего две: SetWindowRgn и GetWindowRgn. В своем примере я пользуюсь лишь первой. Уже из названия понятно назначение этой функции – установка региона окна заданной формы. Ни одна часть окна не будет отображаться вне заданного региона. Первый параметр функции SetWindowRgn(HWND hWnd, HRGN hRgn, BOOL bRedraw) – hWnd хэндл указывает к какому окну применяется эта функция, второй – hRgn определяет новую форму окна, а параметр bRedraw в случае не нулевого значения обеспечивает перерисовку окна (система посылает сообщения WM_WINDOWPOSCHANGING и WM_WINDOWPOSCHANGED).

У функции SetWindowRgn есть ряд особенностей:

Рассмотрим пример. Код, приведенный ниже, взят из обработчика события WM_SIZE. И показывает создание окна в виде эллипса.

создание окна в виде эллипса (WinAPI)
// где то в программе
HRGN hRgn;		// регион

// в обработчике события WM_SIZE
RECT rc; GetWindowRect(hWnd, &rc);	// прямоугольник окна

// так как необходимо задавать регион относительно левого верхнего угла окна, то нужно его сместить так, что бы верхний левый угол прямоугольника был (0, 0)
OffsetRect(&rc, -rc.left, -rc.top);
 
DeleteObject(hRgn);	// удаление ранее созданного региона
// создание региона в виде эллипса, ограниченного прямоугольником rc
hRgn = CreateEllipticRgnIndirect(&rc);

// установка созданного региона для окна и немедленная перерисовка этого окна
SetWindowRgn(hWnd, hRgn, TRUE);
создание окна в виде эллипса (MFC)

В этом фрагменте регион создается функцией CreateEllipticRgnIndirect. Кроме нее существует еще ряд аналогичных функций.

CreateRectRgn Создание региона прямоугольником
CreateRectRgnIndirect Создание региона прямоугольником, определенным структурой RECT
CreateEllipticRgn Создание региона эллипсом
CreateEllipticRgnIndirect Создание региона эллипсом, определенным структурой RECT
CreateRoundRectRgn Создание региона прямоугольником с закругленными краями

Все эти функции возвращаю хэндл созданного объекта. Если hRgn уже владел каким либо объектом, то перед вызовом функции его необходимо удалить. В противном случае, вы потеряете этот объект и он будет занимать ресурсы системы. Вот еще один простой пример.

Создание прямоугольного окна с закругленными краями (WinAPI)
// где то в программе
HRGN hRgn;		// регион

// в обработчике события WM_SIZE
RECT rc; GetWindowRect(hWnd, &rc);	// прямоугольник окна

OffsetRect(&rc, -rc.left, -rc.top);

DeleteObject(hRgn);

// создание региона в виде прямоугольника с закругленными краями
hRgn = CreateRoundRectRgn(
	rc.left, rc.top, rc.right, rc.bottom, 
	(rc.right-rc.left)/2, (rc.bottom-rc.top)/2);

// установка созданного региона для окна и немедленная перерисовка этого окна
SetWindowRgn(hWnd, hRgn, TRUE);
+Создание прямоугольного окна с закругленными краями (MFC)
// где то в программе
CRgn m_rgn;	// регион

// в обработчике события WM_SIZE
CRect rc; GetWindowRect(&rc);		
rc -= rc.TopLeft();

m_rgn.DeleteObject();		// удаление ранее созданного региона
	
// создание региона в виде прямоугольника с закругленными краями
m_rgn.CreateRoundRectRgn(rc.left, rc.top, rc.right, rc.bottom, rc.Width()/2, rc.Height()/2);

// установка созданного региона для окна и немедленная перерисовка окна
SetWindowRgn(m_rgn, TRUE);

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

После того как для объекта CRgn задан регион, объект допускает изменение региона при помощи следующих методов:

CombineRgn Устанавливает регион эквивалентным объединению двух определенных CRgn объектов
OffsetRgn Смещает регион, на заданное количество точек, по вертикали и/или горизонтали

Рассмотрим параметры функции CombineRgn(HRGN dest, HRGN src1, HRGN src2, int mode). Первый хэндл задает регион “приемник” для результата объединения следующих двух регионов по правилу определяемому четвертым параметром mode:

Для облегчения комбинирования областей в файле windowsx.h определены макрокоманды, предназначенные для копирования, пересечения, объединения и вычитания областей. Все они созданы на основе CombineRegion:

#define CopyRgn (hrgnDst, hrgnSrc) \
   CombineRgn(hrgnDst, hrgnSrc, 0, RGN_COPY)
#define IntersectRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_AND)
#define SubtractRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_DIFF)
#define UnionRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_OR)
#define XorRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_XOR)

Применение функции OffsetRgn уже было показано. Рассмотрим применение функции CombineRgn на примере создания окна в виде кольца с эллипсом посередине.

Демонстрация CombineRgn (WinAPI)
//внешняя граница кольца (создаем большой эллипс)
DeleteObject(hRgn);
hRgn = CreateEllipticRgn(rc.left,rc.top, rc.right,rc.bottom);
		
//внутрення граница кольца (создаем эллипс немного меньше – во временном буффере)
hHdrRgn = CreateEllipticRgn(	
		rc.left+(rc.right-rc.left)/4,
		rc.top+(rc.bottom-rc.top)/4, 
		rc.right-(rc.right-rc.left)/4,
		rc.bottom-(rc.bottom-rc.top)/4);

//создадим кольцо путем “вырезания” в большом эллипсе отверстия размером с меньший эллипс
CombineRgn(hRgn, hRgn, hHdrRgn, RGN_XOR);

// создание маленького эллипса в центре (во временном буфере)
DeleteObject(hHdrRgn);
hHdrRgn = CreateEllipticRgn(
		(rc.right-rc.left)/2-(rc.right-rc.left)/16,
		(rc.bottom-rc.top)/2-(rc.bottom-rc.top)/16, 
		(rc.right-rc.left)/2+(rc.right-rc.left)/16,
		(rc.bottom-rc.top)/2+(rc.bottom-rc.top)/16 );

// объединим полученные регионы (кольцо и маленький эллипс)
CombineRgn(hRgn, hRgn, hHdrRgn, RGN_OR);
Демонстрация CombineRgn (MFC)

Прокомментировать текст программы лучше всего схемами:


после создания двух эллипсов и вызова функции CombineRgn с параметром RGN_XOR;


вызов функции CombineRgn с параметром RGN_OR.

Как было обещано, рассмотрим применение функции CreatePolygonRgn.

создание окна в виде забора (WinAPI)
//точки для создания элемента ("доска") региона ("забор"). Имея одну “доску” мы затем “размножим” ее в цикле
POINT pnt[5];

pnt[0].x=rc.left; 		pnt[0].y=rc.bottom;
pnt[1].x=rc.left;		pnt[1].y=rc.top+(rc.bottom-rc.top)*0.75;
pnt[2].x=rc.left+(rc.right-rc.left)/8;	pnt[2].y=rc.top;
pnt[3].x=rc.left+(rc.right-rc.left)/4;	pnt[3].y=rc.top+(rc.bottom-rc.top)*0.75;
pnt[4].x=rc.left+(rc.right-rc.left)/4;	pnt[4].y=rc.bottom;

//создадим первый элемент ("доску")
DeleteObject(hRgn);
hRgn=CreatePolygonRgn(pnt, 5, ALTERNATE);

//добавим еще три "доски"...
for(n=0; n<3; n++){
//каждый раз смещая все пять точек доски на четверть размера окна
	for(k=0; k<5; k++)
		pnt[k].x+=(rc.right-rc.left)/4;

	//создавая новую "доску"
	hHdrRgn=CreatePolygonRgn(pnt, 5, ALTERNATE);

//и добавляя ее к уже сщуствующим
CombineRgn(hRgn, hRgn, hHdrRgn, RGN_OR);

DeleteObject(hHdrRgn);	//удаление промежуточного объекта
}

// установка созданного региона для окна и немедленная перерисовка этого окна
SetWindowRgn(m_rgn, TRUE);
создание окна в виде забора (MFC)

Результат работы этого фрагмента будет таков:


последовательное применение CombineRgn с параметром RGN_OR.

Использование параметров ALTERNATE = 1 или WINDING = POLYFILL_LAST = 2, в данном случае, не имеет значения. Эти определения играют роль в случае сложных пересекающихся регионах. При использовании этой функции обязательно учитывать направление обхода. Классическим примером, демонстрирующим различия между этими параметрами, является создание региона в виде звезды. При одном и том же передаваемом массиве вершин мы получаем разные регионы:


различие в применении параметров ALTERNATE и WINDING

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

ExtCreateRegion (CreateFromData – MFC) Создает новый регион из передаваемого региона и данных о преобразовании, определяемых структурой XFORM
GetRegionData Заполняет буфер данными, описывающими регион

Смысл этих функций в том, что имея массив данных о регионе и располагая структурой XFORM над регионом можно произвести следующие действия, определяемые полями этой структуры:

Поля структуры XFORM
Действие eM11 eM12 eM21 eM22
Поворот Косинус угла поворота Синус угла поворота Отрицательный синус угла поворота Косинус угла поворота
Масштабирование Горизонтальный коэффициент Вертикальный коэффициент
Смещение Горизонтальный коэффициент Вертикальный коэффициент
Отображение Горизонтальный коэффициент Вертикальный коэффициент

Кроме указанных функций в приведенном фрагменте будет использована дополнительная:

GetRgnBox Вычисляет координаты описанного вокруг региона прямоугольника
- трансформации регионов (WinAPI) трансформации регионов (WinAPI)
// трансформации
XFORM xf, xf2;

// буфер для хранения данных описывающих первый "луч"
// здесь вызов GetRegionData(HRGN, 0, NULL) с “нулями” возвращает число байт, необходимых для хранения информации о регионе. В данной статье содержимое структуры LPRGNDATA не имеет значения. Имеет  значение то, что она представляет определенный регион
LPRGNDATA lpRgnData;

// прямоугольник описанный вокруг региона
RECT rt; 

//массив точек для создания первого "луча"
POINT pnt[4];
pnt[0].x=rc.left;					pnt[0].y=rc.top+(rc.bottom-rc.top)/2;
pnt[1].x=rc.left+(rc.right-rc.left)*3/4;	pnt[1].y=rc.top+(rc.bottom-rc.top)*3/4;
pnt[2].x=rc.left+(rc.right-rc.left)/2;	pnt[2].y=rc.top+(rc.bottom-rc.top)/2;
pnt[3].x=rc.left+(rc.right-rc.left)*3/4;	pnt[3].y=rc.bottom-(rc.bottom-rc.top)*3/4;

// создадим первый "луч"
DeleteObject(hRgn);
hRgn = CreatePolygonRgn(pnt, 4, ALTERNATE);
_ASSERT(hRgn);
		
//буфер для хранения данных описывающих первый "луч"
lpRgnData = GlobalAlloc(GMEM_FIXED, sizeof(RGNDATA)*GetRegionData(hRgn, 0, NULL));
_ASSERT(lpRgnData);

//получение данных
GetRegionData(hRgn, GetRegionData(hRgn, 0, NULL), lpRgnData);
		
xf.eM11 = -1;		//в данном случае
xf.eM22 = 1;		//она описывает зеркальное
xf.eM12 = xf.eM21 = 0;	//отображение относительно
xf.eDx  = xf.eDy  = 0;	//оси ординат
            
//создание второго "луча" используя данный первого и струкутуры
//описывающей необходимые изменения
hHdrRgn = ExtCreateRegion(&xf, GetRegionData(hRgn, 0, NULL), lpRgnData);
_ASSERT(hHdrRgn);

//смещение второго "луча" (т. к. ось ординат проходит через
//верхний левый угол)
OffsetRgn(hHdrRgn, rc.right-rc.left, 0);

//объединение первого и второго "лучей"
CombineRgn(hRgn, hRgn, hHdrRgn, RGN_OR);

GlobalFree(lpRgnData);	//удаление промежуточного буфера

//еще буфер для хранения данных о двух первых "лучах"
lpRgnData = GlobalAlloc(GMEM_FIXED, sizeof(RGNDATA)*GetRegionData(hRgn, 0, NULL));	

//получение данных
GetRegionData(hRgn, GetRegionData(hRgn, 0, NULL), lpRgnData);

xf.eDx=xf.eDy=0;		//здесь структура XFORM
xf.eM11=xf.eM22=0;	//определяет поворот
xf.eM12=1;			//относительно центра
xf.eM21=-1;		//на 180 градусов
		
xf2.eDx=xf2.eDy=0;			// необходимая для масштабирования
xf2.eM21=xf2.eM12=0;			// повернутых лучей.
xf2.eM11=(float)(rc.right-rc.left)/(rc.bottom-rc.top);	// Так как в общем случае
xf2.eM22=(float)(rc.bottom-rc.top)/(rc.right-rc.left);	// окно не квадратное, а прямоугольное

CombineTransform(&xf, &xf, &xf2);	//объединение двух трансформаций
		
// и создание повернутых и промаштобированных "вертикальных лучей" 
DeleteObject(hHdrRgn);
hHdrRgn = ExtCreateRegion(&xf, GetRegionData(hRgn, 0, NULL), lpRgnData);

GlobalFree(lpRgnData);	//удаление промежуточного буфера

// сместим полученные "вертикальные лучи", это так же связано
// с положением центра координат в верхнем левом углу
GetRgnBox(hHdrRgn, &rt);
OffsetRgn(hHdrRgn,
-rt.left+(rc.left+(rc.right-rc.left)/2-(rt.right-rt.left)/2), -rt.top+(rc.top));

//получение "звезды"
CombineRgn(hRgn, hRgn, hHdrRgn, RGN_OR);

//очистим дополнительный регион 
DeleteObject(hHdrRgn);
трансформации регионов (MFC)

Используемая в листинге функция GetRegionData(HRGN hRgn, DWORD dwCount, LPRGNDATA lpRgnData) заполняет буфер lpRgnData данными о регионе hRgn. Параметр dwCount передает количество байт необходимых для заполнения. Это количество можно узнать если вызвать эту функцию с параметром dwCount равным нулю. В этой статье я не углубляюсь в содержимое структуры RGNDATA, к тому же, это требуется в очень редких случаях.

Считается, что использование структуры XFORM сопряжено с координатными преобразованиями, которые работаю только на платформе NT 3.1 и выше. При использовании регионов для создания окон координатные преобразования практически никакого отношения не имеют. Исключение составляет функция CombineTransform которая осуществляет объединение двух трансформаций и действительно работает только на NT 3.1 и выше. Для того, что бы данный код исполнялся на 9x платформе можно обоитись применением двух последовательных трансформаций без объединеня их в одной.

СОВЕТ

Если используется структура XFORM, то никакие макросы и отладчики не заменят лист бумаги и карандаш.

Кроме того, необходимо помнить о немного нетрадиционной системе координат для окна (центр – левый верхний угол, направление вертикальной оси - вниз). Однако это можно изменить, для чего в Win API существует целый класс функций для координатных преобразований.

В результате выполнения этой части программы на экране появится окно следующего вида:


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


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


Сайт управляется системой uCoz