Статья
Версия для печати
Обсудить на форуме (6)
Создание собственного графического элемента управления с использованием библиотеки MFC.
Часть 1.


Автор: Алексей1153.
Дата написания: 21.11.2009.
Права на статью принадлежат автору и Клубу программистов «Весельчак У».

Содержание.


Предисловие.

Иногда в практике программиста возникает ситуация, когда стандартный набор элементов управления (в статье будет использоваться сокращение ЭУ) старой доброй Visual Studio 6...10 не содержит того ЭУ, который сейчас позарез понадобился. Где-то в Интернете, возможно, лежит этот элемент, но его почему-то никак не удаётся найти, а всё, что нашлось, не то, не то, не то...  Например, нам срочно понадобилось сделать ЭУ, который <придумаем позже, что он делает, наберитесь терпения>.
В этой статье я подробно опишу, как создать свой графический ЭУ. Все действия, описанные в статье, производятся в среде MS Visual Studio 2008 (Visual Studio 9.0), но версия студии — это не так уж важно (код класса будет протестирован и в VS6), важнее то, что работа выполняется при помощи библиотеки MFC, интерфейс которой практически не меняется. Да, кстати, у меня "русифицированная" версия студии, поэтому все названия пунктов меню оттуда — на русском. Непривычно, конечно, но как они в английской версии называются, я все точно не помню, поэтому буду писать на русском. Кроме того, некоторые слова явно коряво переведены, но для достоверности буду писать так, как они есть в студии.
В статье собираюсь разобрать следующие вопросы:
  • создание класс ЭУ, от чего можно наследовать,
  • размещение на форме (диалоге, view) при помощи визарда или "вручную",
  • "прозрачность" (не путать с полупрозрачностью),
  • отрисовка контента с учётом текущего размера ЭУ,
  • рассчёт координат элементов графики и интерактивность,
  • работа с мышью,
  • определение выхода курсора мыши за край ЭУ и захода обратно,
  • захват мыши,
  • борьба с мерцанием при отрисовке,
  • обрезание графики в размер определённого прямоугольника,
  • дочерние окна элемента управления, их размещение и обработка сообщений от них.

Создание тестового проекта.

Проект и весь код создаётся прямо по ходу написания статьи, потом лишь будет правка текста и отладка кода — по мелочам. Поэтому иногда куски кода в статье содержат то, что будет объяснено позднее — обычно я об этом буду предупреждать. Содержимое функций, показанное в примерах в статье, также будет меняться, эволюционируя.
Вот прямо сейчас, пишу эти строки и создаю новый проект Visual C++,  тип проекта "Приложение MFC", имя проекта "MyGraphControl". Галочку "создать каталог для решения" не ставлю. Выбираю "на базе диалоговых окон". Дальше всё по умолчанию. Затем делаю чистку кода от визардовского мусора, это не описываю. Добавляю защиту диалога от закрытия клавишами Esc и Enter:

Код:
void CMyGraphControlDlg::OnBnClickedOk()
{
//OnOK();
}

void CMyGraphControlDlg::OnBnClickedCancel()
{
//OnCancel();
}

// а это, чтобы диалог можно было закрыть "крестиком"
void CMyGraphControlDlg::OnClose()
{
CDialog::OnCancel();
}

Итак, имеем чистый пока ещё проект, при компиляции и запуске которого видим лишь пустой диалог — будущий аэродром для нашего ЭУ.

Файлы класса CMyControl.

Наш класс CMyControl, как мы его назовём, будет находиться по традиции C++ в двух файлах *.h и *cpp. Создадим в папке проекта два пустых файла и назовём CMyControl.h и CMyControl.cpp. Добавим в дерево проекта: удобно создать в дереве так называемый фильтр-папку с именем CMyControl, а в него уже добавить эти два файла. Щёлкаем по имени проекта в дереве правой кнопкой мыши, "добавить→новый фильтр", задаём имя CMyControl. Затем добавляем файлы — щёлкаем по фильтру правой кнопкой мыши, "добавить→существующий элемент", в строке ввода имени файла пишем "CMyControl*" , нажимаем Enter. Покажутся только наши два файла, их выделяем рамкой и добавляем в проект.  Эти два файла и есть наш компонент — элемент управления. Пока что он ничего не умеет. Его пока что вообще нет.
Наполним файлы стандартным для MFC минимальным содержимым:

Файл CMyControl.h
Код:
#pragma once

class CMyControl
{
};

Файл CMyControl.cpp
Код:
#include "stdafx.h"
#include "CMyControl.h"

// место для  других include


#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// место для кода реализации

Родителей не выбирают.

В C++ ещё как выбирают! Итак, мы добрались до вопроса, как создать класс нашего ЭУ. Создавать класс ЭУ с чистого листа — довольно хлопотное занятие, если учесть, что это будет, ни много ни мало, окно Windows. Сам собой напрашивается вопрос произвести класс от класса окна библиотеки MFC CWnd (для экстремалов — можно просто инкапсулировать хендл типа HWND и делать всё вручную). Однако, чтобы создать объект окна, необходимо будет вызвать метод CWnd::Create или CWnd::CreateEx, где нужно детально указать параметры создаваемого объекта. Это не всегда удобно и отвлекает от полёта мысли (ведь это нужно делать каждый раз, то есть рутина). Лично мне нравится производить графические ЭУ от класса CStatic (который, надо заметить, уже и так произведён от CWnd и является окном Windows). Это даёт возможность разместить прямоугольник ЭУ на ресурсе диалога при помощи встроенного в студию редактора ресурсов — просто напросто размещаем элемент Static и связываем его с переменной нашего класса, определённой в классе диалога. Но оставить способ создавать ЭУ динамически тоже никто не мешает. В общем, выбирать в будущем вам, я же останавливаюсь на родителе CStatic. Так и запишем:

Файл CMyControl.h
Код:
#pragma once

class CMyControl:public CStatic
{
DECLARE_DYNAMIC(CMyControl)
};

Строчка DECLARE_DYNAMIC(CMyControl) необязательна — это для любителей получения информации о классе во время выполнения (рантайме).
Сохраняем проект и закрываем студию. В папке проекта удаляем файл *.ncb. Затем снова открываем проект. Так мы познакомили визард с нашим новым классом, и теперь можно добавлять при его, визарда, помощи обработчики сообщений окна. Например, добавим обработчик WM_PAINT (потом всё равно пригодится): находим в дереве классов наш класс CMyControl, щёлкаем правой кнопкой мыши→"свойства". Вверху окна свойств нажимаем кнопочку "сообщения", находим в таблице сообщение WM_PAINT и в правой колонке выбираем пункт <Добавить OnPaint>. Визард добавит отсутствовавшие до этого строчки DECLARE_MESSAGE_MAP, BEGIN_MESSAGE_MAP и END_MESSAGE_MAP, а также сам обработчик OnPaint(), в котором пока ничего не делается, кроме как создаётся новый контекст устройства для рисования. Одна единственная строчка

Код:
	CPaintDC dc(this);

— создаётся новый чистый контекст устройства для рисования.

Размещение на диалоге при помощи визарда.

На диалог главного окна в редакторе ресурсов кладём ЭУ Static и задаём ему размеры, растягивая мышью. Задаём также идентификатор, скажем, IDC_MY1. Создаём член-переменную нашего класса, связанную с только что созданным элементом Static: нажимаем клавишу Ctrl и дважды щёлкаем левой кнопкой мыши по статику. Указываем тип переменной — CMyControl (придётся вписать вручную, привет разработчикам. Всё равно в 6-й студии этот момент был удобнее). Задаём имя переменной: m_IDC_MY1  (это у меня такая полезная привычка — давать имя просто приписыванием "m_" к идентификатору — это потом во многих случаях помогает, да и просто не нужно мучиться с придумыванием имени :) ). Нажимаем "Готово". При этом в класс главного диалога добавится: в файл MyGraphControlDlg.h:

Код:
	#include "cmycontrol.h"
...
CMyControl m_IDC_MY1;

в файл MyGraphControlDlg.cpp , в обработчике DoDataExchange:

Код:
	DDX_Control(pDX, IDC_MY1, m_IDC_MY1);

Негусто добавилось, надо заметить.

Кто нарисовал "мяу".

Напишем в статике в редакторе ресурсов какой-либо текст ("мяу" ;) ), зададим рамку (например, "статическая граница"). Запустим программу. И обнаружим, что рамка видна, а вот текст — нет! Дело в том, что наш класс уже работает: в добавленной визардом функции есть всего одна строчка

Код:
CPaintDC dc(this);

— она создаёт новый чистый контекст устройства для рисования. А поскольку мы ничего не рисуем, виден сам диалог. А на диалоге под нашим ЭУ сейчас нарисован только серый фон — мы этот фон и видим. Поместите какой-нибудь другой элемент управления под наш — он будет виден (чтобы другой элемент был под нашим, нажмите Ctrl+D в редакторе ресурсов и щёлкните сначала по тому, другому, элементу, а потом по нашему — так задаётся порядок обхода).
Если строку же строку

Код:
CPaintDC dc(this)

заменить строкой с вызовом родительского обработчика,

Код:
CStatic::OnPaint();

то текст станет виден, так как CStatic сам его и нарисует, как обычно он это и делает. В родном обработчике CStatic тоже создаётся новый контекст устройства + рисуется на нём своя, статиковая, графика (обычно это текст).
Почему видна рамка? Рамка — это неклиентская область окна, она отрисовывается по сообщению WM_NCPAINT ; обработчик OnNcPaint(). Поскольку мы обработчик не переопределяли, CStatic сам нарисовал рамку в своём обработчике. В этом всём легко убедится, если добавить обработчик сообщения WM_NCPAINT и оставить этот обработчик пустым (тут контекст создавать не нужно — он создался в OnPaint() ).
Неклиентскую область мы трогать не будем. Тогда рамка будет автоматически отображаться, а мы лишь будем задавать её в редакторе (или программно — при помощи установки свойств окна).
Нас интересует лишь клиентская часть окна, мы её будем рисовать в OnPaint().

Каким цветом закрасить фон ЭУ.

Теперь нам нужно "нарисовать" фон нашего ЭУ. Когда мы находимся внутри нашего класса, верхний левый угол клиентской области имеет координаты (0;0). То есть, рисуя, не задумываемся об окружении нашего ЭУ, здесь координаты относительные (экранные же координаты — абсолютные). Получить размеры клиентской области можно при помощи функции CWnd::GetClientRect()

Код:
void CMyControl::OnPaint()
{
// CStatic::OnPaint();
CPaintDC dc(this);

// прямоугольник клиентской области
CRect r_CL;
GetClientRect(&r_CL);

// определение высоты и ширины клиентской области
int Hig=r_CL.Height();
int Wid=r_CL.Width();
}

Выходить при рисовании за границы x=0...(Wid-1), y=0...(Hig-1) нельзя, так как контекст работает для всего родительского окна. В этом легко убедиться, добавив строчки

Код:
void CMyControl::OnPaint()
{
...
...
dc.FillSolidRect(-1000,-10,Wid+2000,Hig+20,RGB(0,255,0));//зелёный
dc.FillSolidRect(-20,-20,Wid+40,Hig+40,RGB(0,0,0));//чёрный
dc.FillSolidRect(0,0,Wid,Hig,RGB(255,0,0));//красный
}

Сначала нарисовался зелёный прямоугольник. Он явно шире диалога, поэтому доходит до его краёв, а всё, что дальше, обрезается диалогом. Потом рисуется чёрный прямоугольник — он во все стороны больше клиентской области на 20 пикселов. Красный прямоугольник точно заполняет клиентскую область.


рисунок 1

Стираем эти 3 строчки кода. И, собственно, теперь встаёт очевидный вопрос — каким цветом закрасить фон нашего ЭУ. А это сделать необходимо, так как сейчас наш ЭУ неприлично прозрачный! Хотя, если есть такая задача — нарисовать что-то поверх остального, то это самое то. Но нам-то надо рисовать всё своё. Можете выбрать цвет свой, по вкусу — макрос RGB(<к>,<з>,<с>) позволяет определить произвольный цвет по его составляющим компонентам — красному, зелёному и синему цветам. Если же не хочется, чтобы ЭУ выбивался из текущей цветовой схемы Windows (а их море), то нужно применить системные цвета, их можно получить функцией GetSysColor. В большинстве случаев нужны всего 4 цвета — "обычный серый", "тень", "глубокая тень"  и "свет". К примеру, прикинемся кнопкой (не забудьте убрать рамку статика — помешает восприятию :) ):

Код:
void CMyControl::OnPaint()
{
CPaintDC dc(this);

CRect r_CL;
GetClientRect(&r_CL);

int Hig=r_CL.Height();
int Wid=r_CL.Width();

// демонстрация системных цветов
COLORREF colorGrey=::GetSysColor(COLOR_BTNFACE);
COLORREF colorLihg=::GetSysColor(COLOR_BTNHIGHLIGHT);
COLORREF colorShad=::GetSysColor(COLOR_BTNSHADOW);
COLORREF colorDeepShad=::GetSysColor(COLOR_3DDKSHADOW);

int f=1; // толщина линии
dc.FillSolidRect(0,0,Wid,Hig,colorDeepShad);
dc.FillSolidRect(0,0,Wid-f,Hig-f,colorLihg);
dc.FillSolidRect(f,f,Wid-f*2,Hig-f*2,colorShad);
dc.FillSolidRect(f,f,Wid-f*3,Hig-f*3,colorGrey);
}


рисунок 2

Пока сделаем фон нашего ЭУ серым системным цветом. Ну а потом, может, и передумаем )).

Лирическое отступление: на этом месте студия 9 дала сбой: просто рушилась при попытке открыть дерево ресурсов нажатием на плюсик. Вылечилось удалением файла *.ncb и *.aps из папки проекта.

Кстати, способ рисования границ при помощи прямоугольников, применённый здесь, неэкономичен, с точки зрения производительности. Специально провёл эксперимент (код показан чуть ниже): линиями рамка рисуется раза в три с небольшим быстрее. Однако учитывая, что так рисуется всего одна-две рамки, то для красоты кода можно этот метод применить. Если же рисуется много объектов, то лучше линиями. В любом случае, всегда рекомендую делать подобные тесты и запоминать типовые ситуации — чтобы в похожих случаях и без тестов знать, что будет быстрее работать.
Тестовый код рисует одинаковую рамку в обоих случаях, а фон не закрашивается — тестируем только скорость отрисовки рамки. Время отрисовки пишется в заголовке главного окна.

Код:
void CMyControl::OnPaint()
{
CPaintDC dc(this);
CRect r_CL;
GetClientRect(&r_CL);
int Hig=r_CL.Height();
int Wid=r_CL.Width();
DWORD dwd1=::GetTickCount();
COLORREF colorGrey=::GetSysColor(COLOR_BTNFACE);
COLORREF colorLihg=::GetSysColor(COLOR_BTNHIGHLIGHT);
COLORREF colorShad=::GetSysColor(COLOR_BTNSHADOW);
COLORREF colorDeepShad=::GetSysColor(COLOR_3DDKSHADOW);

for(int i=0; i<50000; i++)
{
int f=1; // толщина линии
#if(0) // переключатель теста — 0 или 1
{
dc.FillSolidRect(0,0,Wid,Hig,colorDeepShad);
dc.FillSolidRect(0,0,Wid-f,Hig-f,colorLihg);
dc.FillSolidRect(f,f,Wid-f*2,Hig-f*2,colorShad);
}
#else
{
CPen* olpPen=0;

CPen m_LihgPen(PS_SOLID,f,colorLihg);
CPen m_ShadPen(PS_SOLID,f,colorShad);
CPen m_DeepPen(PS_SOLID,f,colorDeepShad);

// сохранение старого пера
olpPen=dc.SelectObject(&m_DeepPen);

dc.MoveTo(Wid-1,0);
dc.LineTo(Wid-1,Hig-1);
dc.LineTo(0,Hig-1);

dc.SelectObject(&m_ShadPen);
dc.MoveTo(Wid-2,1);
dc.LineTo(Wid-2,Hig-2);
dc.LineTo(1,Hig-2);

dc.SelectObject(&m_LihgPen);
dc.MoveTo(Wid-2,0);
dc.LineTo(0,0);
dc.LineTo(0,Hig-1);

// возврат старого пера
dc.SelectObject(olpPen);
}
#endif
}
DWORD dwd2=::GetTickCount();
DWORD dwdDiff=dwd2-dwd1;
CString txt;
txt.Format("отрисовано примерно за %u мс",dwdDiff);
GetParent()->SetWindowText(txt);
}

Изменяемый размер ЭУ.

Во время отрисовки нашего ЭУ постоянно будем учитывать, что его размеры могут быть произвольными. Для контроля этого момента сделаем следующее:
1. у главного диалога добавляем изменяемую границу (свойства→граница, "изменение размера").
2. в классе главного диалога добавляем функцию перерасчёта размещения нашего контрола.

Код:
void CMyGraphControlDlg::ResizeControl()
{
// подгонка контрола к размеру диалога
CRect mainRect; // прямоугольник диалога
GetClientRect(&mainRect); // относительные координаты на диалоге

int n=3; // отступ от краёв
CRect contRect; // прямоугольник элемента
contRect.left=mainRect.left+n;
contRect.top=mainRect.top+n;
contRect.right=mainRect.right-n;
contRect.bottom=mainRect.bottom-n;

// если края "захлестнулись" — корректируем
if(contRect.left>contRect.right)contRect.left=contRect.right;
if(contRect.top>contRect.bottom)contRect.top=contRect.bottom;

// боремся за производительность: перед переразмещением контрола убедимся,
// что положение на самом деле поменяется. В данном случае это малополезно,
// поскольку элемент один, да и размер его меняется в соответствии с диалогом.
// Но для тренировки всё-таки это сделаем:)
CRect currRect; // текущий прямоугольник контрола в абсолютных координатах
CRect newRect; // устанавливаемый прямоугольник в абсолютных координатах
{
// получаем абсолютные координаты контрола
m_IDC_MY1.GetWindowRect(&currRect);

// проецируем contRect на абсолютные координаты
// класс CRect — эквивалентен массиву CPoint[2]
newRect=contRect;
::MapWindowPoints(m_hWnd,0,(CPoint*)&newRect,2);
}

if(currRect!=newRect)
{
// задаём новый размер и положение
m_IDC_MY1.MoveWindow(&contRect,0);

// перерисовка всего диалога
Invalidate(1);
}
}

3. в обработчике сообщения WM_WINDOWPOSCHANGED (не путать с WM_WINDOWPOSCHANGING!) вызываем данную функцию

Код:
void CMyGraphControlDlg::OnWindowPosChanged(WINDOWPOS* lpwndpos)
{
CDialog::OnWindowPosChanged(lpwndpos);

ResizeControl();
}

Рисуем кнопки меню.

У нашего элемента управления имеются собственные рисованные кнопки меню. Поскольку мы не только рисуем эти кнопки, но и хотим заставить их отзываться на команды курсора мыши, то расчёт всех нужных координат кнопок нужно поручить одной функции, которая будет использоваться и при рисовании, и при определении попадания курсора. Это и удобно, и точно.
Меню будет расположено вверху элемента. Высоту меню в процентах от текущей высоты ЭУ задаёт константа e_Menu_hig_percent. Однако ограничиваем высоту меню константой e_Menu_max_hig, ведь зачем нам меню на полэкрана? Ширина кнопок меню в процентах от высоты задаётся константой e_Menu_but_wid_percent. Константа e_Menu_but_horis_spasing задаёт расстояние по горизонтали между кнопками и от левого края нашего ЭУ. Положение кнопки задаётся индексом, начинающимся с 0. Создаём набор вспомогательных функций для получения координат и размеров.
Всю остальную область, расположенную под меню, я назвал "комнатой" (room).

Код:
	// определение высоты меню
void GetMenuH(const CRect& LayerRect,int& H)
{
H=min( LayerRect.Height()*e_Menu_hig_percent/100 , e_Menu_max_hig );
}
int GetMenuH(const CRect& LayerRect)
{
int H;
GetMenuH(LayerRect,H);
return H;
}

// определение прямоугольника меню
void GetMenuRect(const CRect& LayerRect,CRect& r_Menu)
{
r_Menu=LayerRect;
r_Menu.bottom=LayerRect.top+GetMenuH(LayerRect)-1;
}

// определение прямоугольника комнаты
void GetRoomRect(const CRect& LayerRect,CRect& r_Room)
{
r_Room=LayerRect;
r_Room.top+=GetMenuH(LayerRect);
}

// определение координат кнопки
// LayerRect(in) — прямоугольник окна
// butIndx_zb(in) — индекс кнопки, начинающийся с 0
// butRect (out) — прямоугольник кнопки на окне
void GetButtonRect(const CRect& LayerRect,int butIndx_zb,CRect& butRect)
{
int H; // высота кнопки
int W; // ширина кнопки

// определение ширины и высоты любой кнопки
GetMenuH(LayerRect,H);
H=H-e_Menu_but_vert_spasing*2;
W=H*e_Menu_but_wid_percent/100;

butRect.left=LayerRect.left+
e_Menu_but_horis_spasing+
(W+e_Menu_but_horis_spasing-1)*butIndx_zb;

butRect.right=butRect.left+W-1;

butRect.top=LayerRect.top+e_Menu_but_vert_spasing;

butRect.bottom=butRect.top+H-1;
}

// определить индекс и, если надо, прямоугольник кнопки по заданной координате
bool GetButtonIndxAndRectFromPoint(
const CPoint& pnt, const CRect& LayerRect,
int* pbutIndx_zb=0,CRect* pbutRect=0)
{
CRect r_Menu;
GetMenuRect(LayerRect,r_Menu);
if(!r_Menu.PtInRect(pnt))return false;

// кнопок немного, поэтому ищем просто перебором
CRect but;

for(int i=0; i<e_but_buttons_count; i++)
{
GetButtonRect(LayerRect,i,but);
if(but.PtInRect(pnt))
{
if(pbutRect)*pbutRect=but;
if(pbutIndx_zb)*pbutIndx_zb=i;
return true;
}
}

return false;
}

Теперь наш обработчик OnPaint выглядит так:

Код:
void CMyControl::OnPaint()
{
CPaintDC dc(this);

// прямоугольник клиентской области
CRect r_CL;
GetClientRect(&r_CL);
// определение высоты и ширины клиентской области
int Hig=r_CL.Height();
int Wid=r_CL.Width();

// прямоугольник комнаты
CRect r_Room;
r_Room=r_CL;
r_Room.top+=GetMenuH(r_CL);
PaintRoom(dc,r_Room);

// прямоугольник меню
CRect r_Menu;
r_Menu=r_CL;
r_Menu.bottom=r_Room.top-1;
PaintMenu(dc,r_CL,r_Menu);
}

Мы для удобства разнесли различные области отрисовки в разные функции.
Меню отрисовывается в функции  PaintMenu(). Наши кнопки будут иметь 3 состояния:

Код:
	enum ee_button_state
{
e_but_calm, // кнопка в спокойном состоянии
e_but_push, // кнопка нажата
e_but_over, // курсор находится над ненажатой кнопкой

e_but_auto, // выбрать состояние в зависимости от текущих действий мышью
};

Нарисуем в функции PaintMenu() черту под меню, фон меню и 3 кнопки с индексами 0, 1 и 2 в различных состояниях (на блок if(state==e_but_auto){...} пока не обращайте внимания, он будет объяснён позже. Я даже заремлю его пока, чтобы не смущать):

Код:
void CMyControl::PaintMenu(CDC& dc,const CRect& ClRect,const CRect& MenuRect)
{
dc.FillSolidRect(&MenuRect,::GetSysColor(COLOR_3DFACE));
dc.FillSolidRect(MenuRect.left,MenuRect.bottom,MenuRect.Width(),1,::GetSysColor(COLOR_3DSHADOW));

PaintButton(dc,ClRect,0,e_but_calm);
PaintButton(dc,ClRect,1,e_but_push);
PaintButton(dc,ClRect,2,e_but_over);
}

void CMyControl::PaintButton(CDC& dc,const CRect& ClRect,int butIndx_zb,ee_button_state state)
{
CRect butRect;
GetButtonRect(ClRect,butIndx_zb,butRect);
LONG& L=butRect.left;
LONG& T=butRect.top;
LONG  W=butRect.Width();
LONG  H=butRect.Height();

/*
if(state==e_but_auto)
{
if(m_TC.IsMouseInRect(butRect))
{
// курсор сейчас над кнопкой
if(m_TC.IsMouseCaptured())
{
// был захват мыши, причём нашим окном
state=e_but_push;
}
else
{
state=e_but_over;
}
}
else
{
// курсор сейчас не над кнопкой
state=e_but_calm;
}
}
*/

switch(state)
{
default:
case e_but_calm:
{
dc.FillSolidRect(&butRect,::GetSysColor(COLOR_3DFACE));
}
break;

case e_but_push:
{
dc.FillSolidRect(L+0,T+0,W+0,H+0,::GetSysColor(COLOR_BTNHIGHLIGHT));
dc.FillSolidRect(L+0,T+0,W-1,H-1,::GetSysColor(COLOR_BTNSHADOW));
dc.FillSolidRect(L+1,T+1,W-2,H-2,::GetSysColor(COLOR_3DFACE));
}
break;

case e_but_over:
{
dc.FillSolidRect(L+0,T+0,W+0,H+0,::GetSysColor(COLOR_BTNHIGHLIGHT));
dc.FillSolidRect(L+1,T+1,W-1,H-1,::GetSysColor(COLOR_BTNSHADOW));
dc.FillSolidRect(L+1,T+1,W-2,H-2,::GetSysColor(COLOR_3DFACE));
}
break;
}
}


рисунок 3

Попробуйте также уменьшать размер диалога — кнопки тоже будут уменьшаться, когда их высота станет более, чем e_Menu_hig_percent процентов от высоты клиентской части ЭУ.
Как видите, код отрисовки кнопок тоже убран в свою функцию, её ещё будем дополнять для помещения картинки на кнопку. Да и с самим набором кнопок мы ещё не определились, это произойдёт чуть далее.

Сообщения от мыши и трекинг курсора.

Для того, чтобы наш ЭУ мог обрабатывать сообщения от мыши (щелчки, перемещение курсора), нужно сделать несколько важных вещей.

Получаем сообщения от мыши.

Самое первое, что делаем: боремся с наследственностью CStatic: для того, чтобы получать в наш ЭУ сообщения от мыши, необходимо установить свойство SS_NOTIFY. Это делается двумя способами:
  • можно вручную в редакторе ресурсов выставить свойство "уведомление" в true;
  • но предпочтительнее сделать это из кода. Обязуем пользователя нашего для начала работы ЭУ вызвать метод CreateMe() (где ещё сделаем и динамическое создание ЭУ). Если метод не вызывался, то переменная m_bIsCreated класса, которая в конструкторе установлена в false, блокирует отрисовку в OnPaint(), просто закрасив фон чёрным цветом. Тем самым мы визуально сообщим себе, что забыли вызвать метод CreateMe(). Когда можно вызывать CreateMe()?

  • если родительский диалог производен от CView, то в обработчике OnInitialUpdate() или в любое другое время позднее (например, в конструкторе ещё нельзя, ведь хендл окна там ещё не создан).
  • если родительский диалог производен от CDialog (как у нас), то в обработчике OnInitDialog() или в любое другое время позднее (в конструкторе также ещё нельзя).

Выглядит всё это так: В диалоге, в OnInitDialog (а если бы это был CView) — вызываем метод создания

Код:
BOOL CMyGraphControlDlg::OnInitDialog()
{
CDialog::OnInitDialog(); // здесь хендл диалога создаётся

m_IDC_MY1.CreateMe(this);

return TRUE;
}

, в классе нашего ЭУ

Код:
void CMyControl::OnPaint()
{
CPaintDC dc(this);

...
...

if(!m_bIsCreated)
{
// закрашиваем фон чёрным
dc.FillSolidRect(&r_CL,0);
return;
}
...
...
}

bool CMyControl::CreateMe(CWnd* pParent,UINT ID=0,CRect* pRect=0)
{
if(m_bIsCreated)return true;

if(::IsWindow(GetSafeHwnd()))
{
// хендл окна уже создан
m_bIsCreated=true;

// устанавливаем стиль
ModifyStyle(0,SS_NOTIFY,0);

DWORD dwdSt=GetStyle();
DWORD dwdStEx=GetExStyle();
int iii=1;
}
else
{
if(pParent && ::IsWindow(pParent->GetSafeHwnd()) && ID)
{
CRect r(0,0,10,10);
if(!pRect)pRect=&r;
// хендл окна ещё не создан
if(CStatic::Create("",WS_CHILD|WS_VISIBLE|SS_NOTIFY,*pRect,pParent,ID))
{
// если нужно рамку, добавить ещё WS_EX_STATICEDGE
ModifyStyleEx(0,WS_EX_NOPARENTNOTIFY,0);
m_bIsCreated=true;
}
}
}


return(m_hWnd!=0 && m_bIsCreated);
}

Теперь сообщения от мыши будут приходить. Когда по окну нашего ЭУ двигают курсором мыши, окно получает сообщение WM_MOUSEMOVE. Соответствующий обработчик — OnMouseMove() — мы используем для того, чтобы запускать трекинг курсора.
Метод CreateMe() позволяет создать ЭУ и без помощи визарда. Пример:

Код:
class CMyGraphControlDlg : public CDialog
{
CMyControl* m_pCONTROL;

CMyGraphControlDlg()
{
pCONTROL=0;
}

~CMyGraphControlDlg()
{
if(pCONTROL)delete pCONTROL;
}

BOOL CMyGraphControlDlg::OnInitDialog()
{
CDialog::OnInitDialog();

m_pCONTROL=new CMyControl;

CRect r =...;
m_pCONTROL->CreateMe(this,1000/*,&r*/);

return TRUE;
}
};

Запускаем трекинг.

Трекинг позволит нам точно знать, когда курсор двигается над клиентской частью окна ЭУ, когда покидает прямоугольник окна и когда заходит на область окна. Когда включен трекинг, то при входе курсора в прямоугольник клиентской части окна в окно приходит сообщение WM_MOUSEHOVER, а при выходе — WM_MOUSELEAVE (и неважно, находится ли сейчас фокус на окне или нет. Правда, если где-то включен захват мыши (Capture), то сообщения не будет — об этом позднее.). Причём, учитывается не только выход за пределы прямоугольника, но также и частичное перекрытие другим окном:


рисунок 4

Всё это довольно полезно, с точки зрения косметической красоты элемента управления и управления производительностью.
Трекинг будем включать в обработчике OnMouseMove(). Кроме обработчика OnMouseMove() нам ещё потребуются обработчики сообщений WM_MOUSEHOVER и WM_MOUSELEAVE (OnMouseHover() и OnMouseLeave() соответственно). Чтобы не размазывать код этого механизма по проекту, завернём всё во вспомогательный класс.

Код:
class CTrackingControl
{
bool m_bInsideWin; // курсор над окном
CWnd* m_pWin; // окучиваемое окно

public:
CTrackingControl(CWnd* pWin=0)
{
m_bInsideWin=false;
m_pWin=0;

if(pWin)
{
m_pWin=pWin;
}
}

void BeginTracking()
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return;

TRACKMOUSEEVENT t;
::memset(&t,0,sizeof(t));
t.cbSize=sizeof(t);
t.dwFlags=TME_HOVER|TME_LEAVE;
t.dwHoverTime=10; // мс
t.hwndTrack=m_pWin->m_hWnd;
::TrackMouseEvent(&t);
}

void StopTracking()
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return;

TRACKMOUSEEVENT t;
::memset(&t,0,sizeof(t));
t.cbSize=sizeof(t);
t.dwFlags=TME_CANCEL|TME_HOVER|TME_LEAVE;
t.hwndTrack=m_pWin->m_hWnd;
::TrackMouseEvent(&t);
}

void ProcessMessage(const MSG* pM) // GetCurrentMessage()
{
if(!pM)return;

switch(pM->message)
{
case WM_MOUSEMOVE:
{
if(!m_bInsideWin)
{
BeginTracking();
}
}
break;

case WM_MOUSEHOVER:
{
m_bInsideWin=true;
}
break;

case WM_MOUSELEAVE:
{
m_bInsideWin=false;
}
break;
}
}

bool GetCursorIsInsideWindow()
{
return m_bInsideWin;
}

// определение, захвачена ли мышь нашим окном сейчас
bool IsMouseCaptured()
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return false;
return ::GetCapture()==m_pWin->m_hWnd;
}

// определение, находится ли курсор в прямоугольнике,
// который задан в относительных координатах окна
bool IsMouseInRect(const CRect& r_relat)
{
if(!m_pWin || !::IsWindow(m_pWin->GetSafeHwnd()))return false;
CPoint pnt;
if(::GetCursorPos(&pnt))
{
::MapWindowPoints(0,m_pWin->m_hWnd,&pnt,1);
if(r_relat.PtInRect(pnt))
{
return true;
}
}

return false;
}
};
   
(Функция IsMouseInRect() даже уже была применена выше.) Объявим переменную этого класса в ЭУ:

Код:
class CMyControl:public CStatic
{
CTrackingControl m_TC;
...

Инициализируем:

Код:
CMyControl::CMyControl()
{
m_TC=CTrackingControl(this);
...
}

И добавим обработчики сообщений в ЭУ:

Код:
void CMyControl::OnMouseMove(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());

// тут будет ещё код
// ...
}

void CMyControl::OnMouseHover(UINT nFlags, CPoint point)
{
m_TC.ProcessMessage(GetCurrentMessage());
}

void CMyControl::OnMouseLeave()
{
m_TC.ProcessMessage(GetCurrentMessage());
// ...
}

Троеточие в OnMouseMove() — место для будущего кода обработки мышедвижения.

(Продолжение: часть 2.)
Версия для печати
Обсудить на форуме (6)