Диалог по управлению светом
В окне редактора диалогов (Resource View > Dialog > Контекстное меню > Insert Dialog) создайте окно диалога по управлению светом, которое должно иметь такой вид:
Рис. 7.4. Вид окна диалога по управлению параметрами света
Обратите внимание на то, что справа от каждого движка расположен элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме. Три регулятора (элемента типа Slider Control) в левом верхнем углу окна диалога предназначены для управления свойствами света. Группа регуляторов справа от них поможет пользователю изменить координаты источника света. Группа регуляторов, объединенная рамкой (типа Group Box) с заголовком Material, служит для изменения отражающих свойств материала. Кнопка с надписью Data File позволит пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Для диалогов, предназначенных для работы в немодальном режиме, необходимо установить стиль Visible. Сделайте это в окне Properties > Behavior. Идентификаторы элементов управления мы сведем в табл. 7.1.
Таблица 7.1. Идентификаторы элементов управления
Диалоговый класс
Для управления диалогом следует создать новый класс. Для этого можно воспользоваться контекстным меню, вызванным над формой диалога.
Выберите в контекстном меню команду Add Class.
В левом окне диалога Add Class раскройте дерево Visual C++, сделайте выбор MFC > MFC Class и нажмите кнопку Open.
В окне мастера MFC Class Wizard задайте имя класса CPropDlg, в качестве базового класса выберите CDialog. При этом станет доступным ноле Dialog ID.
В это поле введите или выберите из выпадающего списка идентификатор шаблона диалога IDD_PROP и нажмите кнопку Finish.
Просмотрите объявление класса CPropDlg, которое должно появиться в новом окне PropDlg.h. Как видите, мастер сделал заготовку функции DoDataExchange для обмена данными с элементами управления на форме диалога. Однако она нам не понадобится, так как обмен данными будет производиться в другом стиле, характерном для приложений не MFC-происхождения.
Такое решение выбрано в связи с тем, что мы собираемся перенести рассматриваемый код в приложение, созданное на основе библиотеки шаблонов ATL. Это будет сделано в уроке 9 при разработке элемента ActiveX, а сейчас введите в диалоговый класс новые данные. Они необходимы для эффективной работы с диалогом в немодальном режиме. Важным моментом в таких случаях является использование указателя на оконный класс. С его помощью легко управлять окном прямо из диалога. Мы слегка изменили конструктор и ввели вспомогательный метод GetsiiderNum. Изменения косметического характера вы обнаружите сами:
#pragma once
class COGView; // Упреждающее объявление
class CPropDlg : public CDialog
{
DECLARE_DYNAMIC(CPropDlg)
public:
COGView *m_pView; // Адрес представления
int m_Pos[ll]; // Массив позиций ползунков
CPropDlg(COGView* p) ;
virtual ~CPropDlg();
// Метод для выяснения ID активного ползунка int GetsiiderNum(HWND hwnd, UINT& nID) ;
enum { IDD = IDD_PROP };
protected: virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
};
Откройте файл реализации диалогового класса и с учетом сказанного про адрес окна введите изменение в тело конструктора, который должен приобрести такой вид:
CPropDlg::CPropDlg(COGView* p)
: CDialog(CPropDlg::IDD, p)
{
//====== Запоминаем адрес объекта
m_pView = p;
}
Инициализация диалога
При каждом открытии диалога все его элементы управления должны отражать текущие состояния регулировок (положения движков), которые хранятся в классе представления. Обычно эти установки производят в коде функции OninitDialog. Введите в класс CPropDlg стартовую заготовку этой функции (CPropDlg > Properties > Overrides > OninitDialog > Add) и наполните ее кодами, как показано ниже:
BOOL CPropDlg: rOnlnitDialog (void)
{ CDialog: :OnInitDialog () ;
//====== Заполняем массив текущих параметров света
m_pView->GetLightParams (m _Pos) ;
//====== Массив идентификаторов ползунков
UINT IDs[] =
{
IDC_XPOS, IDC_YPOS, IDC_ZPOS,
IDC_AMBIENT,
IDC_DIFFUSE,
IDC_SPECULAR,
IDC_AMBMAT,
IDC_DIFFMAT,
IDC_SPECMAT,
IDC_SHINE,
IDCEMISSION
//====== Цикл прохода по всем регуляторам
for (int i=0; Ksizeof (IDs) /sizeof (IDs [ 0] ) ; i++)
{
//=== Добываем Windows-описатель окна ползунка H
WND hwnd = GetDlgItem(IDs[i] } ->GetSafeHwnd () ;
UINT nID;
//====== Определяем его идентификатор
int num = GetSliderNum(hwnd, nID) ;
// Требуем установить ползунок в положение m_Pos[i]
: :SendMessage(hwnd, TBM_SETPOS, TRUE, (LPARAM) m_Pos [i] )
char s [ 8 ] ;
//====== Готовим текстовый аналог текущей позиции
sprintf (s, "%d" ,m_Pos [ i] ) ;
//====== Помещаем текст в окно справа от ползунка
SetDlgltemText (nID, (LPCTSTR) s) ;
}
return TRUE;
}
Вспомогательная функция GetsliderNum по переданному ей описателю окна (hwnd ползунка) определяет идентификатор связанного с ним информационного окна (типа Static text) и возвращает индекс соответствующей ползунку пози ции в массиве регуляторов:
int CPropDlg: :GetSliderNum (HWND hwnd, UINT& nID)
{
//==== GetDlgCtrllD по известному hwnd определяет
//==== и возвращает идентификатор элемента управления
switch ( : : GetDlgCtrllD (hwnd) )
{
// ====== Выясняем идентификатор окна справа
case IDC_XPOS:
nID = IDC_XPOS_TEXT;
return 0;
case IDC_YPOS:
nID = IDC_YPOS_TEXT;
return 1;
case IDC_ZPOS:
nID = IDC_ZPOS_TEXT;
return 2;
case IDC_AMBIENT:
nID = IDC_AMB_TEXT;
return 3;
case IDC_DIFFUSE:
nID = IDC_DIFFUSE_TEXT;
return 4 ;
case IDC_SPECULAR:
nID = IDC_SPECULAR_TEXT;
return 5; case IDC_AMBMAT:
nID = IDC_AMBMAT_TEXT;
return 6 ;
case IDC_DIFFMAT:
nID = IDC_DIFFMAT_TEXT;
return 7 ;
case IDC_SPECMAT:
nID = IDC_SPECMAT_TEXT;
return 8 ; case IDC_SHINE:
nID = IDC_SHINE_TEXT;
return 9;
case IDC_EMISSION:
nID = IDC_EMISSION_TEXT;
return 10;
}
return 0;
}
Работа с группой регуляторов
В диалоговый класс введите обработчики сообщений WM_HSCROLL и WM_CLOSE, a также реакцию на нажатие кнопки IDC_FILENAME.
Воспользуйтесь для этого окном Properties и его кнопками Messages и Events. В обработчик OnHScroll введите логику определения ползунка и управления им с помощью мыши и клавиш. Подобный код мы подробно рассматривали в уроке 4. Прочтите объяснения вновь, если это необходимо, Вместе с сообщением WM_HSCROLL система прислала нам адрес объекта класса GScrollBar, связанного с активным ползунком. Мы добываем Windows-описатель его окна (hwnd) и передаем его в функцию GetsliderNum, которая возвращает целочисленный индекс. Последний используется для доступа к массиву позиций ползунков. Кроме этого, система передает nSBCode, который соответствует сообщению об одном из множества событий, которые могут произойти с ползунком (например, управление клавишей левой стрелки — SB_LINELEFT). В зависимости от события мы выбираем для ползунка новую позицию:
void CPropDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Windows-описатель окна активного ползунка
HWND hwnd = pScrollBar->GetSafeHwnd();
UINT nID;
//=== Определяем индекс в массиве позиций ползунков
int num = GetSliderNum(hwnd, nID) ;
int delta, newPos;
//====== Анализируем код события
switch (nSBCode)
{
case SBJTHUMBTRACK:
case SB_THUMBPOSITION: // Управление мышью
m_Pos[num] = nPos;
break; case SB_LEFT: // Клавиша Home
delta = -100;
goto New_Pos; case SB_RIGHT: // Клавиша End
delta = + 100;
goto New__Pos; case SB_LINELEFT: // Клавиша <-
delta = -1;
goto New_Pos; case SB_LINERIGHT: // Клавиша ->
delta = +1;
goto New_Pos; case SB_PAGELEFT: // Клавиша PgUp
delta = -20;
goto New_Pos; case SB_PAGERIGHT: // Клавиша PgDn
delta = +20-;
goto New_Pos;
New_Pos: // Общая ветвь
//====== Устанавливаем новое значение регулятора
newPos = m_Pos[num] + delta;
//====== Ограничения
m_Pos[num] = newPos<0 ? 0 :
newPos>100 ? 100 : newPos;
break; case SB ENDSCROLL:
default:
return;
}
//====== Синхронизируем текстовый аналог позиции
char s [ 8 ] ;
sprintf (s, "%d",m__Pos [num] ) ;
SetDlgltemText (nID, (LPCTSTR)s);
//---- Передаем изменение в класс COGView
m_pView->SetLightParam (num, m_Pos [num] ) ;
}
Особенности немодального режима
Рассматриваемый диалог используется в качестве панели управления освещением сцены, поэтому он должен работать в немодальном режиме. Особенностью такого режима, как вы знаете, является то, что при закрытии диалога он сам должен позаботиться об освобождении памяти, выделенной под объект собственного класса. Эту задачу можно решить разными способами. Здесь мы покажем, как это делается в функции обработки сообщения WM_CLOSE. До того как уничтожено Windows-окно диалога, мы обнуляем указатель m_pDlg, который должен храниться в классе COGView и содержать адрес объекта диалогового класса. Затем вызываем родительскую версию функции OnClose, которая уничтожает Windows-окно. Только после этого мы можем освободить память, занимаемую объектом своего класса:
void CPropDlg: :OnClose (void)
{
//=== Обнуляем указатель на объект своего класса
m_pView->m_pDlg = 0;
//====== Уничтожаем окно
CDialog: :OnClose () ;
//====== Освобождаем память
delete this;
}
Реакция на нажатие кнопки IDC_FILENAME совсем проста, так как основную работу выполняет класс COGView. Мы лишь вызываем функцию, которая реализована в этом классе:
void CPropDlg:: OnClickedFilename (void)
{
//=== Открываем файловый диалог и читаем данные
m_pView->ReadData ( ) ;
}
Создание немодального диалога должно происходить в ответ на выбор команды меню Edit > Properties. Обычно объект диалогового класса, используемого в немодальном режиме, создается динамически. При этом предполагается, что класс родительского окна хранит указатель m_pDlg на объект класса диалога. Значение указателя обычно используется не только для управления им, но и как признак его наличия в данный момент. Это позволяет правильно обработать ситуацию, когда диалог уже существует и вновь приходит команда о его открытии.
Введите в класс COGView новую public-переменную:
CPropDlg *m_pDlg; // Указатель на объект диалога
В начало файла заголовков OGView.h вставьте упреждающее объявление класса
CPropDlg:
class CPropDlg; // Упреждающее объявление
В конструктор COGView вставьте обнуление указателя:
m_pDlg =0; // Диалог отсутствует
Для обеспечения видимости класса CPropDlg дополните список директив препроцессора файла OGView.cpp директивой:
linclude "PropDlg.h"
Теперь можно ввести коды функции, которая создает диалог и запускает его вызовом функции Create (в отличие от DoModal для модального режима). Если происходит попытка повторного открытия диалога, то возможны два варианта развития событий:
новый диалог не создается, но окно существующего диалога делается активным;
команда открытия диалога недоступна, так как ее состояние зависит от значения указателя m_pDlg.
Реализуем первый вариант:
void COGView::OnEditProperties (void)
{
//====== Если диалог еще не открыт
if (!m_pDlg)
{
//=== Создаем его и запускаем в немодальном режиме
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP);
}
else
// Иначе, переводим фокус в окно диалога
m_pDlg->SetActiveWindow();
}
Реакция на команду обновления пользовательского интерфейса при этом может быть такой:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->SetCheck (m_pDlg != 0);
}
Второй вариант потребует меньше усилий:
void COGView::OnEditProperties (void)
{
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP); }
Но при этом необходима другая реакция на команду обновления интерфейса:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_pDlg == 0);
}
Выберите и реализуйте один из вариантов.
Панель управления
Завершая разработку приложения, вставьте в панель управления четыре кнопки
Для команд ID_EDIT_BACKGROUND, ID_EDIT_PROPERTIES, ID_VIEW_FILL И ID_VIEW_
QUAD. Заодно уберите из нее неиспользуемые нами кнопки с идентификаторами
ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE, ID_FILE_PRINT, ID__EDIT_CUT,
ID_EDIT_COPY, ID_EDIT_PASTE. Запустите приложение, включите диалог Edit > Properties и попробуйте управлять регуляторами параметров света. Отметьте, что далеко не все из них отчетливым образом изменяют облик поверхности. Нажмите кнопку Data File, при этом должен открыться файловый диалог, но мы не сможем открыть никакого другого файла, кроме того, что был создан по умолчанию. Он имеет имя «Sin.dat» и должен находиться (и быть виден) в папке проекта. В качестве упражнения создайте какой-либо другой файл с данными, отражающими какую-либо поверхность в трехмерном пространстве. Вы можете воспользоваться для этой цели функцией DefaultGraphic, немного модифицировав ее код. На рис. 7.5 и 7.6 приведены поверхности, полученные таким способом. Вы можете видеть эффект, вносимый различными настройками параметров освещения.
Если вы тщательно протестируете поведение приложения, то обнаружите недостатки. Отметим один из них. Закрытые части изображения при некотором ракурсе просвечивают сквозь те части поверхности, которые находятся ближе к наблюдателю. Причину этого дефекта было достаточно трудно выявить. И здесь опять пришли на помощь молодые, талантливые слушатели Microsoft Authorized Educational Center (www.Avalon.ru) Кондрашов С. С. (scondor@rambler.ru) и Фролов Д. С. (dmfrolov@rambler.ru). Оказалось, что при задании типа проекции с помощью команды gluPerspective значения ближней границы фрустума не должны быть слишком маленькими:
gluPerspective (45., dAspect, 0.01, 10000.);
В нашем случае этот параметр равен 0.01. Замените его на 10. и сравните качество генерируемой поверхности.
Подведем итог. В этой главе мы:
научились превращать окно, поддерживаемое классом cview, в окно OpenGL;
вновь использовали стандартный контейнер объектов класса GPoint3D, который удобен для хранения вершин изображаемой поверхности;
Рис. 7.5. Вид поверхности, освещенной слева
Рис. 7.6. Вид той же поверхности, но освещенной справа
убедились, что использование списка команд OpenGL повышает эффективность передачи сложного изображения;
применили формулу вычисления нормали к поверхности и убедились в необходимости уделять этой проблеме достаточное внимание;
научились управлять освещенностью сцены OpenGL с помощью группы регуляторов;
оценили удобство управления группой регуляторов типа slider Control в функции обработки сообщения о прокрутке WM_HSCROLL.
Содержание раздела