программирование, создание программ, учебник Delphi, учебник по программированию, бейсек, делфи, си, паскаль
 
 
 

 

График по умолчанию


Пришла пора создать тестовую поверхность у = f (x, z), которую мы будем демонстрировать по умолчанию, то есть до того, как пользователь обратился к файловому диалогу и выбрал файл с данными, которые он хочет отобразить в окне OpenGL Функция Def aultGraphic, коды которой вы должны вставить в файл ChildView,cpp, задает поверхность, описываемую уравнением:

Yi,j=[3*п*(i-Nz/2)/2*Nz]*SIN[3*п*(j-Nx/2)/2*Nx]
Здесь п. обозначает количество ячеек сетки вдоль оси Z, а пх — количество ячеек вдоль оси X. Индексы i (0 < i < пz) и j (0 < j < nx)выполняют роль дискретных значений координат (Z, X) и обозначают местоположение текущей ячейки при пробеге по всем ячейкам сетки в порядке, описанном выше. Остальные константы подобраны экспериментально так, чтобы видеть полтора периода изменения гармонической функции.
Мы собираемся работать с двоичным файлом и хранить в нем информацию в своем формате. Формат опишем словесно: сначала следуют два целых числа m_xsize и m_zSize (размеры сетки), затем последовательность значений функции у = f (х, z) в том же порядке, в котором они были созданы. Перед тем как записать данные в файл, мы поместим их в буфер, то есть временный массив buff, каждый элемент которого имеет тип BYTE, то есть unsigned char. В буфер попадают значения переменных разных типов, что немного усложняет кодирование, но зато упрощает процесс записи и чтения, который может быть выполнен одной командой, так как мы пишем и читаем сразу весь буфер. В процессе размещения данных в буфер используются указатели разных типов, а также преобразование их типов:
void COGView::DefaultGraphic()
{
//====== Размеры сетки узлов
m xSize = m zSize = 33;
//====Число ячеек на единицу меньше числа узлов
UINTnz = m_zSize - 1, nx = m_xSize - 1;
// Размер файла в байтах для хранения значений функции
DWORD nSize = m_xSize * m_zSize * sizeof (float) + 2*sizeof (UINT) ;
//====== Временный буфер для хранения данных
BYTE *buff = new BYTE[nSize+l] ;
//====== Показываем на него указателем целого типа
UINT *p = (UINT*)buff;
//====== Размещаем данные целого типа
*р++ = m_xSize;
*р++ = m_zSize;
//====== Меняем тип указателя, так как дальше
//====== собираемся записывать вещественные числа
float *pf = (float*)?;
//=== Предварительно вычисляем коэффициенты уравнения
double fi = atan(l.)*6,
kx = fi/nx,
kz = fi/nz;
//====== В двойном цикле пробега по сетке узлов
//=== вычисляем и помещаем в буфер данные типа float
for (UINT i=0; i<ra_zSize;
for (UINT j=0; j<m_xSize;
{
*pf++ = float (sin(kz* (i-nz/2.) ) * sin (kx* (j-nx/2. ) )
}
}
//=== Переменная для того, чтобы узнать сколько
//=== байт было реально записано в файл DWORD nBytes;
//=== Создание и открытие файла данных sin.dat
HANDLE hFile = CreateFile (_T ("sin .dat") , GENERIC_WRITE,
0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0)
//====== Запись в файл всего буфера
WriteFile (hFile, (LPCVOID) buff, nSize, SnBytes, 0) ;
//====== Закрываем файл
CloseHandle (hFile) ;
//====== Создание динамического массива m_cPoints
SetGraphPoints (buff, nSize) ;
//====== Освобождаем временный буфер
delete [] buff;
}
В процессе создания, открытия и записи в файл мы пользуемся API-функциями CreateFile, WriteFile и CloseHandle, которые предоставляют значительно больше возможностей управлять файловых хозяйством, чем, например, методы класса CFile или функции из библиотек stdio.h или iostream.h. Обратитесь к документации, для того чтобы получить представление о них.
  
Работа с контейнером
Для работы с файлом мы пользовались буфером переменных типа BYTE. Для работы с данными в памяти значительно более удобной структурой данных является динамический контейнер. Мы, как вы помните, выбрали для этой цели контейнер, скроенный по шаблону vector. При заказе на его изготовление указали тип данных для хранения в контейнере. Это объекты класса CPointSD (точки трехмерного пространства). Мы пошли по простому пути и храним в файле только один компонент Y из трех координат точек поверхности в 3D. Остальные две координаты (узлов сетки на плоскости X-Z) будем генерировать на регулярной основе. Такой подход оправдан тем, что изображение OpenGL все равно претерпевает нормирующие преобразования, перед тем как попасть на двухмерный экран. Создание контейнера точек производится в теле функции SetGraphPoints, к разработке которой сейчас и приступим.
На вход функции подается временный буфер (и его размер), в который попали данные из файла. В настоящий момент в буфере находятся данные тестовой поверхности, а потом, при вызове из функции ReadData, в него действительно попадут данные из файла. Выбор данных из буфера происходит аналогично их записи. Здесь мы пользуемся адресной арифметикой, определяемой типом указателя. Так, операция ++ в применении к указателю типа UINT сдвигает его в памяти на sizeof (UINT) байт. Смена типа указателя (на float*) происходит в тот момент, когда выбраны данные о размерах сетки узлов.
Для надежности сначала проверяем данные из буфера на внутреннюю непротиворечивость в смысле размерностей. Затем мы уничтожаем данные контейнера и генерируем новые на основе содержимого буфера. В процессе генерации трехмерных координат точек их ординаты (Y) масштабируются для того, чтобы график имел пропорции, удобные для просмотра:
void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
{
//====== Готовимся к расшифровке данных буфера
//====== Указываем на него указателем целого типа
UINT *p = (UINT*)buff;
//=== Выбираем данные целого типа, сдвигая указатель
m_xSize = *р; m_zSize = *++p;
//====== Проверка на непротиворечивость
if (m_xSize<2 || m_zSize<2 ||
m_xSize*m_zSize*sizeof(float)
+ 2 * sizeof(UINT) != nSize)
{
MessageBox (_T ("Данные противоречивы") ) ;
return;
}
//====== Изменяем размер контейнера
//====== При этом его данные разрушаются
m_cPoints . resize (m_xSize*m_zSize) ;
if (m_cPoints .empty () )
{
MessageBox (_T ("He возможно разместить данные")
return;
}
//====== Подготовка к циклу пробега по буферу
//====== и процессу масштабирования
float x, z,
//====== Считываем первую ординату
*pf = (float*) ++р,
fMinY = *pf,
fMaxY = *pf,
right = (m_xSize-l) /2 . f ,
left = -right,
read = (m_zSize-l) /2 . f ,
front = -rear,
range = (right + rear) /2. f;
UINTi, j, n;
//====== Вычисление размаха изображаемого объекта
m_fRangeY = range;
m_fRangeX = float (m_xSize) ;
m_fRangeZ = float (m_zSize) ;
//====== Величина сдвига вдоль оси Z
m_zTrans = -1.5f * m_fRangeZ;
//====== Генерируем координаты сетки (X-Z)
//====== и совмещаем с ординатами Y из буфера
for (z=front, i=0, n=0; i<m_zSize; i++, z+=l.f)
{
for (x=left, j=0; j<m_xSize; j++, x+=l.f, n++)
{
MinMax (*pf, fMinY, fMaxY) ;
m_cPoints[n] = CPoint3D(x, z, *pf++) ;
}
}
//====== Масштабирование ординат
float zoom = fMaxY > fMinY ? range/ (fMaxY-fMinY)
: l.f;
for (n=0; n<m_xSize*m_zSize;n++)
{
m_cPoints [n] . у = zoom * (m_cPoints [n] . у - fMinY) - range/2. f;
}
}
При изменении размеров контейнера методом (resize) все его данные разрушаются. В двойном цикле пробега по узлам сетки мы восстанавливаем (генерируем заново) координаты X и Z всех вершин четырехугольников. В отдельном цикле пробега по всему контейнеру происходит масштабирование ординат (умножение на предварительно вычисленный коэффициент zoom). В используемом алгоритме необходимо искать экстремумы функции у = f (x, z). С этой целью удобно иметь глобальную функцию MinMax, которая корректирует значение минимума или максимума, если входной параметр превышает существующие на сей момент экстремумы. Введите тело этой функции в начало файла реализации оконного класса (ChildView.cpp):
inline void MinMax (float d, floats Min, float& Max)
{
//====== Корректируем переданные по ссылке параметры
if (d > Max)
Max = d; // Претендент на максимум
else if (d < Min)
Min = d; // Претендент на минимум
}
  
Чтение данных
В теле следующей функции ReadData мы создадим файловый диалог, в контексте которого пользователь выбирает файл с новыми данными графика, затем вызовем функцию непосредственного чтения данных (DoRead) и создадим новую сцену на основе прочитанных данных. Попутно мы демонстрируем, как обрабатывать ошибки и работать с файловым диалогом, созданным с помощью функций API. Стандартный диалог открытия файла в этом случае более управляем, и ему можно придать множество сравнительно новых стилей. Стиль OFN_EXPLORER работает только в Windows 2000:
void COGView: : ReadData ()
{
//=== Строка, в которую будет помещен файловый путь
TCHAR szFile[MAX_PATH] = { 0 } ;
//====== Строка фильтров демонстрации файлов
TCHAR *szFilter =TEXT ("Graphics Files (*.dat)\0")
TEXT("*.dat\0")
TEXT ("All FilesNO")
TEXT ( " * . * \ 0 " ) ;
//====== Выявляем текущую директорию
TCHAR szCurDir[MAX_PATH] ;
: :GetCurrentDirectory (MAX_PATH-1, szCurDir) ;
//== Структура данных, используемая файловым диалогом
OPENFILENAME ofn;
ZeroMemory (&ofn,sizeof (OPENFILENAME) ) ;
//====== Установка параметров будущего диалога
ofn.lStructSize = sizeof (OPENFILENAME) ;
и * . *, текстовые описания которых можно увидеть в одном из окон стандартного диалога поиска и открытия файлов:
//====== Функция непосредственного чтения данных
bool COGView: : DoRead ( HANDLE hFile) {
//====== Сначала узнаем размер файла
DWORD nSize = GetFileSize (hFile, 0) ;
//=== Если не удалось определить размер, GetFileSize
//====== возвращает 0xFFFFFFFF
if (nSize == 0xFFFFFFFF)
{
GetLastError () ;
MessageBox (_T ("Некорректный размер файла"));
CloseHandle (hFile) ;
return false;
//=== Создаем временный буфер размером в весь файл BYTE
*buff = new BYTE [nSize+1] ;
//====== Обработка отказа выделить память
if (Ibuff) {
MessageBox (_T ("Слишком большой размер файла"))
CloseHandle (hFile) ;
return false;
//====== Реальный размер файла
DWORD nBytes;
//====== Попытка прочесть файл
ReadFile (hFile, buff, nSize, &nBytes, 0) ; CloseHandle (hFile) ;
//====== Если реально прочитано меньшее число байт
if (nSize != nBytes)
{
MessageBox (_T ("Ошибка при чтении файла"));
return false;
}
//====== Генерация точек изображения
SetGraphPoints (buff, nSize) ;
//====== Освобождение временного буфера
delete [] buff;
// ====== Возвращаем успех
return true;
}
В данный момент можно запустить приложение, и оно должно работать. В окне вы должны увидеть изображение поверхности, которое приведено на рис. 7.1. Для создания рисунка мы изменили цвет фона на белый, так как в книге этот вариант считается более предпочтительным. Попробуйте изменить размеры окна. Изображение поверхности должно пропорционально изменить свои размеры. Оцените качество интерполяции цветов внутренних точек примитивов и степень влияния освещения. Позже мы создадим диалог для управления параметрами света и отражающих свойств материала. А сейчас отметим, что напрашивается введение возможности управлять ориентацией и местоположением поверхности с помощью мыши. Для того чтобы убедиться в сложности автомата состояний OpenGL, a также в том, что все в нем взаимосвязано, временно поменяйте местами две строки программы: glVertexSf (xi, yi, zi); и glVertex3f (xn, yn, zn);. Вы найдете их в теле функции DrawScene.

Управление изображением с помощью мыши


Итак, мы собираемся управлять ориентацией изображения с помощью левой кнопки мыши. Перемещение курсора мыши при нажатой кнопке должно вращать изображение наиболее естественным образом, то есть горизонтальное перемещение должно происходить вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то мы будем перемещать (транслировать) изображение вдоль осей X и Y. С помощью правой кнопки будем перемещать изображение вдоль оси Z. Кроме того, с помощью левой кнопки мыши мы дадим возможность придать вращению постоянный характер. Для этого в обработчик WM_LBUTTONUP введем анализ на превышение квантом перемещения (m_dx, m_dy) некоторого порога чувствительности. Если он превышен, то мы запустим таймер, и дальнейшее вращение будем производить с его помощью. Если очередной квант перемещения ниже порога чувствительности, то мы остановим таймер, прекращая вращение. В обработке WM_MOUSEMOVE следует оценивать желаемую скорость вращения, которая является векторной величиной из двух компонентов и должна быть пропорциональна разности двух последовательных координат курсора. Такой алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта. Начнем с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:
void COGView: :OnLButtonDown (UINT nFlags, CPoint point)
{
//====== Останавливаем таймер
KillTimer(1);
//====== Обнуляем кванты перемещения
m_dx = 0.f; m_dy = 0.f;
//====== Захватываем сообщения мыши,
//====== направляя их в свое окно
SetCapture ();
//====== Запоминаем факт захвата
m_bCaptured = true;
//====== Запоминаем координаты курсора
m_pt = point;
}
При нажатии на правую кнопку необходимо выполнить те же действия, что и при нажатии на левую, но дополнительно надо запомнить сам факт нажатия правой кнопки, с тем чтобы правильно интерпретировать последующие сообщения о перемещении указателя мыши и вместо вращения производить сдвиг вдоль оси Z:
void COGView::OnRButtonDown(UINT nFlags, CPoint point)
{
//====== Запоминаем факт нажатия правой кнопки
m_bRightButton = true;
//====== Воспроизводим реакцию на левую кнопку
OnLButtonDown(nFlags, point);
}
В обработчик отпускания левой кнопки мы вводим анализ на необходимость продолжения вращения с помощью таймера. В случае превышения порога чувствительности, запускаем таймер, сообщения от которого будут говорить, что надо продолжать вращение, поддерживая текущее значение скорости:
void COGView::OnLButtonUp(UINT nFlags, CPoint point)
{
//====== Если был захват,
if (m_bCaptured)
//=== то анализируем желаемый квант перемещения
//=== на превышение порога чувствительности
if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)
//=== Включаем режим постоянного вращения
SetTimer(1,33,0);
else
//=== Выключаем режим постоянного вращения
KillTimer(1);
//====== Снимаем флаг захвата мыши
m_bCaptured = false;
//====== Отпускаем сообщения мыши
ReleaseCapture();
}
}
Отпускание правой кнопки должно просто отмечать факт прекращения перемещения вдоль оси Z и отпускать сообщения мыши для того, чтобы они работали на другие окна, в том числе и на наше окно-рамку. Если этого не сделать, то станет невозможным использование меню главного окна. Проверьте, если хотите. Для этого достаточно закомментировать вызов функции ReleaseCapture в обеих функциях:
void COGView::OnRButtonUp(UINT nFlags, CPoint point)
{
//====== Правая кнопка отпущена
m_bRightButton = false;
//====== Снимаем флаг захвата мыши
m_bCaptured = false;
//====== Отпускаем сообщения мыши
ReleaseCapture();
}
Теперь реализуем самую сложную часть алгоритма — реакцию на перемещение курсора. Здесь мы должны оценить желаемую скорость вращения. Она зависит от того, насколько резко пользователь подвинул объект, то есть оценить модуль разности двух последних позиций курсора, В этой же функции надо выделить случай одновременного нажатия служебной клавиши Ctrl Если она нажата, то интерпретация движения мыши при нажатой левой кнопке изменяется. Теперь вместо вращения мы должны сдвигать объект, то есть пропорционально изменять переменные m_xTrans и m_yTrans, которые затем подаются на вход функции glTranslate. Третья ветвь алгоритма обрабатывает движение указателя при нажатой правой кнопке. Здесь необходимо изменять значение переменной m_zTrans, обеспечивая сдвиг объекта вдоль оси Z. Числовые коэффициенты пропорциональности, которые вы видите в коде функции, влияют на чувствительность мыши и подбираются экспериментально. Вы можете изменить их на свой вкус так, чтобы добиться желаемой управляемости изображения:
void COGView::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_bCaptured) // Если был захват,
{
// Вычисляем компоненты желаемой скорости вращения
m_dy = float (point .у - m_pt .у) /40 . f ;
m_dx = float (point .x - m_pt .x) /40. f ;
//====== Если одновременно была нажата Ctrl,
if (nFlags & MK_CONTROL)
{
//=== Изменяем коэффициенты сдвига изображения
m_xTrans += m_dx;
m_yTrans -= m_dy;
}
else
{
//====== Если была нажата правая кнопка
if (m_bRightButton)
//====== Усредняем величину сдвига
m_zTrans += (m_dx + m_dy)/2.f;
else
{
//====== Иначе, изменяем углы поворота
m_AngleX += m_dy;
m_AngleY += m_dx;
}
}
//=== В любом случае запоминаем новое положение мыши
m_pt = point; Invalidate (FALSE) ;
}
}
Запустите и проверьте управляемость объекта. Введите коррективы чувствительности на свой вкус. Попробуйте скорректировать эффект влияния поворота вокруг оси X на интерпретацию знака желаемого вращения вокруг оси Y. Здесь можно воспользоваться стеком матриц моделирования. Теперь добавим код в заготовку функции реакции на сообщения таймера с тем, чтобы ввести фиксацию состояния вращения.
  
Включаем анимацию
Реакция на сообщение о том, что истек очередной квант времени в 33 миллисекунды (именно такую установку мы сделали в OnLButtonUp) выглядит очень просто. Увеличиваем углы поворота изображения на те кванты, которые вычислили в функции OnMouseMove и вызываем перерисовку окна. Так как при непрерывном вращении углы постоянно растут, то можно искусственно реализовать естественную их периодичность с циклом в 360 градусов. Однако с этой задачей успешно справляется OpenGL, и вы можете убрать код ограничения углов:
void COGView: :OnTimer (UINT nIDEvent)
{
//====== Если это был наш таймер
if (nIDEvent==l)
{
//====== Увеличиваем углы поворота
m_AngleX += m_dy;
m_AngleY += m_dx;
//====== Ограничители роста углов
if (m_AngleX > 360)
m_AngleX -= 360;
if (m_AngleX <-360)
m_AngleX += 360;
if (m_AngleY > 360)
m_AngleY -=360;
if (m_AngleY <-360)
m_AngleY +=360;
//====== Просим перерисовать окно
Invalidate(FALSE);
}
else
//=== Каркас приложения обработает другие таймеры
CView::OnTimer(nIDEvent);
}
Запустите и протестируйте приложение. Скорректируйте, если необходимо, коэффициенты чувствительности.
  
Ввод новых команд
Вы заметили, что до сих пор обходились без каких-либо ресурсов. Мы не учитываем традиционный диалог About, планку меню главного окна, панель инструментов, две таблицы (строк и ускорителей) и два значка, которые присутствовали в каркасе приложения изначально. Дальнейшее развитие потребует ввести новые ресурсы. Главным из них будет диалог, который мы запустим в немодальном режиме и который позволит подробно исследовать влияние параметров освещения на качество изображения. Начинать, как обычно, следует с команд меню. Скорректируйте меню главного окна так, чтобы в нем появились новые команды:

  • Edit > Properties (ID_EDIT_PROPERTIES);
  • Edit > Background (ID_EDIT_BACKGROUND);
  • View > Fill (ID_VIEW_FILL);
  • View > Quad (ID_VIEW_QUAD).

Одновременно удалите не используемые нами команды: File > New, File > Open, File > Save, File > Save as, File > Recent File, Edit > Undo, Edit > Cut, Edit > Copy и Edit > Paste.
Примечание
Вы, конечно, знаете, что идентификаторы команд можно не задавать. Они генерируются автоматически при перемещении фокуса от вновь созданной команды к любой другой.
После этого в классе cocview создайте обработчики всех новых команд с именами по умолчанию (их предлагает Studio.Net). При создании реакций на эти команды меню (COGView > Properties > Events) предварительно раскройте все необходимые элементы в дереве Properties t Commands. Одновременно с функциями обработки типа COMMAND создайте (для всех команд, кроме Edit > Background) функции обновления пользовательского интерфейса, то есть функции обработки типа UPDATE_ COMMANDJJI. Они, как вы помните, следят за состоянием команд меню и соответствующих им кнопок панели управления, обновляя интерфейс пользователя. Команды становятся доступными или, наоборот, в зависимости признака, управляемого програмистом.
В обработчике OnEditBackground мы вызовем стандартный диалог по выбору цвета, сразу открыв обе его страницы (см. флаг CC_FULLOPEN). С помощью этого диалога пользователь сможет изменить цвет фона:
void COGView::OnEditBackground(void)
{
//====== Создаем объект диалогового класса
CColorDialog dig(m_BkClr); //====== Устанавливаем бит стиля
dig.m_cc.Flags |= CC_FULLOPEN;
//====== Запускаем диалог и выбираем результат
if (cilg.DoModal ()==IDOK)
{
m_BkClr = dig.m_cc.rgbResuit;
//====== Изменяем цвет фона
SetBkColor();
Invalidate(FALSE);
}
}
Проверьте результат, запустив приложение и вызвав диалог. При желании создайте глобальный массив с 16 любимыми цветами и присвойте его адрес переменной lpCustColors, которая входит в состав полей структуры m_сс, являющейся членом класса CColorDialog. В этом случае пользователь сможет подобрать и запомнить некоторые цвета.
В обработчик OnViewQuad введите коды, инвертирующие булевский признак m_bQuad, который мы используем как флаг необходимости рисования отдельными четырехугольниками (GL_QUADS), и заново создают изображение. Если признак инвертирован, то мы рисуем полосами (GL_QUAD_STRIP):
void COGView::OnViewQuad(void)
{
// Инвертируем признак стиля задания четырехугольников
m_bQuad = ! m_bQuad;
//====== Заново создаем изображение
DrawScene (); Invalidate(FALSE); UpdateWindow();
}
В обработчик команды обновления интерфейса введите коды, которые обеспечивают появление маркера выбора рядом с командой меню (или залипания кнопки панели управления):
void COGView::OnUpdateViewQuad(CCmdUI* pCmdUI)
{
//====== Вставляем или убираем маркер (пометку)
pCmdUI->SetCheck(m_bQuad==true);
}
Проверьте результат и попробуйте объяснить зубчатые края поверхности (рис. 7.2). Не знаю, правильно ли я поступаю, когда по ходу изложения вставляю задачи подобного рода. Но мной движет желание немного приоткрыть дверь в кухню разработчика и показать, что все не так уж просто. Искать ошибки в алгоритме, особенно чужом, является очень кропотливым занятием. Однако совершенно необходимо приобрести этот навык, так как без него невозможна работа в команде, а также восприятие новых технологий, раскрываемых в основном посредством анализа содержательных (чужих) примеров (Samples). Чтобы обнаружить ошибку подобного рода, надо тщательно проанализировать код, в котором создается изображение (ветвь GL_QUAD_STRIP), и понять, что неправильно выбран индекс вершины. Замените строку givertex3f (xn, yn, zn); HaglVertexSf (xi, yi, zi); и вновь проверьте работу приложения. Зубчатость края должна исчезнуть, но в алгоритме, тем не менее, осталась еще небольшая, слабо заметная неточность. Ее обнаружение и исправление я оставляю вам, дорогой читатель.
Обработку следующей команды меню мы проведем в том же стиле, за исключением того, что переменная m_FillMode не является булевской, хоть и принимает лишь два значения (GL_FILL и GL_LINE). Из материала предыдущей главы помните, возможен еще одни режим изображения полигонов — GL_POINT. Логику его реализации при желании вы введете самостоятельно, а сейчас введите коды двух функции обработки команды меню:
void COGView::OnViewFill(void)
{
//=== Переключаем режим заполнения четырехугольника
m_FillMode = m_FillMode==GL_FILL ? GL_LINE : GL__FILL;
//====== Заново создаем изображение
DrawScene();
Invalidate(FALSE);
UpdateWindow() ;
}
void COGView::OnUpdateViewFill(CCmdUI *pCmdUI)
{
//====== Вставляем или убираем маркер выбора
pCmdUI->SetCheck(m_FillMode==GL_FILL) ;
}
Запустите и проверьте работу команд меню. Отметьте, что формула учета освещения работает и в случае каркасного изображения примитивов (рис. 7.3).
Для обмена с диалогом по управлению освещением нам понадобятся две вспомогательные функции GetLightParams и SetLightParam. Назначение первой из которых заполнить массив переменных, отражающих текущее состояние параметров освещения сцены OpenGL. Затем этот массив мы передадим в метод диалогового класса для синхронизации движков (sliders) управления. Вторая функция позволяет изменить отдельный параметр и привести его в соответствие с положением движка. Так как мы насчитали 11 параметров, которыми хотим управлять, то придется ввести в окно диалога 11 регуляторов, которым соответствует массив m_LightPaxam из 11 элементов. Массив уже помещен в класс COGView, нам осталось лишь задействовать его:
void COGView: :GetLightParams (int *pPos)
{
//====== Проход по всем регулировкам
for (int i=0; i<ll; i++)
//====== Заполняем транспортный массив pPos
pPos[i] = m_LightParam[i] ;
void COGView: :SetLightParam (short Ip, int nPos)
{ //====== Синхронизируем параметр lp и
//====== устанавливаем его в положение nPos
m_LightParam[lp] = nPos;
//=== Перерисовываем представление с учетом изменений
Invalidate (FALSE) ;
}
  
Диалог по управлению светом

В окне редактора диалогов (Resource View > Dialog > Контекстное меню > Insert Dialog) создайте окно диалога по управлению светом, которое должно иметь такой вид:
Обратите внимание на то, что справа от каждого движка расположен элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме. Три регулятора (элемента типа Slider Control) в левом верхнем углу окна диалога предназначены для управления свойствами света. Группа регуляторов справа от них поможет пользователю изменить координаты источника света. Группа регуляторов, объединенная рамкой (типа Group Box) с заголовком Material, служит для изменения отражающих свойств материала. Кнопка с надписью Data File позволит пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Для диалогов, предназначенных для работы в немодальном режиме, необходимо установить стиль Visible. Сделайте это в окне Properties > Behavior. Идентификаторы элементов управления мы сведем в табл. 7.1.
Таблица 7.1. Идентификаторы элементов управления


Элемент

Идентификатор

Диалог

IDD_PROP

Ползунок Ambient в группе Light

IDC_AMBIENT

Ползунок Diffuse в группе Light

IDC_DIFFUSE

Ползунок Specular в группе Light

IDC_SPECULAR

; Static Text справа от Ambient в группе Light

IDC_AMB_TEXT

, Static Text справа от Diffuse в группе Light

IDC_DIFFUSE_TEXT

Static Text справа от Specular в группе Light

IDC_SPECULAR_TEXT

Ползунок Ambient в группе Material

IDC_AMBMAT

Ползунок Diffuse в группе Material

IDC_DIFFMAT

' Ползунок Specular в группе Material

IDC_SPECMAT

f Static Text справа от Ambient в группе Material

IDC_AMBMAT_TEXT

:! Static Text справа от Diffuse. в группе Material

IDC_DIFFMATJFEXT

; Static Text справа от Specular в группе Material

IDC_SPECMAT_TEXT

Ползунок Shim'ness

IDC_SHINE

Ползунок Emission

IDC_EMISSION

« Static Text справа от Shininess

IDC_SHINE_TEXT

Static Text справа от Emission

IDC_EMISSION_TEXT

Ползунок X

IDC_XPOS

| Ползунок Y

IDC_YPOS

1 Ползунок Z

IDC_ZPOS

Static Text справа от X

IDC_XPOS_TEXT

Static Text справа от Y

IDC_YPOS_TEXT

Static Text справа от Z

IDC_ZPOS_TEXT

Кнопка Data File

IDC_FILENAME

Диалоговый класс


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

  1. Выберите в контекстном меню команду Add Class.
  2. В левом окне диалога Add Class раскройте дерево Visual C++, сделайте выбор MFC > MFC Class и нажмите кнопку Open.
  3. В окне мастера MFC Class Wizard задайте имя класса CPropDlg, в качестве базового класса выберите CDialog. При этом станет доступным ноле Dialog ID.
  4. В это поле введите или выберите из выпадающего списка идентификатор шаблона диалога 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, который удобен для хранения вершин изображаемой поверхности;
  • убедились, что использование списка команд OpenGL повышает эффективность передачи сложного изображения;
  • применили формулу вычисления нормали к поверхности и убедились в необходимости уделять этой проблеме достаточное внимание;
  • научились управлять освещенностью сцены OpenGL с помощью группы регуляторов;

оценили удобство управления группой регуляторов типа slider Control в функции обработки сообщения о прокрутке WM_HSCROLL.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика